The forensics command showed "Completed Keys: 0" because it read from completed-units.json, which is never populated during normal auto-mode completion. Now queries the DB (milestones/slices/tasks tables) for authoritative completion counts, falling back to the legacy file only when DB is unavailable. Also fixes STATE.md showing "Active Milestone" for completed milestones — now shows "Last Completed Milestone" when phase is complete. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dff73009c8
commit
dae746b905
5 changed files with 171 additions and 8 deletions
|
|
@ -28,6 +28,8 @@ import { deriveState } from "./state.js";
|
|||
import { isAutoActive } from "./auto.js";
|
||||
import { loadPrompt } from "./prompt-loader.js";
|
||||
import { gsdRoot } from "./paths.js";
|
||||
import { isDbAvailable, getAllMilestones, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
||||
import { isClosedStatus } from "./status-guards.js";
|
||||
import { formatDuration } from "../shared/format-utils.js";
|
||||
import { getAutoWorktreePath } from "./auto-worktree.js";
|
||||
import { loadEffectiveGSDPreferences, loadGlobalGSDPreferences, getGlobalGSDPreferencesPath } from "./preferences.js";
|
||||
|
|
@ -85,6 +87,15 @@ interface JournalSummary {
|
|||
fileCount: number;
|
||||
}
|
||||
|
||||
interface DbCompletionCounts {
|
||||
milestones: number;
|
||||
milestonesTotal: number;
|
||||
slices: number;
|
||||
slicesTotal: number;
|
||||
tasks: number;
|
||||
tasksTotal: number;
|
||||
}
|
||||
|
||||
interface ForensicReport {
|
||||
gsdVersion: string;
|
||||
timestamp: string;
|
||||
|
|
@ -95,6 +106,7 @@ interface ForensicReport {
|
|||
unitTraces: UnitTrace[];
|
||||
metrics: MetricsLedger | null;
|
||||
completedKeys: string[];
|
||||
dbCompletionCounts: DbCompletionCounts | null;
|
||||
crashLock: LockData | null;
|
||||
doctorIssues: DoctorIssue[];
|
||||
anomalies: ForensicAnomaly[];
|
||||
|
|
@ -276,8 +288,9 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
|
|||
// 3. Load metrics
|
||||
const metrics = loadLedgerFromDisk(basePath);
|
||||
|
||||
// 4. Load completed keys
|
||||
// 4. Load completed keys (legacy) and DB completion counts
|
||||
const completedKeys = loadCompletedKeys(basePath);
|
||||
const dbCompletionCounts = getDbCompletionCounts();
|
||||
|
||||
// 5. Check crash lock
|
||||
const crashLock = readCrashLock(basePath);
|
||||
|
|
@ -336,6 +349,7 @@ export async function buildForensicReport(basePath: string): Promise<ForensicRep
|
|||
unitTraces,
|
||||
metrics,
|
||||
completedKeys,
|
||||
dbCompletionCounts,
|
||||
crashLock,
|
||||
doctorIssues,
|
||||
anomalies,
|
||||
|
|
@ -586,6 +600,44 @@ function loadCompletedKeys(basePath: string): string[] {
|
|||
return [];
|
||||
}
|
||||
|
||||
// ─── DB Completion Counts ────────────────────────────────────────────────────
|
||||
|
||||
function getDbCompletionCounts(): DbCompletionCounts | null {
|
||||
if (!isDbAvailable()) return null;
|
||||
|
||||
const milestones = getAllMilestones();
|
||||
let completedMilestones = 0;
|
||||
let totalSlices = 0;
|
||||
let completedSlices = 0;
|
||||
let totalTasks = 0;
|
||||
let completedTasks = 0;
|
||||
|
||||
for (const m of milestones) {
|
||||
if (isClosedStatus(m.status)) completedMilestones++;
|
||||
|
||||
const slices = getMilestoneSlices(m.id);
|
||||
for (const s of slices) {
|
||||
totalSlices++;
|
||||
if (isClosedStatus(s.status)) completedSlices++;
|
||||
|
||||
const tasks = getSliceTasks(m.id, s.id);
|
||||
for (const t of tasks) {
|
||||
totalTasks++;
|
||||
if (isClosedStatus(t.status)) completedTasks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
milestones: completedMilestones,
|
||||
milestonesTotal: milestones.length,
|
||||
slices: completedSlices,
|
||||
slicesTotal: totalSlices,
|
||||
tasks: completedTasks,
|
||||
tasksTotal: totalTasks,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Anomaly Detectors ───────────────────────────────────────────────────────
|
||||
|
||||
function detectStuckLoops(units: UnitMetrics[], anomalies: ForensicAnomaly[]): void {
|
||||
|
|
@ -1072,8 +1124,16 @@ function formatReportForPrompt(report: ForensicReport): string {
|
|||
sections.push("");
|
||||
}
|
||||
|
||||
// Completed keys count
|
||||
sections.push(`### Completed Keys: ${report.completedKeys.length}`);
|
||||
// Completion status — prefer DB counts, fall back to legacy completed-units.json
|
||||
if (report.dbCompletionCounts) {
|
||||
const c = report.dbCompletionCounts;
|
||||
sections.push(`### Completion Status (from DB)`);
|
||||
sections.push(`- ${c.milestones}/${c.milestonesTotal} milestones complete`);
|
||||
sections.push(`- ${c.slices}/${c.slicesTotal} slices complete`);
|
||||
sections.push(`- ${c.tasks}/${c.tasksTotal} tasks complete`);
|
||||
} else {
|
||||
sections.push(`### Completed Keys: ${report.completedKeys.length}`);
|
||||
}
|
||||
sections.push(`### GSD Version: ${report.gsdVersion}`);
|
||||
sections.push(`### Active Milestone: ${report.activeMilestone ?? "none"}`);
|
||||
sections.push(`### Active Slice: ${report.activeSlice ?? "none"}`);
|
||||
|
|
|
|||
|
|
@ -557,7 +557,8 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
|
|||
? `All milestones complete. ${activeReqs} active requirement${activeReqs === 1 ? '' : 's'} in REQUIREMENTS.md ${activeReqs === 1 ? 'has' : 'have'} not been mapped to a milestone.`
|
||||
: 'All milestones complete.';
|
||||
return {
|
||||
activeMilestone: lastEntry ? { id: lastEntry.id, title: lastEntry.title } : null,
|
||||
activeMilestone: null,
|
||||
lastCompletedMilestone: lastEntry ? { id: lastEntry.id, title: lastEntry.title } : null,
|
||||
activeSlice: null, activeTask: null,
|
||||
phase: 'complete',
|
||||
recentDecisions: [], blockers: [],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const gsdDir = join(__dirname, "..");
|
||||
|
||||
/**
|
||||
* Tests for #3129: forensics reads DB for completion status instead of legacy file.
|
||||
*
|
||||
* The old loadCompletedKeys() reads completed-units.json which is never populated
|
||||
* during normal auto-mode completion. The DB (milestones/slices/tasks tables) is
|
||||
* the authoritative source for completion status.
|
||||
*/
|
||||
describe("forensics DB completion status (#3129)", () => {
|
||||
const forensicsSrc = readFileSync(join(gsdDir, "forensics.ts"), "utf-8");
|
||||
const stateSrc = readFileSync(join(gsdDir, "state.ts"), "utf-8");
|
||||
|
||||
// ── Primary fix: forensics queries DB for completion counts ──────────
|
||||
|
||||
it("ForensicReport has dbCompletionCounts field for DB-sourced completion data", () => {
|
||||
assert.ok(
|
||||
forensicsSrc.includes("dbCompletionCounts"),
|
||||
"ForensicReport must include dbCompletionCounts field for DB-sourced completion data",
|
||||
);
|
||||
});
|
||||
|
||||
it("buildForensicReport queries DB for completed milestones, slices, and tasks", () => {
|
||||
assert.ok(
|
||||
forensicsSrc.includes("getDbCompletionCounts"),
|
||||
"buildForensicReport must call getDbCompletionCounts to query DB completion status",
|
||||
);
|
||||
});
|
||||
|
||||
it("getDbCompletionCounts checks isDbAvailable before querying", () => {
|
||||
assert.ok(
|
||||
forensicsSrc.includes("isDbAvailable"),
|
||||
"getDbCompletionCounts must check isDbAvailable() before querying the DB",
|
||||
);
|
||||
});
|
||||
|
||||
it("getDbCompletionCounts queries getAllMilestones for milestone completion", () => {
|
||||
assert.ok(
|
||||
forensicsSrc.includes("getAllMilestones"),
|
||||
"getDbCompletionCounts must use getAllMilestones() to count completed milestones",
|
||||
);
|
||||
});
|
||||
|
||||
it("completion counting uses isClosedStatus for consistent status checks", () => {
|
||||
assert.ok(
|
||||
forensicsSrc.includes("isClosedStatus"),
|
||||
"forensics must use isClosedStatus() for consistent status checks",
|
||||
);
|
||||
});
|
||||
|
||||
it("report rendering shows DB completion counts instead of just legacy key count", () => {
|
||||
assert.ok(
|
||||
forensicsSrc.includes("milestones complete"),
|
||||
"report must show '__ milestones complete' from DB data",
|
||||
);
|
||||
assert.ok(
|
||||
forensicsSrc.includes("slices complete"),
|
||||
"report must show '__ slices complete' from DB data",
|
||||
);
|
||||
assert.ok(
|
||||
forensicsSrc.includes("tasks complete"),
|
||||
"report must show '__ tasks complete' from DB data",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to completed-units.json only when DB is unavailable", () => {
|
||||
// loadCompletedKeys should still exist as fallback
|
||||
assert.ok(
|
||||
forensicsSrc.includes("loadCompletedKeys"),
|
||||
"loadCompletedKeys must still exist as fallback for non-DB projects",
|
||||
);
|
||||
// But the report should prefer DB counts
|
||||
assert.ok(
|
||||
forensicsSrc.includes("dbCompletionCounts"),
|
||||
"report must prefer dbCompletionCounts over legacy completedKeys",
|
||||
);
|
||||
});
|
||||
|
||||
// ── Secondary fix: STATE.md label when all milestones complete ───────
|
||||
|
||||
it("state.ts returns null activeMilestone when all milestones are complete", () => {
|
||||
// When phase is "complete", activeMilestone should be null, not the last milestone
|
||||
// The last completed milestone should be in a separate field
|
||||
assert.ok(
|
||||
stateSrc.includes("lastCompletedMilestone"),
|
||||
"GSDState must have lastCompletedMilestone field for the final milestone when phase=complete",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -249,6 +249,8 @@ export interface GSDState {
|
|||
slices?: { done: number; total: number };
|
||||
tasks?: { done: number; total: number };
|
||||
};
|
||||
/** When phase=complete, holds the last completed milestone (instead of activeMilestone). */
|
||||
lastCompletedMilestone?: ActiveRef | null;
|
||||
}
|
||||
|
||||
// ─── Post-Unit Hook Types ─────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -235,14 +235,18 @@ export function renderStateContent(state: GSDState): string {
|
|||
const lines: string[] = [];
|
||||
lines.push("# GSD State", "");
|
||||
|
||||
const activeMilestone = state.activeMilestone
|
||||
? `${state.activeMilestone.id}: ${state.activeMilestone.title}`
|
||||
: "None";
|
||||
const activeSlice = state.activeSlice
|
||||
? `${state.activeSlice.id}: ${state.activeSlice.title}`
|
||||
: "None";
|
||||
|
||||
lines.push(`**Active Milestone:** ${activeMilestone}`);
|
||||
if (state.phase === 'complete' && state.lastCompletedMilestone) {
|
||||
lines.push(`**Last Completed Milestone:** ${state.lastCompletedMilestone.id}: ${state.lastCompletedMilestone.title}`);
|
||||
} else {
|
||||
const activeMilestone = state.activeMilestone
|
||||
? `${state.activeMilestone.id}: ${state.activeMilestone.title}`
|
||||
: "None";
|
||||
lines.push(`**Active Milestone:** ${activeMilestone}`);
|
||||
}
|
||||
lines.push(`**Active Slice:** ${activeSlice}`);
|
||||
lines.push(`**Phase:** ${state.phase}`);
|
||||
if (state.requirements) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue