gsd-pi 2.8.0 → 2.8.2
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/loader.js +5 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +32 -22
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +3 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +4 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +35 -22
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +4 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
- package/package.json +6 -1
- package/packages/pi-coding-agent/dist/config.d.ts +2 -0
- package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/config.js +4 -0
- package/packages/pi-coding-agent/dist/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +32 -22
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +3 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
- package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/packages/pi-coding-agent/src/config.ts +5 -0
- package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/packages/pi-coding-agent/src/core/tools/bash.ts +35 -22
- package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/packages/pi-coding-agent/src/index.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
- package/src/resources/extensions/bg-shell/index.ts +2 -1
- package/src/resources/extensions/browser-tools/lifecycle.ts +6 -1
- package/src/resources/extensions/gsd/auto.ts +92 -49
- package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
- package/src/resources/extensions/gsd/exit-command.ts +18 -0
- package/src/resources/extensions/gsd/files.ts +9 -40
- package/src/resources/extensions/gsd/git-service.ts +62 -17
- package/src/resources/extensions/gsd/gitignore.ts +28 -0
- package/src/resources/extensions/gsd/guided-flow.ts +49 -11
- package/src/resources/extensions/gsd/index.ts +111 -16
- package/src/resources/extensions/gsd/preferences.ts +8 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +27 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
- package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +116 -39
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
- package/src/resources/extensions/ttsr/index.ts +163 -0
- package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
- package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
- package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
|
@@ -76,6 +76,6 @@ If this milestone requires any external API keys or secrets:
|
|
|
76
76
|
|
|
77
77
|
If this milestone does not require any external API keys or secrets, skip this step entirely — do not create an empty manifest.
|
|
78
78
|
|
|
79
|
-
**You MUST write the file `{{
|
|
79
|
+
**You MUST write the file `{{outputPath}}` before finishing.**
|
|
80
80
|
|
|
81
81
|
When done, say: "Milestone {{milestoneId}} planned."
|
|
@@ -37,7 +37,7 @@ Then:
|
|
|
37
37
|
- a matching task plan file with description, steps, must-haves, verification, inputs, and expected output
|
|
38
38
|
- Observability Impact section **only if the task touches runtime boundaries, async flows, or error paths** — omit it otherwise
|
|
39
39
|
6. Write `{{outputPath}}`
|
|
40
|
-
7. Write individual task plans in `{{
|
|
40
|
+
7. Write individual task plans in `{{slicePath}}/tasks/`: `T01-PLAN.md`, `T02-PLAN.md`, etc.
|
|
41
41
|
8. **Self-audit the plan.** Walk through each check — if any fail, fix the plan files before moving on:
|
|
42
42
|
- **Completion semantics:** If every task were completed exactly as written, the slice goal/demo should actually be true.
|
|
43
43
|
- **Requirement coverage:** Every must-have in the slice maps to at least one task. No must-have is orphaned. If `REQUIREMENTS.md` exists, every Active requirement this slice owns maps to at least one task.
|
|
@@ -52,6 +52,6 @@ Then:
|
|
|
52
52
|
|
|
53
53
|
The slice directory and tasks/ subdirectory already exist. Do NOT mkdir. You are on the slice branch; all work stays here.
|
|
54
54
|
|
|
55
|
-
**You MUST write the file `{{
|
|
55
|
+
**You MUST write the file `{{outputPath}}` before finishing.**
|
|
56
56
|
|
|
57
57
|
When done, say: "Slice {{sliceId}} planned."
|
|
@@ -34,15 +34,15 @@ If all criteria have at least one remaining owning slice, the coverage check pas
|
|
|
34
34
|
|
|
35
35
|
**If the roadmap is still good:**
|
|
36
36
|
|
|
37
|
-
Write `{{
|
|
37
|
+
Write `{{assessmentPath}}` with a brief confirmation that roadmap coverage still holds after {{completedSliceId}}. If requirements exist, explicitly note whether requirement coverage remains sound.
|
|
38
38
|
|
|
39
39
|
**If changes are needed:**
|
|
40
40
|
|
|
41
41
|
1. Rewrite the remaining (unchecked) slices in `{{roadmapPath}}`. Keep completed slices exactly as they are (`[x]`). Update the boundary map for changed slices. Update the proof strategy if risks changed. Update requirement coverage if ownership or scope changed.
|
|
42
|
-
2. Write `{{
|
|
42
|
+
2. Write `{{assessmentPath}}` explaining what changed and why — keep it brief and concrete.
|
|
43
43
|
3. If `.gsd/REQUIREMENTS.md` exists and requirement ownership or status changed, update it.
|
|
44
44
|
4. Commit: `docs({{milestoneId}}): reassess roadmap after {{completedSliceId}}`
|
|
45
45
|
|
|
46
|
-
**You MUST write the file `{{
|
|
46
|
+
**You MUST write the file `{{assessmentPath}}` before finishing.**
|
|
47
47
|
|
|
48
48
|
When done, say: "Roadmap reassessed."
|
|
@@ -20,7 +20,7 @@ All relevant context has been preloaded below — the roadmap, current slice pla
|
|
|
20
20
|
|
|
21
21
|
1. Read the blocker task summary carefully. Understand exactly what was discovered and why it blocks the current plan.
|
|
22
22
|
2. Analyze the remaining `[ ]` tasks in the slice plan. Determine which are still valid, which need modification, and which should be replaced.
|
|
23
|
-
3. Write `{{
|
|
23
|
+
3. Write `{{replanPath}}` documenting:
|
|
24
24
|
- What blocker was discovered and in which task
|
|
25
25
|
- What changed in the plan and why
|
|
26
26
|
- Which incomplete tasks were modified, added, or removed
|
|
@@ -34,6 +34,6 @@ All relevant context has been preloaded below — the roadmap, current slice pla
|
|
|
34
34
|
6. Do not commit manually — the system auto-commits your changes after this unit completes.
|
|
35
35
|
7. Update `.gsd/STATE.md`
|
|
36
36
|
|
|
37
|
-
**You MUST write `{{
|
|
37
|
+
**You MUST write `{{replanPath}}` and the updated slice plan before finishing.**
|
|
38
38
|
|
|
39
39
|
When done, say: "Slice {{sliceId}} replanned."
|
|
@@ -32,6 +32,6 @@ Then research the codebase and relevant technologies. Narrate key findings and s
|
|
|
32
32
|
|
|
33
33
|
**Research is advisory, not auto-binding.** Surface candidate requirements clearly instead of silently expanding scope.
|
|
34
34
|
|
|
35
|
-
**You MUST write the file `{{
|
|
35
|
+
**You MUST write the file `{{outputPath}}` before finishing.**
|
|
36
36
|
|
|
37
37
|
When done, say: "Milestone {{milestoneId}} researched."
|
|
@@ -23,6 +23,6 @@ Then research what this slice needs. Narrate key findings and surprises as you g
|
|
|
23
23
|
|
|
24
24
|
The slice directory already exists at `{{slicePath}}/`. Do NOT mkdir — just write the file.
|
|
25
25
|
|
|
26
|
-
**You MUST write the file `{{
|
|
26
|
+
**You MUST write the file `{{outputPath}}` before finishing.**
|
|
27
27
|
|
|
28
28
|
When done, say: "Slice {{sliceId}} researched."
|
|
@@ -14,7 +14,7 @@ If a `GSD Skill Preferences` block is present in system context, use it to decid
|
|
|
14
14
|
|
|
15
15
|
**UAT file:** `{{uatPath}}`
|
|
16
16
|
**UAT type:** `{{uatType}}`
|
|
17
|
-
**Result file to write:** `{{
|
|
17
|
+
**Result file to write:** `{{uatResultPath}}`
|
|
18
18
|
|
|
19
19
|
### If UAT type is `artifact-driven`
|
|
20
20
|
|
|
@@ -37,7 +37,7 @@ After running all checks, compute the **overall verdict**:
|
|
|
37
37
|
- `FAIL` — one or more checks failed
|
|
38
38
|
- `PARTIAL` — some checks passed, some failed or were skipped
|
|
39
39
|
|
|
40
|
-
Write `{{
|
|
40
|
+
Write `{{uatResultPath}}` with:
|
|
41
41
|
|
|
42
42
|
```markdown
|
|
43
43
|
---
|
|
@@ -68,7 +68,7 @@ date: <ISO 8601 timestamp>
|
|
|
68
68
|
|
|
69
69
|
This UAT type requires human execution or live-runtime observation that you cannot perform mechanically. Your role is to surface it clearly for review.
|
|
70
70
|
|
|
71
|
-
Write `{{
|
|
71
|
+
Write `{{uatResultPath}}` with:
|
|
72
72
|
|
|
73
73
|
```markdown
|
|
74
74
|
---
|
|
@@ -104,6 +104,6 @@ Once updated, run `/gsd auto` to resume auto-mode.
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
-
**You MUST write `{{
|
|
107
|
+
**You MUST write `{{uatResultPath}}` before finishing.**
|
|
108
108
|
|
|
109
109
|
When done, say: "UAT {{sliceId}} complete."
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { RoadmapSliceEntry, RiskLevel } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
function extractSlicesSection(content: string): string {
|
|
4
|
+
const headingMatch = /^## Slices\s*$/m.exec(content);
|
|
5
|
+
if (!headingMatch || headingMatch.index == null) return "";
|
|
6
|
+
|
|
7
|
+
const start = headingMatch.index + headingMatch[0].length;
|
|
8
|
+
const rest = content.slice(start).replace(/^\r?\n/, "");
|
|
9
|
+
const nextHeading = /^##\s+/m.exec(rest);
|
|
10
|
+
return (nextHeading ? rest.slice(0, nextHeading.index) : rest).trimEnd();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
|
|
14
|
+
const slicesSection = extractSlicesSection(content);
|
|
15
|
+
const slices: RoadmapSliceEntry[] = [];
|
|
16
|
+
if (!slicesSection) return slices;
|
|
17
|
+
|
|
18
|
+
const checkboxItems = slicesSection.split("\n");
|
|
19
|
+
let currentSlice: RoadmapSliceEntry | null = null;
|
|
20
|
+
|
|
21
|
+
for (const line of checkboxItems) {
|
|
22
|
+
const cbMatch = line.match(/^\s*-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
|
|
23
|
+
if (cbMatch) {
|
|
24
|
+
if (currentSlice) slices.push(currentSlice);
|
|
25
|
+
|
|
26
|
+
const done = cbMatch[1].toLowerCase() === "x";
|
|
27
|
+
const id = cbMatch[2]!;
|
|
28
|
+
const title = cbMatch[3]!;
|
|
29
|
+
const rest = cbMatch[4] ?? "";
|
|
30
|
+
|
|
31
|
+
const riskMatch = rest.match(/`risk:(\w+)`/);
|
|
32
|
+
const risk = (riskMatch ? riskMatch[1] : "low") as RiskLevel;
|
|
33
|
+
|
|
34
|
+
const depsMatch = rest.match(/`depends:\[([^\]]*)\]`/);
|
|
35
|
+
const depends = depsMatch && depsMatch[1]!.trim()
|
|
36
|
+
? depsMatch[1]!.split(",").map(s => s.trim())
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
currentSlice = { id, title, risk, depends, done, demo: "" };
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (currentSlice && line.trim().startsWith(">")) {
|
|
44
|
+
currentSlice.demo = line.trim().replace(/^>\s*/, "").replace(/^After this:\s*/i, "");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentSlice) slices.push(currentSlice);
|
|
49
|
+
return slices;
|
|
50
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { getPriorSliceCompletionBlocker } from "../dispatch-guard.ts";
|
|
6
|
+
|
|
7
|
+
let passed = 0;
|
|
8
|
+
let failed = 0;
|
|
9
|
+
|
|
10
|
+
function assertEq<T>(actual: T, expected: T, message: string): void {
|
|
11
|
+
if (JSON.stringify(actual) === JSON.stringify(expected)) {
|
|
12
|
+
passed += 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
failed += 1;
|
|
16
|
+
console.error(`FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function run(command: string, cwd: string): void {
|
|
20
|
+
execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
|
|
26
|
+
mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
|
|
27
|
+
|
|
28
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
|
|
29
|
+
"# M002: Previous",
|
|
30
|
+
"",
|
|
31
|
+
"## Slices",
|
|
32
|
+
"- [x] **S01: Done** `risk:low` `depends:[]`",
|
|
33
|
+
"- [ ] **S02: Pending** `risk:low` `depends:[S01]`",
|
|
34
|
+
"",
|
|
35
|
+
].join("\n"));
|
|
36
|
+
|
|
37
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
|
|
38
|
+
"# M003: Current",
|
|
39
|
+
"",
|
|
40
|
+
"## Slices",
|
|
41
|
+
"- [ ] **S01: First** `risk:low` `depends:[]`",
|
|
42
|
+
"- [ ] **S02: Second** `risk:low` `depends:[S01]`",
|
|
43
|
+
"",
|
|
44
|
+
].join("\n"));
|
|
45
|
+
|
|
46
|
+
run("git init -b main", repo);
|
|
47
|
+
run("git config user.email test@example.com", repo);
|
|
48
|
+
run("git config user.name Test", repo);
|
|
49
|
+
run("git add .", repo);
|
|
50
|
+
run("git commit -m init", repo);
|
|
51
|
+
|
|
52
|
+
assertEq(
|
|
53
|
+
getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M003/S01"),
|
|
54
|
+
"Cannot dispatch plan-slice M003/S01: earlier slice M002/S02 is not complete on main.",
|
|
55
|
+
"blocks first slice of next milestone when prior milestone is incomplete on main",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
|
|
59
|
+
"# M002: Previous",
|
|
60
|
+
"",
|
|
61
|
+
"## Slices",
|
|
62
|
+
"- [x] **S01: Done** `risk:low` `depends:[]`",
|
|
63
|
+
"- [x] **S02: Done** `risk:low` `depends:[S01]`",
|
|
64
|
+
"",
|
|
65
|
+
].join("\n"));
|
|
66
|
+
run("git add .", repo);
|
|
67
|
+
run("git commit -m complete-m002", repo);
|
|
68
|
+
|
|
69
|
+
assertEq(
|
|
70
|
+
getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
|
|
71
|
+
"Cannot dispatch execute-task M003/S02/T01: earlier slice M003/S01 is not complete on main.",
|
|
72
|
+
"blocks later slice in same milestone when an earlier slice is incomplete on main",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
|
|
76
|
+
"# M003: Current",
|
|
77
|
+
"",
|
|
78
|
+
"## Slices",
|
|
79
|
+
"- [x] **S01: First** `risk:low` `depends:[]`",
|
|
80
|
+
"- [ ] **S02: Second** `risk:low` `depends:[S01]`",
|
|
81
|
+
"",
|
|
82
|
+
].join("\n"));
|
|
83
|
+
run("git add .", repo);
|
|
84
|
+
run("git commit -m complete-m003-s01", repo);
|
|
85
|
+
|
|
86
|
+
assertEq(
|
|
87
|
+
getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
|
|
88
|
+
null,
|
|
89
|
+
"allows dispatch when all earlier slices are complete on main",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
assertEq(
|
|
93
|
+
getPriorSliceCompletionBlocker(repo, "main", "plan-milestone", "M003"),
|
|
94
|
+
null,
|
|
95
|
+
"does not affect non-slice dispatch types",
|
|
96
|
+
);
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync(repo, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`Passed: ${passed}, Failed: ${failed}`);
|
|
102
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { registerExitCommand } from "../exit-command.ts";
|
|
5
|
+
|
|
6
|
+
test("/exit requests graceful shutdown instead of process.exit", async () => {
|
|
7
|
+
const commands = new Map<
|
|
8
|
+
string,
|
|
9
|
+
{
|
|
10
|
+
description?: string;
|
|
11
|
+
handler: (args: string, ctx: { shutdown: () => Promise<void> }) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
>();
|
|
14
|
+
|
|
15
|
+
const pi = {
|
|
16
|
+
registerCommand(name: string, options: any) {
|
|
17
|
+
commands.set(name, options);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let stopAutoCalls = 0;
|
|
22
|
+
registerExitCommand(pi as any, {
|
|
23
|
+
async stopAuto() {
|
|
24
|
+
stopAutoCalls += 1;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const exit = commands.get("exit");
|
|
29
|
+
assert.ok(exit, "registerExitCommand should register /exit");
|
|
30
|
+
assert.equal(exit.description, "Exit GSD gracefully");
|
|
31
|
+
|
|
32
|
+
let shutdownCalls = 0;
|
|
33
|
+
const originalExit = process.exit;
|
|
34
|
+
process.exit = ((code?: number) => {
|
|
35
|
+
throw new Error(`process.exit should not be called: ${code ?? "undefined"}`);
|
|
36
|
+
}) as typeof process.exit;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await exit.handler("", {
|
|
40
|
+
async shutdown() {
|
|
41
|
+
shutdownCalls += 1;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
} finally {
|
|
45
|
+
process.exit = originalExit;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
assert.equal(stopAutoCalls, 1, "handler should stop auto-mode exactly once before shutdown");
|
|
49
|
+
assert.equal(shutdownCalls, 1, "handler should request graceful shutdown exactly once");
|
|
50
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
1
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
@@ -210,8 +210,8 @@ async function main(): Promise<void> {
|
|
|
210
210
|
|
|
211
211
|
assertEq(
|
|
212
212
|
RUNTIME_EXCLUSION_PATHS.length,
|
|
213
|
-
|
|
214
|
-
"exactly
|
|
213
|
+
7,
|
|
214
|
+
"exactly 7 runtime exclusion paths"
|
|
215
215
|
);
|
|
216
216
|
|
|
217
217
|
const expectedPaths = [
|
|
@@ -220,6 +220,7 @@ async function main(): Promise<void> {
|
|
|
220
220
|
".gsd/worktrees/",
|
|
221
221
|
".gsd/auto.lock",
|
|
222
222
|
".gsd/metrics.json",
|
|
223
|
+
".gsd/completed-units.json",
|
|
223
224
|
".gsd/STATE.md",
|
|
224
225
|
];
|
|
225
226
|
|
|
@@ -347,52 +348,76 @@ async function main(): Promise<void> {
|
|
|
347
348
|
rmSync(repo, { recursive: true, force: true });
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
// ─── GitServiceImpl: smart staging
|
|
351
|
+
// ─── GitServiceImpl: smart staging excludes tracked runtime files ──────
|
|
351
352
|
|
|
352
|
-
console.log("\n=== GitServiceImpl: smart staging
|
|
353
|
+
console.log("\n=== GitServiceImpl: smart staging excludes tracked runtime files ===");
|
|
353
354
|
|
|
354
355
|
{
|
|
355
|
-
//
|
|
356
|
-
// the
|
|
357
|
-
//
|
|
358
|
-
//
|
|
356
|
+
// Reproduces the real bug: .gsd/ runtime files that are already tracked
|
|
357
|
+
// (in the git index) must be excluded from staging even when .gsd/ is
|
|
358
|
+
// in .gitignore. The old pathspec-exclude approach failed silently in
|
|
359
|
+
// this case and fell back to `git add -A`, staging everything.
|
|
359
360
|
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
361
|
+
// The fix has three layers:
|
|
362
|
+
// 1. Auto-cleanup: git rm --cached removes tracked runtime files from index
|
|
363
|
+
// 2. Stage-then-unstage: git add -A + git reset HEAD replaces pathspec excludes
|
|
364
|
+
// 3. Pre-checkout discard: git checkout -- .gsd/ clears dirty runtime files
|
|
363
365
|
|
|
364
366
|
const repo = initTempRepo();
|
|
367
|
+
const svc = new GitServiceImpl(repo);
|
|
365
368
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// Simulate: try bad pathspec, catch, fallback
|
|
371
|
-
try {
|
|
372
|
-
runGit(this.basePath, ["add", "-A", "--", ".", ":(exclude)__NONEXISTENT_PATHSPEC_SYNTAX_ERROR__["]);
|
|
373
|
-
// If the above doesn't throw, git accepted it (some versions do).
|
|
374
|
-
// That's fine — the point is testing the fallback path.
|
|
375
|
-
throw new Error("force fallback for test");
|
|
376
|
-
} catch {
|
|
377
|
-
console.error("GitService: smart staging failed, falling back to git add -A");
|
|
378
|
-
this.fallbackUsed = true;
|
|
379
|
-
runGit(this.basePath, ["add", "-A"]);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const svc = new FallbackTestService(repo);
|
|
369
|
+
// Simulate a repo where .gsd/ files were previously force-added
|
|
370
|
+
createFile(repo, ".gsd/metrics.json", '{"version":1}');
|
|
371
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1"]');
|
|
372
|
+
createFile(repo, ".gsd/activity/log.jsonl", '{"ts":1}');
|
|
385
373
|
createFile(repo, "src/real.ts", "real code");
|
|
386
|
-
|
|
374
|
+
// Force-add .gsd/ files to simulate historical tracking
|
|
375
|
+
runGit(repo, ["add", "-f", ".gsd/metrics.json", ".gsd/completed-units.json", ".gsd/activity/log.jsonl", "src/real.ts"]);
|
|
376
|
+
runGit(repo, ["commit", "-F", "-"], { input: "init with tracked runtime files" });
|
|
377
|
+
|
|
378
|
+
// Add .gitignore with .gsd/ (matches real-world setup from ensureGitignore)
|
|
379
|
+
createFile(repo, ".gitignore", ".gsd/\n");
|
|
380
|
+
runGit(repo, ["add", ".gitignore"]);
|
|
381
|
+
runGit(repo, ["commit", "-F", "-"], { input: "add gitignore" });
|
|
382
|
+
|
|
383
|
+
// Verify runtime files are tracked (precondition)
|
|
384
|
+
const tracked = run("git ls-files .gsd/", repo);
|
|
385
|
+
assert(tracked.includes("metrics.json"), "precondition: metrics.json tracked");
|
|
386
|
+
assert(tracked.includes("completed-units.json"), "precondition: completed-units.json tracked");
|
|
387
|
+
assert(tracked.includes("activity/log.jsonl"), "precondition: activity log tracked");
|
|
388
|
+
|
|
389
|
+
// Now modify both runtime and real files
|
|
390
|
+
createFile(repo, ".gsd/metrics.json", '{"version":2}');
|
|
391
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1","unit2"]');
|
|
392
|
+
createFile(repo, ".gsd/activity/log.jsonl", '{"ts":2}');
|
|
393
|
+
createFile(repo, "src/real.ts", "updated code");
|
|
394
|
+
|
|
395
|
+
// autoCommit should commit real.ts. The first call also runs auto-cleanup
|
|
396
|
+
// which removes runtime files from the index via a dedicated commit.
|
|
397
|
+
const msg = svc.autoCommit("execute-task", "M001/S01/T01");
|
|
398
|
+
assert(msg !== null, "autoCommit produces a commit");
|
|
399
|
+
|
|
400
|
+
const show = run("git show --stat HEAD", repo);
|
|
401
|
+
assert(show.includes("src/real.ts"), "real files are committed");
|
|
402
|
+
|
|
403
|
+
// After the commit, runtime files must no longer be in the git index.
|
|
404
|
+
// They remain on disk but are untracked (protected by .gitignore).
|
|
405
|
+
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
406
|
+
assertEq(trackedAfter, "", "no .gsd/ runtime files remain in the index");
|
|
387
407
|
|
|
388
|
-
//
|
|
389
|
-
|
|
408
|
+
// Verify a second autoCommit with changed runtime files does NOT stage them
|
|
409
|
+
createFile(repo, ".gsd/metrics.json", '{"version":3}');
|
|
410
|
+
createFile(repo, ".gsd/completed-units.json", '["unit1","unit2","unit3"]');
|
|
411
|
+
createFile(repo, "src/real.ts", "third version");
|
|
390
412
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
assert(
|
|
413
|
+
const msg2 = svc.autoCommit("execute-task", "M001/S01/T02");
|
|
414
|
+
assert(msg2 !== null, "second autoCommit produces a commit");
|
|
415
|
+
|
|
416
|
+
const show2 = run("git show --stat HEAD", repo);
|
|
417
|
+
assert(show2.includes("src/real.ts"), "real files committed in second commit");
|
|
418
|
+
assert(!show2.includes("metrics"), "metrics.json not in second commit");
|
|
419
|
+
assert(!show2.includes("completed-units"), "completed-units.json not in second commit");
|
|
420
|
+
assert(!show2.includes("activity"), "activity not in second commit");
|
|
396
421
|
|
|
397
422
|
rmSync(repo, { recursive: true, force: true });
|
|
398
423
|
}
|
|
@@ -1345,6 +1370,58 @@ async function main(): Promise<void> {
|
|
|
1345
1370
|
assert(true, "PreMergeCheckResult type exported and usable");
|
|
1346
1371
|
}
|
|
1347
1372
|
|
|
1373
|
+
// ─── untrackRuntimeFiles: removes tracked runtime files from index ───
|
|
1374
|
+
|
|
1375
|
+
console.log("\n=== untrackRuntimeFiles ===");
|
|
1376
|
+
|
|
1377
|
+
{
|
|
1378
|
+
const { untrackRuntimeFiles } = await import("../gitignore.ts");
|
|
1379
|
+
const repo = mkdtempSync(join(tmpdir(), "gsd-untrack-"));
|
|
1380
|
+
run("git init -b main", repo);
|
|
1381
|
+
run("git config user.email test@test.com", repo);
|
|
1382
|
+
run("git config user.name Test", repo);
|
|
1383
|
+
|
|
1384
|
+
// Create and track runtime files (simulates pre-.gitignore state)
|
|
1385
|
+
mkdirSync(join(repo, ".gsd", "activity"), { recursive: true });
|
|
1386
|
+
mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true });
|
|
1387
|
+
writeFileSync(join(repo, ".gsd", "completed-units.json"), '["u1"]');
|
|
1388
|
+
writeFileSync(join(repo, ".gsd", "metrics.json"), '{}');
|
|
1389
|
+
writeFileSync(join(repo, ".gsd", "STATE.md"), "# State");
|
|
1390
|
+
writeFileSync(join(repo, ".gsd", "activity", "log.jsonl"), "{}");
|
|
1391
|
+
writeFileSync(join(repo, ".gsd", "runtime", "data.json"), "{}");
|
|
1392
|
+
writeFileSync(join(repo, "src.ts"), "code");
|
|
1393
|
+
run("git add -A", repo);
|
|
1394
|
+
run("git commit -m init", repo);
|
|
1395
|
+
|
|
1396
|
+
// Precondition: runtime files are tracked
|
|
1397
|
+
const trackedBefore = run("git ls-files .gsd/", repo);
|
|
1398
|
+
assert(trackedBefore.includes("completed-units.json"), "untrack: precondition — completed-units tracked");
|
|
1399
|
+
assert(trackedBefore.includes("metrics.json"), "untrack: precondition — metrics tracked");
|
|
1400
|
+
|
|
1401
|
+
// Run untrackRuntimeFiles
|
|
1402
|
+
untrackRuntimeFiles(repo);
|
|
1403
|
+
|
|
1404
|
+
// Runtime files should be removed from the index
|
|
1405
|
+
const trackedAfter = run("git ls-files .gsd/", repo);
|
|
1406
|
+
assertEq(trackedAfter, "", "untrack: all runtime files removed from index");
|
|
1407
|
+
|
|
1408
|
+
// Non-runtime files remain tracked
|
|
1409
|
+
const srcTracked = run("git ls-files src.ts", repo);
|
|
1410
|
+
assert(srcTracked.includes("src.ts"), "untrack: non-runtime files remain tracked");
|
|
1411
|
+
|
|
1412
|
+
// Files still exist on disk
|
|
1413
|
+
assert(existsSync(join(repo, ".gsd", "completed-units.json")),
|
|
1414
|
+
"untrack: completed-units.json still on disk");
|
|
1415
|
+
assert(existsSync(join(repo, ".gsd", "metrics.json")),
|
|
1416
|
+
"untrack: metrics.json still on disk");
|
|
1417
|
+
|
|
1418
|
+
// Idempotent — running again doesn't error
|
|
1419
|
+
untrackRuntimeFiles(repo);
|
|
1420
|
+
assert(true, "untrack: second call is idempotent (no error)");
|
|
1421
|
+
|
|
1422
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1348
1425
|
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
1349
1426
|
if (failed > 0) process.exit(1);
|
|
1350
1427
|
console.log("All tests passed ✓");
|
|
@@ -54,7 +54,7 @@ async function main(): Promise<void> {
|
|
|
54
54
|
const testVars = {
|
|
55
55
|
milestoneId: "M099",
|
|
56
56
|
completedSliceId: "S03",
|
|
57
|
-
|
|
57
|
+
assessmentPath: ".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md",
|
|
58
58
|
roadmapPath: ".gsd/milestones/M099/M099-ROADMAP.md",
|
|
59
59
|
inlinedContext: "--- test inlined context block ---",
|
|
60
60
|
};
|
|
@@ -75,14 +75,14 @@ async function main(): Promise<void> {
|
|
|
75
75
|
// Verify all test variables were substituted into the output
|
|
76
76
|
assert(result.includes("M099"), "prompt contains milestoneId 'M099'");
|
|
77
77
|
assert(result.includes("S03"), "prompt contains completedSliceId 'S03'");
|
|
78
|
-
assert(result.includes(".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md"), "prompt contains
|
|
78
|
+
assert(result.includes(".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md"), "prompt contains assessmentPath");
|
|
79
79
|
assert(result.includes(".gsd/milestones/M099/M099-ROADMAP.md"), "prompt contains roadmapPath");
|
|
80
80
|
assert(result.includes("--- test inlined context block ---"), "prompt contains inlinedContext");
|
|
81
81
|
|
|
82
82
|
// Verify no un-substituted variables remain
|
|
83
83
|
assert(!result.includes("{{milestoneId}}"), "no un-substituted {{milestoneId}}");
|
|
84
84
|
assert(!result.includes("{{completedSliceId}}"), "no un-substituted {{completedSliceId}}");
|
|
85
|
-
assert(!result.includes("{{
|
|
85
|
+
assert(!result.includes("{{assessmentPath}}"), "no un-substituted {{assessmentPath}}");
|
|
86
86
|
assert(!result.includes("{{roadmapPath}}"), "no un-substituted {{roadmapPath}}");
|
|
87
87
|
assert(!result.includes("{{inlinedContext}}"), "no un-substituted {{inlinedContext}}");
|
|
88
88
|
}
|
|
@@ -93,7 +93,7 @@ async function main(): Promise<void> {
|
|
|
93
93
|
const prompt = loadPromptFromWorktree("reassess-roadmap", {
|
|
94
94
|
milestoneId: "M001",
|
|
95
95
|
completedSliceId: "S01",
|
|
96
|
-
|
|
96
|
+
assessmentPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
|
|
97
97
|
roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
|
|
98
98
|
inlinedContext: "context",
|
|
99
99
|
});
|
|
@@ -132,7 +132,7 @@ async function main(): Promise<void> {
|
|
|
132
132
|
const prompt = loadPromptFromWorktree("reassess-roadmap", {
|
|
133
133
|
milestoneId: "M001",
|
|
134
134
|
completedSliceId: "S01",
|
|
135
|
-
|
|
135
|
+
assessmentPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
|
|
136
136
|
roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
|
|
137
137
|
inlinedContext: "context",
|
|
138
138
|
});
|
|
@@ -404,12 +404,13 @@ console.log('\n=== prompt: replan-slice contains preserve-completed-tasks instru
|
|
|
404
404
|
slicePath: '.gsd/milestones/M001/slices/S01',
|
|
405
405
|
planPath: '.gsd/milestones/M001/slices/S01/S01-PLAN.md',
|
|
406
406
|
blockerTaskId: 'T01',
|
|
407
|
+
replanPath: '.gsd/milestones/M001/slices/S01/S01-REPLAN.md',
|
|
407
408
|
inlinedContext: '',
|
|
408
409
|
});
|
|
409
410
|
|
|
410
411
|
assert(prompt.includes('Do NOT renumber or remove completed tasks'), 'prompt contains preserve-completed-tasks instruction');
|
|
411
412
|
assert(prompt.includes('[x]'), 'prompt mentions [x] checkmarks');
|
|
412
|
-
assert(prompt.includes('
|
|
413
|
+
assert(prompt.includes('REPLAN'), 'prompt references replan output path');
|
|
413
414
|
assert(prompt.includes('blocker_discovered'), 'prompt mentions blocker_discovered');
|
|
414
415
|
}
|
|
415
416
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { parseRoadmap } from "../files.ts";
|
|
2
|
+
import { parseRoadmapSlices } from "../roadmap-slices.ts";
|
|
3
|
+
|
|
4
|
+
let passed = 0;
|
|
5
|
+
let failed = 0;
|
|
6
|
+
|
|
7
|
+
function assert(condition: boolean, message: string): void {
|
|
8
|
+
if (condition) passed += 1;
|
|
9
|
+
else {
|
|
10
|
+
failed += 1;
|
|
11
|
+
console.error(`FAIL: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertEq<T>(actual: T, expected: T, message: string): void {
|
|
16
|
+
if (JSON.stringify(actual) === JSON.stringify(expected)) passed += 1;
|
|
17
|
+
else {
|
|
18
|
+
failed += 1;
|
|
19
|
+
console.error(`FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const content = `# M003: Current
|
|
24
|
+
|
|
25
|
+
**Vision:** Build the thing.
|
|
26
|
+
|
|
27
|
+
## Slices
|
|
28
|
+
- [x] **S01: First Slice** \`risk:low\` \`depends:[]\`
|
|
29
|
+
> After this: First demo works.
|
|
30
|
+
- [ ] **S02: Second Slice** \`risk:medium\` \`depends:[S01]\`
|
|
31
|
+
- [x] **S03: Third Slice** \`depends:[S01, S02]\`
|
|
32
|
+
> After this: Third demo works.
|
|
33
|
+
|
|
34
|
+
## Boundary Map
|
|
35
|
+
### S01 → S02
|
|
36
|
+
Produces:
|
|
37
|
+
foo.ts
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
console.log("\n=== parseRoadmapSlices ===");
|
|
41
|
+
const slices = parseRoadmapSlices(content);
|
|
42
|
+
assertEq(slices.length, 3, "slice count");
|
|
43
|
+
assertEq(slices[0]?.id, "S01", "first id");
|
|
44
|
+
assertEq(slices[0]?.done, true, "first done");
|
|
45
|
+
assertEq(slices[0]?.demo, "First demo works.", "first demo");
|
|
46
|
+
assertEq(slices[1]?.depends, ["S01"], "second depends");
|
|
47
|
+
assertEq(slices[1]?.risk, "medium", "second risk");
|
|
48
|
+
assertEq(slices[2]?.risk, "low", "missing risk defaults to low");
|
|
49
|
+
assertEq(slices[2]?.depends, ["S01", "S02"], "third depends");
|
|
50
|
+
|
|
51
|
+
console.log("\n=== parseRoadmap integration ===");
|
|
52
|
+
const roadmap = parseRoadmap(content);
|
|
53
|
+
assertEq(roadmap.slices, slices, "parseRoadmap uses extracted slice parser");
|
|
54
|
+
assertEq(roadmap.title, "M003: Current", "roadmap title preserved");
|
|
55
|
+
assertEq(roadmap.vision, "Build the thing.", "roadmap vision preserved");
|
|
56
|
+
assert(roadmap.boundaryMap.length === 1, "boundary map still parsed");
|
|
57
|
+
|
|
58
|
+
console.log(`Passed: ${passed}, Failed: ${failed}`);
|
|
59
|
+
if (failed > 0) process.exit(1);
|