diff --git a/src/resources/extensions/gsd/gsd-db.ts b/src/resources/extensions/gsd/gsd-db.ts index 7403baa6a..8aba2d085 100644 --- a/src/resources/extensions/gsd/gsd-db.ts +++ b/src/resources/extensions/gsd/gsd-db.ts @@ -1308,6 +1308,20 @@ export function updateSliceStatus(milestoneId: string, sliceId: string, status: }); } +export function setTaskSummaryMd(milestoneId: string, sliceId: string, taskId: string, md: string): void { + if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); + currentDb.prepare( + `UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`, + ).run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId, ":md": md }); +} + +export function setSliceSummaryMd(milestoneId: string, sliceId: string, summaryMd: string, uatMd: string): void { + if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); + currentDb.prepare( + `UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`, + ).run({ ":mid": milestoneId, ":sid": sliceId, ":summary_md": summaryMd, ":uat_md": uatMd }); +} + export interface TaskRow { milestone_id: string; slice_id: string; @@ -1490,11 +1504,11 @@ export function getMilestone(id: string): MilestoneRow | null { * Used by park/unpark to keep the DB in sync with the filesystem marker. * See: https://github.com/gsd-build/gsd-2/issues/2694 */ -export function updateMilestoneStatus(milestoneId: string, status: string): void { +export function updateMilestoneStatus(milestoneId: string, status: string, completedAt?: string | null): void { if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); currentDb.prepare( - `UPDATE milestones SET status = :status WHERE id = :id`, - ).run({ ":status": status, ":id": milestoneId }); + `UPDATE milestones SET status = :status, completed_at = :completed_at WHERE id = :id`, + ).run({ ":status": status, ":completed_at": completedAt ?? null, ":id": milestoneId }); } export function getActiveMilestoneFromDb(): MilestoneRow | null { @@ -1706,6 +1720,20 @@ export function insertAssessment(entry: { }); } +export function deleteAssessmentByScope(milestoneId: string, scope: string): void { + if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); + currentDb.prepare( + `DELETE FROM assessments WHERE milestone_id = :mid AND scope = :scope`, + ).run({ ":mid": milestoneId, ":scope": scope }); +} + +export function deleteVerificationEvidence(milestoneId: string, sliceId: string, taskId: string): void { + if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); + currentDb.prepare( + `DELETE FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid`, + ).run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId }); +} + export function deleteTask(milestoneId: string, sliceId: string, taskId: string): void { if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); // Must delete verification_evidence first (FK constraint) diff --git a/src/resources/extensions/gsd/tools/complete-milestone.ts b/src/resources/extensions/gsd/tools/complete-milestone.ts index 0c71e66de..b9077bb35 100644 --- a/src/resources/extensions/gsd/tools/complete-milestone.ts +++ b/src/resources/extensions/gsd/tools/complete-milestone.ts @@ -14,7 +14,7 @@ import { getMilestone, getMilestoneSlices, getSliceTasks, - _getAdapter, + updateMilestoneStatus, } from "../gsd-db.js"; import { resolveMilestonePath, clearPathCache } from "../paths.js"; import { saveFile, clearParseCache } from "../files.js"; @@ -165,13 +165,7 @@ export async function handleCompleteMilestone( } // All guards passed — perform write - const adapter = _getAdapter()!; - adapter.prepare( - `UPDATE milestones SET status = 'complete', completed_at = :completed_at WHERE id = :mid`, - ).run({ - ":completed_at": completedAt, - ":mid": params.milestoneId, - }); + updateMilestoneStatus(params.milestoneId, 'complete', completedAt); }); if (guardError) { @@ -199,12 +193,7 @@ export async function handleCompleteMilestone( process.stderr.write( `gsd-db: complete_milestone — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`, ); - const rollbackAdapter = _getAdapter(); - if (rollbackAdapter) { - rollbackAdapter.prepare( - `UPDATE milestones SET status = 'active', completed_at = NULL WHERE id = :mid`, - ).run({ ":mid": params.milestoneId }); - } + updateMilestoneStatus(params.milestoneId, 'active', null); invalidateStateCache(); return { error: `disk render failed: ${(renderErr as Error).message}` }; } diff --git a/src/resources/extensions/gsd/tools/complete-slice.ts b/src/resources/extensions/gsd/tools/complete-slice.ts index ae2cf4a30..cf1adb2d8 100644 --- a/src/resources/extensions/gsd/tools/complete-slice.ts +++ b/src/resources/extensions/gsd/tools/complete-slice.ts @@ -19,7 +19,7 @@ import { getSliceTasks, getMilestone, updateSliceStatus, - _getAdapter, + setSliceSummaryMd, } from "../gsd-db.js"; import { resolveSliceFile, resolveSlicePath, clearPathCache } from "../paths.js"; import { checkOwnership, sliceUnitKey } from "../unit-ownership.js"; @@ -299,31 +299,13 @@ export async function handleCompleteSlice( process.stderr.write( `gsd-db: complete_slice — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`, ); - const rollbackAdapter = _getAdapter(); - if (rollbackAdapter) { - rollbackAdapter.prepare( - `UPDATE slices SET status = 'pending' WHERE milestone_id = :mid AND id = :sid`, - ).run({ - ":mid": params.milestoneId, - ":sid": params.sliceId, - }); - } + updateSliceStatus(params.milestoneId, params.sliceId, 'pending'); invalidateStateCache(); return { error: `disk render failed: ${(renderErr as Error).message}` }; } // Store rendered markdown in DB for D004 recovery - const adapter = _getAdapter(); - if (adapter) { - adapter.prepare( - `UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`, - ).run({ - ":summary_md": summaryMd, - ":uat_md": uatMd, - ":mid": params.milestoneId, - ":sid": params.sliceId, - }); - } + setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd); // Invalidate all caches invalidateStateCache(); diff --git a/src/resources/extensions/gsd/tools/complete-task.ts b/src/resources/extensions/gsd/tools/complete-task.ts index cc543f993..fc0e3a005 100644 --- a/src/resources/extensions/gsd/tools/complete-task.ts +++ b/src/resources/extensions/gsd/tools/complete-task.ts @@ -20,7 +20,9 @@ import { getMilestone, getSlice, getTask, - _getAdapter, + updateTaskStatus, + setTaskSummaryMd, + deleteVerificationEvidence, } from "../gsd-db.js"; import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js"; import { checkOwnership, taskUnitKey } from "../unit-ownership.js"; @@ -248,42 +250,17 @@ export async function handleCompleteTask( process.stderr.write( `gsd-db: complete_task — disk render failed, rolling back DB status: ${(renderErr as Error).message}\n`, ); - const rollbackAdapter = _getAdapter(); - if (rollbackAdapter) { - // Delete orphaned verification_evidence rows first (FK constraint - // references tasks, so evidence must go before status change). - // Without this, retries accumulate duplicate evidence rows (#2724). - rollbackAdapter.prepare( - `DELETE FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid`, - ).run({ - ":mid": params.milestoneId, - ":sid": params.sliceId, - ":tid": params.taskId, - }); - rollbackAdapter.prepare( - `UPDATE tasks SET status = 'pending' WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`, - ).run({ - ":mid": params.milestoneId, - ":sid": params.sliceId, - ":tid": params.taskId, - }); - } + // Delete orphaned verification_evidence rows first (FK constraint + // references tasks, so evidence must go before status change). + // Without this, retries accumulate duplicate evidence rows (#2724). + deleteVerificationEvidence(params.milestoneId, params.sliceId, params.taskId); + updateTaskStatus(params.milestoneId, params.sliceId, params.taskId, 'pending'); invalidateStateCache(); return { error: `disk render failed: ${(renderErr as Error).message}` }; } // Store rendered markdown in DB for D004 recovery - const adapter = _getAdapter(); - if (adapter) { - adapter.prepare( - `UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`, - ).run({ - ":md": summaryMd, - ":mid": params.milestoneId, - ":sid": params.sliceId, - ":tid": params.taskId, - }); - } + setTaskSummaryMd(params.milestoneId, params.sliceId, params.taskId, summaryMd); // Invalidate all caches invalidateStateCache(); diff --git a/src/resources/extensions/gsd/tools/plan-milestone.ts b/src/resources/extensions/gsd/tools/plan-milestone.ts index 44749a5ce..7cea0212d 100644 --- a/src/resources/extensions/gsd/tools/plan-milestone.ts +++ b/src/resources/extensions/gsd/tools/plan-milestone.ts @@ -6,7 +6,6 @@ import { insertSlice, upsertMilestonePlanning, upsertSlicePlanning, - _getAdapter, } from "../gsd-db.js"; import { invalidateStateCache } from "../state.js"; import { renderRoadmapFromDb } from "../markdown-renderer.js"; diff --git a/src/resources/extensions/gsd/tools/plan-slice.ts b/src/resources/extensions/gsd/tools/plan-slice.ts index bb5ab8f3f..0f8e06a38 100644 --- a/src/resources/extensions/gsd/tools/plan-slice.ts +++ b/src/resources/extensions/gsd/tools/plan-slice.ts @@ -7,7 +7,6 @@ import { upsertSlicePlanning, upsertTaskPlanning, insertGateRow, - _getAdapter, } from "../gsd-db.js"; import type { GateId } from "../types.js"; import { invalidateStateCache } from "../state.js"; diff --git a/src/resources/extensions/gsd/tools/validate-milestone.ts b/src/resources/extensions/gsd/tools/validate-milestone.ts index d34fd69fe..d38d8cf16 100644 --- a/src/resources/extensions/gsd/tools/validate-milestone.ts +++ b/src/resources/extensions/gsd/tools/validate-milestone.ts @@ -9,7 +9,8 @@ import { join } from "node:path"; import { transaction, - _getAdapter, + insertAssessment, + deleteAssessmentByScope, } from "../gsd-db.js"; import { resolveMilestonePath, clearPathCache } from "../paths.js"; import { saveFile, clearParseCache } from "../files.js"; @@ -97,16 +98,14 @@ export async function handleValidateMilestone( const validatedAt = new Date().toISOString(); transaction(() => { - const adapter = _getAdapter()!; - adapter.prepare( - `INSERT OR REPLACE INTO assessments (path, milestone_id, slice_id, task_id, status, scope, full_content, created_at) - VALUES (:path, :mid, NULL, NULL, :verdict, 'milestone-validation', :content, :created_at)`, - ).run({ - ":path": validationPath, - ":mid": params.milestoneId, - ":verdict": params.verdict, - ":content": validationMd, - ":created_at": validatedAt, + insertAssessment({ + path: validationPath, + milestoneId: params.milestoneId, + sliceId: null, + taskId: null, + status: params.verdict, + scope: 'milestone-validation', + fullContent: validationMd, }); }); @@ -118,12 +117,7 @@ export async function handleValidateMilestone( process.stderr.write( `gsd-db: validate_milestone — disk render failed, rolling back DB row: ${(renderErr as Error).message}\n`, ); - const rollbackAdapter = _getAdapter(); - if (rollbackAdapter) { - rollbackAdapter.prepare( - `DELETE FROM assessments WHERE milestone_id = :mid AND scope = 'milestone-validation'`, - ).run({ ":mid": params.milestoneId }); - } + deleteAssessmentByScope(params.milestoneId, 'milestone-validation'); return { error: `disk render failed: ${(renderErr as Error).message}` }; }