Merge pull request #2759 from igouss/fix/tool-handlers-bypass-db-port-2726

refactor(gsd): wire tool handlers through DB port layer, remove _getAdapter from all tools
This commit is contained in:
TÂCHES 2026-03-26 17:12:24 -06:00 committed by GitHub
commit 473583d349
7 changed files with 57 additions and 89 deletions

View file

@ -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)

View file

@ -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}` };
}

View file

@ -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();

View file

@ -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();

View file

@ -6,7 +6,6 @@ import {
insertSlice,
upsertMilestonePlanning,
upsertSlicePlanning,
_getAdapter,
} from "../gsd-db.js";
import { invalidateStateCache } from "../state.js";
import { renderRoadmapFromDb } from "../markdown-renderer.js";

View file

@ -7,7 +7,6 @@ import {
upsertSlicePlanning,
upsertTaskPlanning,
insertGateRow,
_getAdapter,
} from "../gsd-db.js";
import type { GateId } from "../types.js";
import { invalidateStateCache } from "../state.js";

View file

@ -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}` };
}