@opengsd/gsd-pi 1.7.0-dev.30c4ec2f → 1.7.0-dev.e0039c3c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/doctor.js +653 -14
- package/dist/resources/extensions/gsd/md-importer.js +2 -7
- package/dist/resources/extensions/gsd/migrate/planning-writer.js +2 -8
- package/dist/resources/extensions/gsd/migration-auto-check.js +0 -4
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +14 -54
- package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +5 -48
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/package.json +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/doctor.ts +673 -14
- package/src/resources/extensions/gsd/md-importer.ts +2 -7
- package/src/resources/extensions/gsd/migrate/planning-writer.ts +3 -9
- package/src/resources/extensions/gsd/migration-auto-check.ts +0 -3
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +20 -67
- package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +5 -47
- package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +1 -76
- package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +0 -53
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +0 -53
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -4
- package/src/resources/extensions/gsd/tests/planning-writer.test.ts +1 -18
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +3 -18
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +25 -177
- package/dist/resources/extensions/gsd/doctor-history.js +0 -65
- package/dist/resources/extensions/gsd/doctor-state-checks.js +0 -509
- package/dist/resources/extensions/gsd/doctor-workspace-checks.js +0 -91
- package/src/resources/extensions/gsd/doctor-history.ts +0 -81
- package/src/resources/extensions/gsd/doctor-state-checks.ts +0 -527
- package/src/resources/extensions/gsd/doctor-workspace-checks.ts +0 -97
- package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +0 -25
- /package/dist/web/standalone/.next/static/{ue2JDvA1wboxPBPeblD5N → QoErQJVEEGAIpfz1TMIHj}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ue2JDvA1wboxPBPeblD5N → QoErQJVEEGAIpfz1TMIHj}/_ssgManifest.js +0 -0
|
@@ -810,13 +810,8 @@ export function migrateHierarchyToDb(basePath: string): {
|
|
|
810
810
|
}
|
|
811
811
|
counts.slices++;
|
|
812
812
|
|
|
813
|
-
// Insert tasks from parsed plan
|
|
814
|
-
|
|
815
|
-
// tasks from a placeholder <tasks> block (would falsely flip
|
|
816
|
-
// planning -> executing before the slice is decomposed). Once the
|
|
817
|
-
// `[sketch]` badge is removed by a real plan-slice, tasks import
|
|
818
|
-
// normally on the next pass. (#1286)
|
|
819
|
-
if (!plan || sliceEntry.isSketch) continue;
|
|
813
|
+
// Insert tasks from parsed plan
|
|
814
|
+
if (!plan) continue;
|
|
820
815
|
|
|
821
816
|
for (const taskEntry of plan.tasks) {
|
|
822
817
|
// Per K002: use 'complete' not 'done'
|
|
@@ -11,7 +11,6 @@ import { join, relative } from "node:path";
|
|
|
11
11
|
|
|
12
12
|
import { getAllMilestones, getMilestoneSlices, getSliceTasks } from "../gsd-db.js";
|
|
13
13
|
import { saveFile } from "../files.js";
|
|
14
|
-
import { isClosedStatus } from "../status-guards.js";
|
|
15
14
|
import type { PlanningLayout } from "../compat/compat-marker.js";
|
|
16
15
|
import {
|
|
17
16
|
applyPlanningProjectionWrites,
|
|
@@ -105,7 +104,7 @@ function formatPlanningPlan(
|
|
|
105
104
|
phaseNum: number,
|
|
106
105
|
planNum: number,
|
|
107
106
|
title: string,
|
|
108
|
-
tasks: Array<{ id: string; title: string; estimate?: string
|
|
107
|
+
tasks: Array<{ id: string; title: string; estimate?: string }>,
|
|
109
108
|
): string {
|
|
110
109
|
const lines: string[] = [];
|
|
111
110
|
lines.push(`# ${pad(phaseNum)}-${pad(planNum)}: ${title}`, "");
|
|
@@ -116,11 +115,7 @@ function formatPlanningPlan(
|
|
|
116
115
|
lines.push("<tasks>");
|
|
117
116
|
for (const t of tasks) {
|
|
118
117
|
const est = t.estimate ? ` _(${t.estimate})_` : "";
|
|
119
|
-
|
|
120
|
-
// projection preserves completion. Hardcoding `[ ]` here silently reset
|
|
121
|
-
// every completed historical task to unchecked on each reconcile (#1276).
|
|
122
|
-
const box = t.done ? "[x]" : "[ ]";
|
|
123
|
-
lines.push(`- ${box} **${t.id}**: ${t.title}${est}`);
|
|
118
|
+
lines.push(`- [ ] **${t.id}**: ${t.title}${est}`);
|
|
124
119
|
}
|
|
125
120
|
lines.push("</tasks>");
|
|
126
121
|
lines.push("");
|
|
@@ -178,7 +173,7 @@ export async function writePlanningDirectory(
|
|
|
178
173
|
|
|
179
174
|
const tasks = getSliceTasks(milestone.id, slice.id);
|
|
180
175
|
const isDone =
|
|
181
|
-
tasks.length > 0 && tasks.every((t) =>
|
|
176
|
+
tasks.length > 0 && tasks.every((t) => t.status === "complete");
|
|
182
177
|
roadmapEntries.push({
|
|
183
178
|
number: phaseNum,
|
|
184
179
|
title: slice.title || slice.id,
|
|
@@ -209,7 +204,6 @@ export async function writePlanningDirectory(
|
|
|
209
204
|
id: task.id,
|
|
210
205
|
title: task.title || task.id,
|
|
211
206
|
estimate: task.estimate || undefined,
|
|
212
|
-
done: isClosedStatus(task.status),
|
|
213
207
|
},
|
|
214
208
|
]),
|
|
215
209
|
);
|
|
@@ -169,9 +169,6 @@ export function scanMarkdownHierarchy(basePath: string): HierarchyScan {
|
|
|
169
169
|
|
|
170
170
|
for (const slice of roadmap.slices) {
|
|
171
171
|
scan.slices.add(`${milestoneId}/${slice.id}`);
|
|
172
|
-
// Sketch slices carry only a stub PLAN until refined; match migrateHierarchyToDb
|
|
173
|
-
// and do not count placeholder tasks from the <tasks> block (#1286).
|
|
174
|
-
if (slice.isSketch) continue;
|
|
175
172
|
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
|
|
176
173
|
if (!planPath || !existsSync(planPath)) continue;
|
|
177
174
|
const plan = parsePlan(readFileSync(planPath, "utf-8"));
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { findMilestoneIds
|
|
10
|
+
import { getMilestone, isDbAvailable } from "../../gsd-db.js";
|
|
11
|
+
import { findMilestoneIds } from "../../milestone-ids.js";
|
|
12
12
|
import { resolveMilestoneFile, resolveMilestonePath } from "../../paths.js";
|
|
13
13
|
import type { GSDState } from "../../types.js";
|
|
14
14
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
@@ -29,26 +29,6 @@ function milestoneHasContent(basePath: string, milestoneId: string): boolean {
|
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
* A directory extracted to a bare `M{NNN}` id is not unregistered when the
|
|
34
|
-
* milestone is actually registered under a `unique_milestone_ids` suffixed id
|
|
35
|
-
* (`M{NNN}-<suffix>`) for the same sequence number. The flat-phase extractor in
|
|
36
|
-
* `milestone-ids` cannot recover the suffix from a descriptive slug (e.g.
|
|
37
|
-
* `07-v40fmq-m007-...-footer-system`), so it falls through to the bare `M007`.
|
|
38
|
-
* Probing the DB for a suffixed row on that sequence resolves the directory to
|
|
39
|
-
* its registered identity instead of firing a false-positive drift (issue
|
|
40
|
-
* #1281).
|
|
41
|
-
*/
|
|
42
|
-
function hasRegisteredSuffixedVariant(milestoneId: string): boolean {
|
|
43
|
-
const { suffix, num } = parseMilestoneId(milestoneId);
|
|
44
|
-
// Only bare, well-formed `M{NNN}` ids can be ambiguous with a suffixed row.
|
|
45
|
-
if (suffix || num === 0) return false;
|
|
46
|
-
return getAllMilestones().some((m) => {
|
|
47
|
-
const parsed = parseMilestoneId(m.id);
|
|
48
|
-
return parsed.num === num && parsed.suffix !== undefined;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
32
|
export function detectUnregisteredMilestoneDrift(
|
|
53
33
|
_state: GSDState,
|
|
54
34
|
ctx: DriftContext,
|
|
@@ -58,7 +38,6 @@ export function detectUnregisteredMilestoneDrift(
|
|
|
58
38
|
const drifts: UnregisteredMilestoneDrift[] = [];
|
|
59
39
|
for (const milestoneId of findMilestoneIds(ctx.basePath)) {
|
|
60
40
|
if (getMilestone(milestoneId)) continue;
|
|
61
|
-
if (hasRegisteredSuffixedVariant(milestoneId)) continue;
|
|
62
41
|
if (!milestoneHasContent(ctx.basePath, milestoneId)) continue;
|
|
63
42
|
drifts.push({ kind: "unregistered-milestone", milestoneId });
|
|
64
43
|
}
|
|
@@ -66,61 +45,35 @@ export function detectUnregisteredMilestoneDrift(
|
|
|
66
45
|
}
|
|
67
46
|
|
|
68
47
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
48
|
+
* Repair intentionally fails closed. The project-root DB is authoritative at
|
|
49
|
+
* runtime; markdown-only milestones must be reconciled through an explicit,
|
|
50
|
+
* operator-controlled action so operators opt into changing canonical state.
|
|
51
|
+
*
|
|
52
|
+
* The hint deliberately leads with the *targeted*, non-destructive options. The
|
|
53
|
+
* common cause of this drift is a directory left under an old ID after a
|
|
54
|
+
* `unique_milestone_ids` rename, where the right fix is to rename (move) the
|
|
55
|
+
* directory — not a full DB reimport. `/gsd recover --confirm` is a destructive
|
|
56
|
+
* clear-and-reimport of the entire DB and is offered only as a last resort, so
|
|
57
|
+
* users do not reach for it expecting a targeted repair (see issue #826).
|
|
76
58
|
*/
|
|
77
|
-
function
|
|
59
|
+
export function repairUnregisteredMilestone(
|
|
78
60
|
record: UnregisteredMilestoneDrift,
|
|
79
61
|
ctx: DriftContext,
|
|
80
|
-
):
|
|
62
|
+
): void {
|
|
81
63
|
const dir = resolveMilestonePath(ctx.basePath, record.milestoneId);
|
|
82
64
|
const dirHint = dir ?? `the .gsd directory for ${record.milestoneId}`;
|
|
83
|
-
|
|
65
|
+
throw new Error(
|
|
84
66
|
`Milestone ${record.milestoneId} exists only as markdown projection ` +
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
67
|
+
"(on-disk ROADMAP/CONTEXT/SUMMARY with no authoritative DB row). " +
|
|
68
|
+
"Runtime reconciliation will not import markdown into the DB. Choose one:\n" +
|
|
69
|
+
` • Rename: if this directory is the same milestone under an old ID (e.g. a unique_milestone_ids rename), move \`${dirHint}\` to the current ID's directory and re-run.\n` +
|
|
70
|
+
` • Discard: if this milestone is no longer relevant, delete \`${dirHint}\` and re-run.\n` +
|
|
71
|
+
" • Last resort: `/gsd recover --confirm` performs a destructive full DB clear-and-reimport — it does NOT do a targeted import and can replace or duplicate existing DB milestones.",
|
|
90
72
|
);
|
|
91
73
|
}
|
|
92
74
|
|
|
93
|
-
/**
|
|
94
|
-
* Terminal blocker for an unregistered milestone. The project-root DB is
|
|
95
|
-
* authoritative at runtime; markdown-only milestones must be reconciled through
|
|
96
|
-
* an explicit, operator-controlled action so operators opt into changing
|
|
97
|
-
* canonical state. Exposing this as a `blocker` (matching the `artifact-db`
|
|
98
|
-
* convention) surfaces the drift as a pause-with-hint that gates dispatch,
|
|
99
|
-
* instead of a guaranteed repair throw that escalates auto-mode health to red
|
|
100
|
-
* (see issue #1281).
|
|
101
|
-
*/
|
|
102
|
-
export function describeUnregisteredMilestoneBlocker(
|
|
103
|
-
record: UnregisteredMilestoneDrift,
|
|
104
|
-
ctx: DriftContext,
|
|
105
|
-
): string {
|
|
106
|
-
return unregisteredMilestoneGuidance(record, ctx);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Repair intentionally fails closed. Retained as a defensive fallback for the
|
|
111
|
-
* reconciliation engine; in practice `blocker` short-circuits this drift into a
|
|
112
|
-
* non-fatal pause before repair is ever attempted.
|
|
113
|
-
*/
|
|
114
|
-
export function repairUnregisteredMilestone(
|
|
115
|
-
record: UnregisteredMilestoneDrift,
|
|
116
|
-
ctx: DriftContext,
|
|
117
|
-
): void {
|
|
118
|
-
throw new Error(unregisteredMilestoneGuidance(record, ctx));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
75
|
export const unregisteredMilestoneHandler: DriftHandler<UnregisteredMilestoneDrift> = {
|
|
122
76
|
kind: "unregistered-milestone",
|
|
123
77
|
detect: detectUnregisteredMilestoneDrift,
|
|
124
|
-
blocker: describeUnregisteredMilestoneBlocker,
|
|
125
78
|
repair: repairUnregisteredMilestone,
|
|
126
79
|
};
|
|
@@ -8,14 +8,13 @@
|
|
|
8
8
|
// sketch slices to plan-slice, which writes PLAN.md but leaves
|
|
9
9
|
// is_sketch=1 — the next reconciliation pass clears it.
|
|
10
10
|
|
|
11
|
-
import { existsSync
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
getSketchedSliceIds,
|
|
15
15
|
isDbAvailable,
|
|
16
16
|
setSliceSketchFlag,
|
|
17
17
|
} from "../../gsd-db.js";
|
|
18
|
-
import { parsePlan } from "../../parsers-legacy.js";
|
|
19
18
|
import { resolveSliceFile } from "../../paths.js";
|
|
20
19
|
import type { GSDState } from "../../types.js";
|
|
21
20
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
@@ -32,54 +31,13 @@ export function detectStaleSketchFlags(
|
|
|
32
31
|
|
|
33
32
|
const sliceIds = getSketchedSliceIds(mid);
|
|
34
33
|
return sliceIds
|
|
35
|
-
.filter((sid) =>
|
|
34
|
+
.filter((sid) => {
|
|
35
|
+
const planPath = resolveSliceFile(ctx.basePath, mid, sid, "PLAN");
|
|
36
|
+
return planPath !== null && existsSync(planPath);
|
|
37
|
+
})
|
|
36
38
|
.map((sid) => ({ kind: "stale-sketch-flag" as const, mid, sid }));
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
/**
|
|
40
|
-
* True when a task title is a synthetic placeholder emitted by a projection
|
|
41
|
-
* round-trip rather than a real, refined task. `migrate/transformer.buildTaskTitle`
|
|
42
|
-
* produces two shapes when a plan was never decomposed:
|
|
43
|
-
* - `Plan NN` (fallback when phase/plan frontmatter is absent)
|
|
44
|
-
* - `${phase} ${plan}` (e.g. `00 01`, `00 03b`, `29-auth-system 01`)
|
|
45
|
-
* Neither carries genuine planning intent, so both must be treated as stubs.
|
|
46
|
-
*/
|
|
47
|
-
function isPlaceholderTaskTitle(title: string): boolean {
|
|
48
|
-
const t = title.trim();
|
|
49
|
-
if (/^Plan\s+\d+[a-z]*$/i.test(t)) return true;
|
|
50
|
-
// buildTaskTitle returns `${phase} ${plan}` when frontmatter has both fields.
|
|
51
|
-
// Phase slugs are digit-led (e.g. `00`, `29-auth-system`); require that so
|
|
52
|
-
// legitimate titles like `Step 1` are not misclassified as stubs.
|
|
53
|
-
return /^\d[\w-]*\s+\d+[a-z]*$/i.test(t);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* A sketch slice counts as "planned" (so its is_sketch flag may be cleared)
|
|
58
|
-
* only when it has a *real* plan on disk — not merely any PLAN file.
|
|
59
|
-
*
|
|
60
|
-
* File existence alone (#1287) is too weak: a stub/placeholder PLAN, a
|
|
61
|
-
* crash-leftover, or a projection round-trip that emits synthetic placeholder
|
|
62
|
-
* task titles (migrate/transformer.buildTaskTitle) all satisfy existsSync yet
|
|
63
|
-
* were never actually refined. Clearing the flag for one strips the `refining`
|
|
64
|
-
* guard in phase derivation and lets a phantom task drive dispatch.
|
|
65
|
-
*
|
|
66
|
-
* A legitimate plan-slice always decomposes into >= 1 genuine task, so require
|
|
67
|
-
* at least one non-placeholder task. This is strictly stricter than the old
|
|
68
|
-
* existence check — the documented crash-recovery scenarios (a real plan-slice
|
|
69
|
-
* that wrote its tasks) still clear, a bare stub no longer does.
|
|
70
|
-
*/
|
|
71
|
-
function sketchIsPlanned(basePath: string, mid: string, sid: string): boolean {
|
|
72
|
-
const planPath = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
73
|
-
if (planPath === null || !existsSync(planPath)) return false;
|
|
74
|
-
try {
|
|
75
|
-
const plan = parsePlan(readFileSync(planPath, "utf-8"));
|
|
76
|
-
return plan.tasks.some((t) => !isPlaceholderTaskTitle(t.title));
|
|
77
|
-
} catch {
|
|
78
|
-
// A PLAN we cannot parse is not a trustworthy "planning done" signal.
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
41
|
export function repairStaleSketchFlag(record: SketchFlagDrift): void {
|
|
84
42
|
setSliceSketchFlag(record.mid, record.sid, false);
|
|
85
43
|
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
openDatabase,
|
|
18
18
|
} from "../gsd-db.ts";
|
|
19
19
|
import { migrateHierarchyToDb } from "../md-importer.ts";
|
|
20
|
-
import { checkMarkdownHierarchyAgainstDb
|
|
20
|
+
import { checkMarkdownHierarchyAgainstDb } from "../migration-auto-check.ts";
|
|
21
21
|
import { queryDecisions } from "../context-store.ts";
|
|
22
22
|
import { deriveStateFromDb, invalidateStateCache } from "../state.ts";
|
|
23
23
|
import type { Requirement } from "../types.ts";
|
|
@@ -206,78 +206,3 @@ test("explicit markdown import remains opt-in and is not run by startup mismatch
|
|
|
206
206
|
assert.equal(getAllMilestones().length, 1);
|
|
207
207
|
assert.equal(getSliceTasks("M001", "S01").length, 1);
|
|
208
208
|
});
|
|
209
|
-
|
|
210
|
-
// #1286: migrateHierarchyToDb skips a `[sketch]` slice's stub PLAN tasks, so
|
|
211
|
-
// scanMarkdownHierarchy must skip counting them too. Otherwise the markdown
|
|
212
|
-
// scan claims task rows the DB authoritatively omits, and the startup drift
|
|
213
|
-
// check perpetually reports recovery-required / recommends
|
|
214
|
-
// `/gsd recover --confirm` — which re-skips the stub tasks and never converges.
|
|
215
|
-
test("sketch slice stub tasks are not treated as markdown/DB drift", async (t) => {
|
|
216
|
-
const base = makeBase();
|
|
217
|
-
t.after(() => cleanup(base));
|
|
218
|
-
|
|
219
|
-
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
220
|
-
const sketchSliceDir = join(milestoneDir, "slices", "S01");
|
|
221
|
-
const realSliceDir = join(milestoneDir, "slices", "S02");
|
|
222
|
-
mkdirSync(sketchSliceDir, { recursive: true });
|
|
223
|
-
mkdirSync(realSliceDir, { recursive: true });
|
|
224
|
-
|
|
225
|
-
// S01 is a sketch (badge before risk, per ADR-011 render); S02 is refined.
|
|
226
|
-
writeFileSync(
|
|
227
|
-
join(milestoneDir, "M001-ROADMAP.md"),
|
|
228
|
-
[
|
|
229
|
-
"# M001: Sketch Drift",
|
|
230
|
-
"",
|
|
231
|
-
"**Vision:** Sketch stub tasks must not count as drift",
|
|
232
|
-
"",
|
|
233
|
-
"## Slices",
|
|
234
|
-
"- [ ] **S01: Sketch Slice** `[sketch]` `risk:low` `depends:[]`",
|
|
235
|
-
" > After this: Sketch decomposed.",
|
|
236
|
-
"",
|
|
237
|
-
"- [ ] **S02: Real Slice** `risk:low` `depends:[]`",
|
|
238
|
-
" > After this: Real work done.",
|
|
239
|
-
"",
|
|
240
|
-
].join("\n"),
|
|
241
|
-
);
|
|
242
|
-
// The sketch slice still carries a stub PLAN with a placeholder <tasks> block.
|
|
243
|
-
writeFileSync(
|
|
244
|
-
join(sketchSliceDir, "S01-PLAN.md"),
|
|
245
|
-
[
|
|
246
|
-
"# S01: Sketch Slice",
|
|
247
|
-
"",
|
|
248
|
-
"**Goal:** Placeholder.",
|
|
249
|
-
"",
|
|
250
|
-
"## Tasks",
|
|
251
|
-
"- [ ] **T01: Plan 01**",
|
|
252
|
-
" Stub placeholder task.",
|
|
253
|
-
"",
|
|
254
|
-
].join("\n"),
|
|
255
|
-
);
|
|
256
|
-
writeFileSync(
|
|
257
|
-
join(realSliceDir, "S02-PLAN.md"),
|
|
258
|
-
[
|
|
259
|
-
"# S02: Real Slice",
|
|
260
|
-
"",
|
|
261
|
-
"**Goal:** Real work.",
|
|
262
|
-
"",
|
|
263
|
-
"## Tasks",
|
|
264
|
-
"- [ ] **T01: Task** `est:5m`",
|
|
265
|
-
"",
|
|
266
|
-
].join("\n"),
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
openProjectDb(base);
|
|
270
|
-
// The importer seeds S02's task but skips S01's stub (1 task total).
|
|
271
|
-
const imported = migrateHierarchyToDb(base);
|
|
272
|
-
assert.deepEqual(imported, { milestones: 1, slices: 2, tasks: 1 });
|
|
273
|
-
assert.equal(getSliceTasks("M001", "S01").length, 0, "sketch slice seeds no tasks");
|
|
274
|
-
assert.equal(getSliceTasks("M001", "S02").length, 1, "real slice seeds its task");
|
|
275
|
-
|
|
276
|
-
// The markdown scan must mirror the importer: the sketch stub task is not
|
|
277
|
-
// counted, so markdown (1 task) matches the DB (1 task) and the check is
|
|
278
|
-
// in-sync rather than recommending a recover that would never converge.
|
|
279
|
-
assert.deepEqual(countMarkdownHierarchy(base), { milestones: 1, slices: 2, tasks: 1 });
|
|
280
|
-
const check = await checkMarkdownHierarchyAgainstDb(base);
|
|
281
|
-
assert.equal(check.action, "none", "sketch stub tasks must not be flagged as drift");
|
|
282
|
-
assert.equal(check.reason, "in-sync");
|
|
283
|
-
});
|
|
@@ -427,56 +427,3 @@ test('migrate-hier: demo text extracted', () => {
|
|
|
427
427
|
}
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
-
// ─── Test (i): Sketch slice stub tasks are not seeded (#1286) ─────────
|
|
431
|
-
|
|
432
|
-
test('migrate-hier: sketch slice stub PLAN tasks are not imported', () => {
|
|
433
|
-
const base = createFixtureBase();
|
|
434
|
-
try {
|
|
435
|
-
// S01 is a sketch (badge before risk, per ADR-011 render); its PLAN is a
|
|
436
|
-
// stub with a placeholder <tasks> block. S02 is a real, refined slice.
|
|
437
|
-
const roadmap = `# M001: Test Milestone
|
|
438
|
-
|
|
439
|
-
**Vision:** Sketch slices must not seed DB tasks.
|
|
440
|
-
|
|
441
|
-
## Slices
|
|
442
|
-
|
|
443
|
-
- [ ] **S01: Sketch Slice** \`[sketch]\` \`risk:low\` \`depends:[]\`
|
|
444
|
-
> After this: Sketch decomposed.
|
|
445
|
-
|
|
446
|
-
- [ ] **S02: Real Slice** \`risk:low\` \`depends:[]\`
|
|
447
|
-
> After this: Real work done.
|
|
448
|
-
`;
|
|
449
|
-
const sketchPlan = `# S01: Sketch Slice
|
|
450
|
-
|
|
451
|
-
**Goal:** Placeholder.
|
|
452
|
-
|
|
453
|
-
## Tasks
|
|
454
|
-
|
|
455
|
-
- [ ] **T01: Plan 01**
|
|
456
|
-
Stub placeholder task.
|
|
457
|
-
`;
|
|
458
|
-
writeFile(base, 'milestones/M001/M001-ROADMAP.md', roadmap);
|
|
459
|
-
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', sketchPlan);
|
|
460
|
-
writeFile(base, 'milestones/M001/slices/S02/S02-PLAN.md', PLAN_S02_1_TASK);
|
|
461
|
-
|
|
462
|
-
openDatabase(':memory:');
|
|
463
|
-
const counts = migrateHierarchyToDb(base);
|
|
464
|
-
|
|
465
|
-
const slices = getMilestoneSlices('M001');
|
|
466
|
-
assert.deepStrictEqual(slices.length, 2, 'sketch: 2 slices inserted');
|
|
467
|
-
assert.deepStrictEqual(slices[0]!.is_sketch, 1, 'sketch: S01 preserves is_sketch flag');
|
|
468
|
-
|
|
469
|
-
const s01Tasks = getSliceTasks('M001', 'S01');
|
|
470
|
-
assert.deepStrictEqual(s01Tasks.length, 0, 'sketch: no stub tasks seeded for sketch slice S01');
|
|
471
|
-
|
|
472
|
-
const s02Tasks = getSliceTasks('M001', 'S02');
|
|
473
|
-
assert.deepStrictEqual(s02Tasks.length, 1, 'sketch: real slice S02 still imports its task');
|
|
474
|
-
assert.deepStrictEqual(counts.tasks, 1, 'sketch: only the non-sketch slice contributes tasks');
|
|
475
|
-
|
|
476
|
-
closeDatabase();
|
|
477
|
-
} finally {
|
|
478
|
-
closeDatabase();
|
|
479
|
-
cleanup(base);
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
|
|
@@ -420,59 +420,6 @@ test("migration auto-check still reports real drift with scratch dirs excluded f
|
|
|
420
420
|
}
|
|
421
421
|
});
|
|
422
422
|
|
|
423
|
-
test("migration auto-check excludes sketch slice stub tasks from the markdown scan (#1286)", () => {
|
|
424
|
-
const base = makeBase();
|
|
425
|
-
try {
|
|
426
|
-
// S01 is a sketch (`[sketch]` badge): its PLAN is a stub carrying a
|
|
427
|
-
// placeholder <tasks> block until refinement. migrateHierarchyToDb skips
|
|
428
|
-
// seeding those stub tasks, so the markdown scan must skip them too — else
|
|
429
|
-
// the stub inflates the markdown count and reports false drift against a DB
|
|
430
|
-
// that (correctly) holds zero tasks for the sketch slice.
|
|
431
|
-
const roadmap = `# M001: Test Milestone
|
|
432
|
-
|
|
433
|
-
**Vision:** Sketch slices must not inflate markdown task counts.
|
|
434
|
-
|
|
435
|
-
## Slices
|
|
436
|
-
|
|
437
|
-
- [ ] **S01: Sketch Slice** \`[sketch]\` \`risk:low\` \`depends:[]\`
|
|
438
|
-
> After this: Sketch decomposed.
|
|
439
|
-
|
|
440
|
-
- [ ] **S02: Real Slice** \`risk:low\` \`depends:[]\`
|
|
441
|
-
> After this: Real work done.
|
|
442
|
-
`;
|
|
443
|
-
const sketchPlan = `# S01: Sketch Slice
|
|
444
|
-
|
|
445
|
-
**Goal:** Placeholder.
|
|
446
|
-
|
|
447
|
-
## Tasks
|
|
448
|
-
|
|
449
|
-
- [ ] **T01: Plan 01**
|
|
450
|
-
Stub placeholder task.
|
|
451
|
-
`;
|
|
452
|
-
const realPlan = `# S02: Real Slice
|
|
453
|
-
|
|
454
|
-
**Goal:** Real work.
|
|
455
|
-
|
|
456
|
-
## Tasks
|
|
457
|
-
|
|
458
|
-
- [ ] **T01: Real Task**
|
|
459
|
-
Real work.
|
|
460
|
-
`;
|
|
461
|
-
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
462
|
-
mkdirSync(join(milestoneDir, "slices", "S01"), { recursive: true });
|
|
463
|
-
mkdirSync(join(milestoneDir, "slices", "S02"), { recursive: true });
|
|
464
|
-
writeFileSync(join(milestoneDir, "M001-ROADMAP.md"), roadmap);
|
|
465
|
-
writeFileSync(join(milestoneDir, "slices", "S01", "S01-PLAN.md"), sketchPlan);
|
|
466
|
-
writeFileSync(join(milestoneDir, "slices", "S02", "S02-PLAN.md"), realPlan);
|
|
467
|
-
|
|
468
|
-
// Both slices count, but only the refined slice contributes a task; the
|
|
469
|
-
// sketch stub task is excluded, matching migrateHierarchyToDb.
|
|
470
|
-
assert.deepEqual(countMarkdownHierarchy(base), { milestones: 1, slices: 2, tasks: 1 });
|
|
471
|
-
} finally {
|
|
472
|
-
cleanup(base);
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
423
|
test("migration auto-check still compares a roadmapless milestone that HAS a DB row", async () => {
|
|
477
424
|
const base = makeBase();
|
|
478
425
|
try {
|
|
@@ -47,9 +47,6 @@ const ALLOWED_IMPORTERS = new Set([
|
|
|
47
47
|
"gsd/migration-auto-check.ts",
|
|
48
48
|
// drift detection: compares markdown projection against DB by design
|
|
49
49
|
"gsd/state-reconciliation/drift/roadmap.ts",
|
|
50
|
-
// drift detection: reads PLAN.md tasks to distinguish a real plan from a
|
|
51
|
-
// stub before clearing a stale is_sketch flag (#1287)
|
|
52
|
-
"gsd/state-reconciliation/drift/sketch-flag.ts",
|
|
53
50
|
// stale-render detection + render verification helpers over rendered output
|
|
54
51
|
"gsd/markdown-renderer.ts",
|
|
55
52
|
// pre-migration fallback: deriveState must work before the DB exists
|
|
@@ -61,7 +58,6 @@ const ALLOWED_IMPORTERS = new Set([
|
|
|
61
58
|
"gsd/artifact-verification.ts",
|
|
62
59
|
// diagnostics-only surfaces: report on projections, make no dispatch decisions
|
|
63
60
|
"gsd/doctor.ts",
|
|
64
|
-
"gsd/doctor-state-checks.ts",
|
|
65
61
|
// diagnostics-only: compares PLAN.md task checkboxes against DB status
|
|
66
62
|
"gsd/doctor-engine-checks.ts",
|
|
67
63
|
// display/telemetry-only surfaces
|
|
@@ -8,7 +8,7 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
import { writePlanningDirectory } from "../migrate/planning-writer.ts";
|
|
11
|
-
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask
|
|
11
|
+
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask } from "../gsd-db.ts";
|
|
12
12
|
|
|
13
13
|
const tmpDirs: string[] = [];
|
|
14
14
|
function makeTmp(): string {
|
|
@@ -65,23 +65,6 @@ test("writePlanningDirectory emits phase plan file with XML structure", async ()
|
|
|
65
65
|
assert.match(plan, /<tasks>/);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
test("writePlanningDirectory renders completed task checkboxes as [x] (#1276)", async () => {
|
|
69
|
-
const base = makeTmp();
|
|
70
|
-
updateTaskStatus("M001", "S01", "T01", "complete");
|
|
71
|
-
await writePlanningDirectory(base, "flat-phases");
|
|
72
|
-
|
|
73
|
-
const phaseDir = readdirSync(join(base, ".planning", "phases"), { withFileTypes: true })
|
|
74
|
-
.filter((d) => d.isDirectory())
|
|
75
|
-
.map((d) => d.name)[0]!;
|
|
76
|
-
const planFile = readdirSync(join(base, ".planning", "phases", phaseDir))
|
|
77
|
-
.filter((f) => f.endsWith("PLAN.md"))[0]!;
|
|
78
|
-
const plan = readFileSync(join(base, ".planning", "phases", phaseDir, planFile), "utf-8");
|
|
79
|
-
assert.match(plan, /- \[x\] \*\*T01\*\*/, "completed task should project as [x], not [ ]");
|
|
80
|
-
|
|
81
|
-
const roadmap = readFileSync(join(base, ".planning", "ROADMAP.md"), "utf-8");
|
|
82
|
-
assert.match(roadmap, /- \[x\] 01 —/, "phase with all tasks complete should be [x] in ROADMAP");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
68
|
test("writePlanningDirectory is idempotent: writing twice produces stable content", async () => {
|
|
86
69
|
const base = makeTmp();
|
|
87
70
|
await writePlanningDirectory(base, "flat-phases");
|
|
@@ -76,21 +76,6 @@ function writeS01Artifacts(base: string): void {
|
|
|
76
76
|
writeFileSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"), "# S01 Summary\n");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// A *real* decomposed PLAN (>= 1 genuine task). #1287: the stale-sketch-flag
|
|
80
|
-
// detector requires a real plan — not a bare stub — before clearing is_sketch.
|
|
81
|
-
function realPlanContent(sliceId: string): string {
|
|
82
|
-
return [
|
|
83
|
-
`# ${sliceId}: Feature`,
|
|
84
|
-
"",
|
|
85
|
-
"**Goal:** Ship the feature",
|
|
86
|
-
"",
|
|
87
|
-
"## Tasks",
|
|
88
|
-
"",
|
|
89
|
-
"- [ ] **T01: Build the feature** `est:1h`",
|
|
90
|
-
"",
|
|
91
|
-
].join("\n");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
79
|
function cleanup(base: string, originalCwd: string): void {
|
|
95
80
|
try { closeDatabase(); } catch { /* noop */ }
|
|
96
81
|
process.chdir(originalCwd);
|
|
@@ -200,7 +185,7 @@ test("ADR-011: existing PLAN + stale sketch flag heals via reconcileBeforeDispat
|
|
|
200
185
|
writePreferences(base, "phases:\n progressive_planning: true");
|
|
201
186
|
writeFileSync(
|
|
202
187
|
join(base, ".gsd", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
|
|
203
|
-
|
|
188
|
+
"# S02 Plan\n",
|
|
204
189
|
);
|
|
205
190
|
process.chdir(base);
|
|
206
191
|
|
|
@@ -267,7 +252,7 @@ test("ADR-011: reconcileBeforeDispatch auto-heals stale sketch flag when PLAN ex
|
|
|
267
252
|
// Simulate plan-slice completion where PLAN exists but is_sketch was not flipped.
|
|
268
253
|
writeFileSync(
|
|
269
254
|
join(base, ".gsd", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
|
|
270
|
-
|
|
255
|
+
"# S02 Plan\n",
|
|
271
256
|
);
|
|
272
257
|
process.chdir(base);
|
|
273
258
|
|
|
@@ -297,7 +282,7 @@ test("ADR-011: deriveState stays pure across worktree/canonical-root; healing be
|
|
|
297
282
|
writePreferences(base, "phases:\n skip_research: false");
|
|
298
283
|
writeFileSync(
|
|
299
284
|
join(base, ".gsd", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
|
|
300
|
-
|
|
285
|
+
"# S02 Plan\n",
|
|
301
286
|
);
|
|
302
287
|
const worktreePath = join(base, "worker");
|
|
303
288
|
mkdirSync(worktreePath, { recursive: true });
|