fix(P0): close 15 blockers + add 26 regression tests; fix wiring schema regression
Phase A (security red lines) — CLOSED: - B8: 3x command injection fixed (execFileSync + args array in CMake/CppBuilder/Cppcheck) - B6: ToolRegistry permission bypass fixed (real task_scope/profile passed) - B7: ACTION_BRANCHES this-binding crash fixed (instance method) - B17: DeveloperLogEncryptor hardcoded 'dev-key' removed (throws if no key) - B22: CommandRiskAnalyzer 'in' operator bug fixed (includes) - B1: EventStore.project() transaction handle now passed to all repos - B2: workspace projection illegal enum fixed (active/merged) - B4: route_prefix separator unified to '/' - B5: TaskAttempt column mapping fixed Other blockers fixed: - B3: project-level DB schema aligned to db-schema §20 (.air/local, learned_memories) - B9: cpp.* tools registered through PermissionEngine path - B11: Scheduler BLOCKED/CANCELLED states added - B18: CapabilityTrustLevel 5-level enum aligned - B19: PermissionEngine block/refuse/announce_then_run + grant_scope - B20: Worker exit code 4 = parent_cancelled - B24: project_id now randomUUID Regression fix (introduced by B3 schema refactor): - wiring.ts capture_debug_record/promote_memory_entry realigned to refactored DebugRecord/MemoryEntry interfaces (was compile-level decoupling) Tests: 128 regression/unit tests pass (22 regression + 3 unit + 3 e2e suites) Still open (tracked for next round): B10 (INV-2 outbox emit), B12 (Scheduler event projection), B13 (MainAgent LLM classify), B14 (IPC envelope fields), B15 (TUI OpenTUI), B16 (api_key strict), B21 (CLI init INV-3), B23 (e2e real), B25 (MVP tools), B26 (ContextAssembler L6-L9) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,15 +11,14 @@ import { Database } from 'bun:sqlite'
|
||||
|
||||
export interface MemoryEntry {
|
||||
id: string
|
||||
type: 'pattern' | 'rule' | 'skill' | 'experience'
|
||||
title: string
|
||||
memory_type: 'project_rule' | 'toolchain_rule' | 'skill_update' | 'debug_experience'
|
||||
summary: string
|
||||
content: string
|
||||
source_task_ids: string
|
||||
project_id: string
|
||||
status: 'draft' | 'promoted' | 'archived'
|
||||
source_entity_type?: string
|
||||
source_entity_id?: string
|
||||
status: 'candidate' | 'promoted' | 'archived' | 'rejected'
|
||||
created_at: string
|
||||
promoted_at?: string
|
||||
archived_at?: string
|
||||
updated_at: string
|
||||
metadata_json?: string
|
||||
}
|
||||
|
||||
@@ -28,7 +27,7 @@ export class LearnedMemoryStore {
|
||||
private db_path: string
|
||||
|
||||
constructor(project_root: string) {
|
||||
this.db_path = join(project_root, '.air', 'shared', 'learned-memory.db')
|
||||
this.db_path = join(project_root, '.air', 'local', 'learned-memory.db')
|
||||
}
|
||||
|
||||
open(): void {
|
||||
@@ -36,52 +35,54 @@ export class LearnedMemoryStore {
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
||||
|
||||
this.db = new Database(this.db_path)
|
||||
this.db.exec('PRAGMA journal_mode = WAL')
|
||||
this.db.exec('PRAGMA synchronous = NORMAL')
|
||||
this.db.exec('PRAGMA foreign_keys = OFF')
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS learned_memory (
|
||||
CREATE TABLE IF NOT EXISTS learned_memories (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
memory_type TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
source_task_ids TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'draft',
|
||||
source_entity_type TEXT,
|
||||
source_entity_id TEXT,
|
||||
status TEXT DEFAULT 'candidate',
|
||||
created_at TEXT NOT NULL,
|
||||
promoted_at TEXT,
|
||||
archived_at TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
metadata_json TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_type ON learned_memory(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_status ON learned_memory(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_type ON learned_memories(memory_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_status ON learned_memories(status);
|
||||
`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a memory entry.
|
||||
* INV-2: External write first → then emit memory.promoted via outbox.
|
||||
* INV-2: External write first, then emit memory.promoted via outbox.
|
||||
*/
|
||||
insert(entry: MemoryEntry): void {
|
||||
if (!this.db) throw new Error('Store not opened')
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO learned_memory (id, type, title, content, source_task_ids, project_id, status, created_at, promoted_at, archived_at, metadata_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO learned_memories (id, memory_type, summary, content, source_entity_type, source_entity_id, status, created_at, updated_at, metadata_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
stmt.run(entry.id, entry.type, entry.title, entry.content, entry.source_task_ids, entry.project_id, entry.status, entry.created_at, entry.promoted_at, entry.archived_at, entry.metadata_json)
|
||||
stmt.run(entry.id, entry.memory_type, entry.summary, entry.content, entry.source_entity_type, entry.source_entity_id, entry.status, entry.created_at, entry.updated_at, entry.metadata_json)
|
||||
}
|
||||
|
||||
lookup_by_type(type: string): MemoryEntry[] {
|
||||
lookup_by_type(memory_type: string): MemoryEntry[] {
|
||||
if (!this.db) return []
|
||||
return this.db.prepare('SELECT * FROM learned_memory WHERE type = ? AND status != ? ORDER BY created_at DESC').all(type, 'archived') as MemoryEntry[]
|
||||
return this.db.prepare('SELECT * FROM learned_memories WHERE memory_type = ? AND status != ? ORDER BY created_at DESC').all(memory_type, 'archived') as MemoryEntry[]
|
||||
}
|
||||
|
||||
update_status(id: string, status: 'promoted' | 'archived'): void {
|
||||
update_status(id: string, status: 'candidate' | 'promoted' | 'archived' | 'rejected'): void {
|
||||
if (!this.db) return
|
||||
const field = status === 'promoted' ? 'promoted_at' : 'archived_at'
|
||||
this.db.prepare(`UPDATE learned_memory SET status = ?, ${field} = ? WHERE id = ?`).run(status, new Date().toISOString(), id)
|
||||
const updated_at = new Date().toISOString()
|
||||
this.db.prepare('UPDATE learned_memories SET status = ?, updated_at = ? WHERE id = ?').run(status, updated_at, id)
|
||||
}
|
||||
|
||||
scan_stale(days_stale: number = 90): MemoryEntry[] {
|
||||
if (!this.db) return []
|
||||
const cutoff = new Date(Date.now() - days_stale * 86400000).toISOString()
|
||||
return this.db.prepare('SELECT * FROM learned_memory WHERE status = ? AND promoted_at < ?').all('promoted', cutoff) as MemoryEntry[]
|
||||
return this.db.prepare('SELECT * FROM learned_memories WHERE status = ? AND updated_at < ?').all('promoted', cutoff) as MemoryEntry[]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user