From 5ab375b773d10dfe628aa742b5b49894b1c9ae95 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:59:46 +0100 Subject: [PATCH] fix: make transaction() re-entrant and add slice_dependencies to initSchema Two bugs fixed: 1. transaction() now tracks nesting depth. When deleteTask/deleteSlice (which wrap in transaction()) are called from within an outer transaction() in reassess-roadmap.ts or replan-slice.ts, the inner call skips BEGIN/COMMIT since SQLite doesn't support nested transactions. This fixes: - reassess-handler.test.ts: 3 failing tests - replan-handler.test.ts: 4 failing tests All errors were: 'cannot start a transaction within a transaction' 2. slice_dependencies table and v13/v14 indexes were only created in migrateSchema (for upgrades from older versions) but missing from initSchema (for fresh databases). New databases started at schema version 14 but never created the table, causing 'no such table: slice_dependencies' when deleteSlice was called. --- src/resources/extensions/gsd/gsd-db.ts | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/resources/extensions/gsd/gsd-db.ts b/src/resources/extensions/gsd/gsd-db.ts index b7bf10cd8..d8487cd7c 100644 --- a/src/resources/extensions/gsd/gsd-db.ts +++ b/src/resources/extensions/gsd/gsd-db.ts @@ -387,9 +387,31 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void { ) `); + // Slice dependency junction table (v14) + db.exec(` + CREATE TABLE IF NOT EXISTS slice_dependencies ( + milestone_id TEXT NOT NULL, + slice_id TEXT NOT NULL, + depends_on_slice_id TEXT NOT NULL, + PRIMARY KEY (milestone_id, slice_id, depends_on_slice_id), + FOREIGN KEY (milestone_id, slice_id) REFERENCES slices(milestone_id, id), + FOREIGN KEY (milestone_id, depends_on_slice_id) REFERENCES slices(milestone_id, id) + ) + `); + db.exec("CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)"); db.exec("CREATE INDEX IF NOT EXISTS idx_replan_history_milestone ON replan_history(milestone_id, created_at)"); + // v13 indexes — hot-path dispatch queries + db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_active ON tasks(milestone_id, slice_id, status)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_slices_active ON slices(milestone_id, status)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_milestones_status ON milestones(status)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_quality_gates_pending ON quality_gates(milestone_id, slice_id, status)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_verification_evidence_task ON verification_evidence(milestone_id, slice_id, task_id)"); + + // v14 index — slice dependency lookups + db.exec("CREATE INDEX IF NOT EXISTS idx_slice_deps_target ON slice_dependencies(milestone_id, depends_on_slice_id)"); + db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`); db.exec(`CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`); db.exec(`CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`); @@ -800,8 +822,23 @@ export function vacuumDatabase(): void { } catch { /* non-fatal */ } } +let _txDepth = 0; + export function transaction(fn: () => T): T { if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); + + // Re-entrant: if already inside a transaction, just run fn() without + // starting a new one. SQLite does not support nested BEGIN/COMMIT. + if (_txDepth > 0) { + _txDepth++; + try { + return fn(); + } finally { + _txDepth--; + } + } + + _txDepth++; currentDb.exec("BEGIN"); try { const result = fn(); @@ -810,6 +847,8 @@ export function transaction(fn: () => T): T { } catch (err) { currentDb.exec("ROLLBACK"); throw err; + } finally { + _txDepth--; } }