@kata-sh/cli 0.1.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
import { parseRoadmap, parsePlan, parseSummary, parseContinue, parseRequirementCounts } from '../files.ts';
|
|
2
|
+
|
|
3
|
+
let passed = 0;
|
|
4
|
+
let failed = 0;
|
|
5
|
+
|
|
6
|
+
function assert(condition: boolean, message: string): void {
|
|
7
|
+
if (condition) passed++;
|
|
8
|
+
else {
|
|
9
|
+
failed++;
|
|
10
|
+
console.error(` FAIL: ${message}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function assertEq<T>(actual: T, expected: T, message: string): void {
|
|
15
|
+
if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
|
|
16
|
+
else {
|
|
17
|
+
failed++;
|
|
18
|
+
console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
// parseRoadmap tests
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
console.log('\n=== parseRoadmap: full roadmap ===');
|
|
27
|
+
{
|
|
28
|
+
const content = `# M001: Kata Extension — Hierarchical Planning
|
|
29
|
+
|
|
30
|
+
**Vision:** Build a structured planning system for coding agents.
|
|
31
|
+
|
|
32
|
+
**Success Criteria:**
|
|
33
|
+
- All parsers have test coverage
|
|
34
|
+
- Round-trip formatting preserves data
|
|
35
|
+
- State derivation works correctly
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Slices
|
|
40
|
+
|
|
41
|
+
- [x] **S01: Types + File I/O** \`risk:low\` \`depends:[]\`
|
|
42
|
+
> After this: All types defined and parsers work.
|
|
43
|
+
|
|
44
|
+
- [ ] **S02: State Derivation** \`risk:medium\` \`depends:[S01]\`
|
|
45
|
+
> After this: Dashboard shows real-time state.
|
|
46
|
+
|
|
47
|
+
- [ ] **S03: Auto Mode** \`risk:high\` \`depends:[S01, S02]\`
|
|
48
|
+
> After this: Agent can execute tasks automatically.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Boundary Map
|
|
53
|
+
|
|
54
|
+
### S01 → S02
|
|
55
|
+
\`\`\`
|
|
56
|
+
Produces:
|
|
57
|
+
types.ts — all type definitions
|
|
58
|
+
files.ts — parser and formatter functions
|
|
59
|
+
|
|
60
|
+
Consumes from S02:
|
|
61
|
+
nothing
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
### S02 → S03
|
|
65
|
+
\`\`\`
|
|
66
|
+
Produces:
|
|
67
|
+
state.ts — deriveState function
|
|
68
|
+
|
|
69
|
+
Consumes from S03:
|
|
70
|
+
auto-mode entry points
|
|
71
|
+
\`\`\`
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const r = parseRoadmap(content);
|
|
75
|
+
|
|
76
|
+
assertEq(r.title, 'M001: Kata Extension — Hierarchical Planning', 'roadmap title');
|
|
77
|
+
assertEq(r.vision, 'Build a structured planning system for coding agents.', 'roadmap vision');
|
|
78
|
+
assertEq(r.successCriteria.length, 3, 'success criteria count');
|
|
79
|
+
assertEq(r.successCriteria[0], 'All parsers have test coverage', 'first success criterion');
|
|
80
|
+
assertEq(r.successCriteria[2], 'State derivation works correctly', 'third success criterion');
|
|
81
|
+
|
|
82
|
+
// Slices
|
|
83
|
+
assertEq(r.slices.length, 3, 'slice count');
|
|
84
|
+
|
|
85
|
+
assertEq(r.slices[0].id, 'S01', 'S01 id');
|
|
86
|
+
assertEq(r.slices[0].title, 'Types + File I/O', 'S01 title');
|
|
87
|
+
assertEq(r.slices[0].risk, 'low', 'S01 risk');
|
|
88
|
+
assertEq(r.slices[0].depends, [], 'S01 depends');
|
|
89
|
+
assertEq(r.slices[0].done, true, 'S01 done');
|
|
90
|
+
assertEq(r.slices[0].demo, 'All types defined and parsers work.', 'S01 demo');
|
|
91
|
+
|
|
92
|
+
assertEq(r.slices[1].id, 'S02', 'S02 id');
|
|
93
|
+
assertEq(r.slices[1].title, 'State Derivation', 'S02 title');
|
|
94
|
+
assertEq(r.slices[1].risk, 'medium', 'S02 risk');
|
|
95
|
+
assertEq(r.slices[1].depends, ['S01'], 'S02 depends');
|
|
96
|
+
assertEq(r.slices[1].done, false, 'S02 done');
|
|
97
|
+
|
|
98
|
+
assertEq(r.slices[2].id, 'S03', 'S03 id');
|
|
99
|
+
assertEq(r.slices[2].risk, 'high', 'S03 risk');
|
|
100
|
+
assertEq(r.slices[2].depends, ['S01', 'S02'], 'S03 depends');
|
|
101
|
+
assertEq(r.slices[2].done, false, 'S03 done');
|
|
102
|
+
|
|
103
|
+
// Boundary map
|
|
104
|
+
assertEq(r.boundaryMap.length, 2, 'boundary map entry count');
|
|
105
|
+
assertEq(r.boundaryMap[0].fromSlice, 'S01', 'bm[0] from');
|
|
106
|
+
assertEq(r.boundaryMap[0].toSlice, 'S02', 'bm[0] to');
|
|
107
|
+
assert(r.boundaryMap[0].produces.includes('types.ts'), 'bm[0] produces mentions types.ts');
|
|
108
|
+
assertEq(r.boundaryMap[1].fromSlice, 'S02', 'bm[1] from');
|
|
109
|
+
assertEq(r.boundaryMap[1].toSlice, 'S03', 'bm[1] to');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log('\n=== parseRoadmap: empty slices section ===');
|
|
113
|
+
{
|
|
114
|
+
const content = `# M002: Empty Milestone
|
|
115
|
+
|
|
116
|
+
**Vision:** Nothing yet.
|
|
117
|
+
|
|
118
|
+
## Slices
|
|
119
|
+
|
|
120
|
+
## Boundary Map
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
const r = parseRoadmap(content);
|
|
124
|
+
assertEq(r.title, 'M002: Empty Milestone', 'title with empty slices');
|
|
125
|
+
assertEq(r.slices.length, 0, 'no slices parsed');
|
|
126
|
+
assertEq(r.boundaryMap.length, 0, 'no boundary map entries');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('\n=== parseRoadmap: malformed checkbox lines ===');
|
|
130
|
+
{
|
|
131
|
+
// Lines that don't match the expected bold pattern should be skipped
|
|
132
|
+
const content = `# M003: Malformed
|
|
133
|
+
|
|
134
|
+
**Vision:** Test malformed lines.
|
|
135
|
+
|
|
136
|
+
## Slices
|
|
137
|
+
|
|
138
|
+
- [ ] S01: Missing bold markers \`risk:low\` \`depends:[]\`
|
|
139
|
+
- [x] **S02: Valid Slice** \`risk:medium\` \`depends:[]\`
|
|
140
|
+
> After this: Works.
|
|
141
|
+
- [ ] Not a checkbox at all
|
|
142
|
+
Some random text
|
|
143
|
+
- [x] **S03: Another Valid** \`risk:high\` \`depends:[S02]\`
|
|
144
|
+
> After this: Also works.
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
const r = parseRoadmap(content);
|
|
148
|
+
// Only S02 and S03 should be parsed (malformed lines without bold markers are skipped)
|
|
149
|
+
assertEq(r.slices.length, 2, 'only valid slices parsed from malformed input');
|
|
150
|
+
assertEq(r.slices[0].id, 'S02', 'first valid slice is S02');
|
|
151
|
+
assertEq(r.slices[0].done, true, 'S02 done');
|
|
152
|
+
assertEq(r.slices[1].id, 'S03', 'second valid slice is S03');
|
|
153
|
+
assertEq(r.slices[1].depends, ['S02'], 'S03 depends on S02');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('\n=== parseRoadmap: lowercase vs uppercase X for done ===');
|
|
157
|
+
{
|
|
158
|
+
const content = `# M004: Case Test
|
|
159
|
+
|
|
160
|
+
**Vision:** Test X case sensitivity.
|
|
161
|
+
|
|
162
|
+
## Slices
|
|
163
|
+
|
|
164
|
+
- [x] **S01: Lowercase x** \`risk:low\` \`depends:[]\`
|
|
165
|
+
> After this: done.
|
|
166
|
+
|
|
167
|
+
- [X] **S02: Uppercase X** \`risk:low\` \`depends:[]\`
|
|
168
|
+
> After this: also done.
|
|
169
|
+
|
|
170
|
+
- [ ] **S03: Not Done** \`risk:low\` \`depends:[]\`
|
|
171
|
+
> After this: not yet.
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
const r = parseRoadmap(content);
|
|
175
|
+
assertEq(r.slices.length, 3, 'all three slices parsed');
|
|
176
|
+
assertEq(r.slices[0].done, true, 'lowercase x is done');
|
|
177
|
+
assertEq(r.slices[1].done, true, 'uppercase X is done');
|
|
178
|
+
assertEq(r.slices[2].done, false, 'space is not done');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log('\n=== parseRoadmap: missing boundary map ===');
|
|
182
|
+
{
|
|
183
|
+
const content = `# M005: No Boundary Map
|
|
184
|
+
|
|
185
|
+
**Vision:** A roadmap without a boundary map section.
|
|
186
|
+
|
|
187
|
+
**Success Criteria:**
|
|
188
|
+
- One criterion
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Slices
|
|
193
|
+
|
|
194
|
+
- [ ] **S01: Only Slice** \`risk:low\` \`depends:[]\`
|
|
195
|
+
> After this: Done.
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const r = parseRoadmap(content);
|
|
199
|
+
assertEq(r.title, 'M005: No Boundary Map', 'title');
|
|
200
|
+
assertEq(r.slices.length, 1, 'one slice');
|
|
201
|
+
assertEq(r.boundaryMap.length, 0, 'empty boundary map when section missing');
|
|
202
|
+
assertEq(r.successCriteria.length, 1, 'one success criterion');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log('\n=== parseRoadmap: no sections at all ===');
|
|
206
|
+
{
|
|
207
|
+
const content = `# M006: Bare Minimum
|
|
208
|
+
|
|
209
|
+
Just a title and nothing else.
|
|
210
|
+
`;
|
|
211
|
+
|
|
212
|
+
const r = parseRoadmap(content);
|
|
213
|
+
assertEq(r.title, 'M006: Bare Minimum', 'title from bare roadmap');
|
|
214
|
+
assertEq(r.vision, '', 'empty vision');
|
|
215
|
+
assertEq(r.successCriteria.length, 0, 'no success criteria');
|
|
216
|
+
assertEq(r.slices.length, 0, 'no slices');
|
|
217
|
+
assertEq(r.boundaryMap.length, 0, 'no boundary map');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log('\n=== parseRoadmap: slice with no demo blockquote ===');
|
|
221
|
+
{
|
|
222
|
+
const content = `# M007: No Demo
|
|
223
|
+
|
|
224
|
+
**Vision:** Testing slices without demo lines.
|
|
225
|
+
|
|
226
|
+
## Slices
|
|
227
|
+
|
|
228
|
+
- [ ] **S01: No Demo Here** \`risk:medium\` \`depends:[]\`
|
|
229
|
+
- [ ] **S02: Also No Demo** \`risk:low\` \`depends:[S01]\`
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
const r = parseRoadmap(content);
|
|
233
|
+
assertEq(r.slices.length, 2, 'two slices without demos');
|
|
234
|
+
assertEq(r.slices[0].demo, '', 'S01 demo empty');
|
|
235
|
+
assertEq(r.slices[1].demo, '', 'S02 demo empty');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log('\n=== parseRoadmap: missing risk defaults to low ===');
|
|
239
|
+
{
|
|
240
|
+
const content = `# M008: Default Risk
|
|
241
|
+
|
|
242
|
+
**Vision:** Test default risk.
|
|
243
|
+
|
|
244
|
+
## Slices
|
|
245
|
+
|
|
246
|
+
- [ ] **S01: No Risk Tag** \`depends:[]\`
|
|
247
|
+
> After this: done.
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
const r = parseRoadmap(content);
|
|
251
|
+
assertEq(r.slices.length, 1, 'one slice');
|
|
252
|
+
assertEq(r.slices[0].risk, 'low', 'default risk is low');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
256
|
+
// parsePlan tests
|
|
257
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
258
|
+
|
|
259
|
+
console.log('\n=== parsePlan: full plan ===');
|
|
260
|
+
{
|
|
261
|
+
const content = `# S01: Parser Test Suite
|
|
262
|
+
|
|
263
|
+
**Goal:** All 5 parsers have test coverage with edge cases.
|
|
264
|
+
**Demo:** \`node --test tests/parsers.test.ts\` passes with zero failures.
|
|
265
|
+
|
|
266
|
+
## Must-Haves
|
|
267
|
+
|
|
268
|
+
- parseRoadmap tests cover happy path and edge cases
|
|
269
|
+
- parsePlan tests cover happy path and edge cases
|
|
270
|
+
- All existing tests still pass
|
|
271
|
+
|
|
272
|
+
## Tasks
|
|
273
|
+
|
|
274
|
+
- [ ] **T01: Test parseRoadmap and parsePlan** \`est:45m\`
|
|
275
|
+
Create tests/parsers.test.ts with comprehensive tests for the two most complex parsers.
|
|
276
|
+
|
|
277
|
+
- [x] **T02: Test parseSummary and parseContinue** \`est:35m\`
|
|
278
|
+
Extend tests/parsers.test.ts with tests for the remaining parsers.
|
|
279
|
+
|
|
280
|
+
## Files Likely Touched
|
|
281
|
+
|
|
282
|
+
- \`tests/parsers.test.ts\` — new test file
|
|
283
|
+
- \`types.ts\` — add observability_surfaces
|
|
284
|
+
- \`files.ts\` — update parseSummary
|
|
285
|
+
`;
|
|
286
|
+
|
|
287
|
+
const p = parsePlan(content);
|
|
288
|
+
|
|
289
|
+
assertEq(p.id, 'S01', 'plan id');
|
|
290
|
+
assertEq(p.title, 'Parser Test Suite', 'plan title');
|
|
291
|
+
assertEq(p.goal, 'All 5 parsers have test coverage with edge cases.', 'plan goal');
|
|
292
|
+
assertEq(p.demo, '`node --test tests/parsers.test.ts` passes with zero failures.', 'plan demo');
|
|
293
|
+
|
|
294
|
+
// Must-haves
|
|
295
|
+
assertEq(p.mustHaves.length, 3, 'must-have count');
|
|
296
|
+
assertEq(p.mustHaves[0], 'parseRoadmap tests cover happy path and edge cases', 'first must-have');
|
|
297
|
+
|
|
298
|
+
// Tasks
|
|
299
|
+
assertEq(p.tasks.length, 2, 'task count');
|
|
300
|
+
|
|
301
|
+
assertEq(p.tasks[0].id, 'T01', 'T01 id');
|
|
302
|
+
assertEq(p.tasks[0].title, 'Test parseRoadmap and parsePlan', 'T01 title');
|
|
303
|
+
assertEq(p.tasks[0].done, false, 'T01 not done');
|
|
304
|
+
assert(p.tasks[0].description.includes('comprehensive tests'), 'T01 description content');
|
|
305
|
+
|
|
306
|
+
assertEq(p.tasks[1].id, 'T02', 'T02 id');
|
|
307
|
+
assertEq(p.tasks[1].title, 'Test parseSummary and parseContinue', 'T02 title');
|
|
308
|
+
assertEq(p.tasks[1].done, true, 'T02 done');
|
|
309
|
+
|
|
310
|
+
// Files likely touched
|
|
311
|
+
assertEq(p.filesLikelyTouched.length, 3, 'files likely touched count');
|
|
312
|
+
assert(p.filesLikelyTouched[0].includes('tests/parsers.test.ts'), 'first file');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('\n=== parsePlan: multi-line task description concatenation ===');
|
|
316
|
+
{
|
|
317
|
+
const content = `# S02: Multi-line Test
|
|
318
|
+
|
|
319
|
+
**Goal:** Test multi-line descriptions.
|
|
320
|
+
**Demo:** Descriptions are concatenated.
|
|
321
|
+
|
|
322
|
+
## Must-Haves
|
|
323
|
+
|
|
324
|
+
- Multi-line works
|
|
325
|
+
|
|
326
|
+
## Tasks
|
|
327
|
+
|
|
328
|
+
- [ ] **T01: Multi-line Task** \`est:30m\`
|
|
329
|
+
First line of description.
|
|
330
|
+
Second line of description.
|
|
331
|
+
Third line of description.
|
|
332
|
+
|
|
333
|
+
- [ ] **T02: Single Line** \`est:10m\`
|
|
334
|
+
Just one line.
|
|
335
|
+
|
|
336
|
+
## Files Likely Touched
|
|
337
|
+
|
|
338
|
+
- \`foo.ts\`
|
|
339
|
+
`;
|
|
340
|
+
|
|
341
|
+
const p = parsePlan(content);
|
|
342
|
+
|
|
343
|
+
assertEq(p.tasks.length, 2, 'two tasks');
|
|
344
|
+
// Multi-line descriptions should be concatenated with spaces
|
|
345
|
+
assert(p.tasks[0].description.includes('First line'), 'T01 desc has first line');
|
|
346
|
+
assert(p.tasks[0].description.includes('Second line'), 'T01 desc has second line');
|
|
347
|
+
assert(p.tasks[0].description.includes('Third line'), 'T01 desc has third line');
|
|
348
|
+
// Verify concatenation with space separator
|
|
349
|
+
assert(p.tasks[0].description.includes('description. Second'), 'lines joined with space');
|
|
350
|
+
|
|
351
|
+
assertEq(p.tasks[1].description, 'Just one line.', 'T02 single-line desc');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
console.log('\n=== parsePlan: task with missing estimate ===');
|
|
355
|
+
{
|
|
356
|
+
const content = `# S03: No Estimate
|
|
357
|
+
|
|
358
|
+
**Goal:** Handle tasks without estimates.
|
|
359
|
+
**Demo:** Parser doesn't crash.
|
|
360
|
+
|
|
361
|
+
## Tasks
|
|
362
|
+
|
|
363
|
+
- [ ] **T01: No Estimate Task**
|
|
364
|
+
A task without an estimate backtick.
|
|
365
|
+
|
|
366
|
+
- [ ] **T02: Has Estimate** \`est:20m\`
|
|
367
|
+
This one has an estimate.
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
const p = parsePlan(content);
|
|
371
|
+
|
|
372
|
+
assertEq(p.tasks.length, 2, 'two tasks parsed');
|
|
373
|
+
assertEq(p.tasks[0].id, 'T01', 'T01 id');
|
|
374
|
+
assertEq(p.tasks[0].title, 'No Estimate Task', 'T01 title without estimate');
|
|
375
|
+
assertEq(p.tasks[0].done, false, 'T01 not done');
|
|
376
|
+
// The estimate backtick text appears in description if present, but parser doesn't crash without it
|
|
377
|
+
assertEq(p.tasks[1].id, 'T02', 'T02 id');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log('\n=== parsePlan: empty tasks section ===');
|
|
381
|
+
{
|
|
382
|
+
const content = `# S04: Empty Tasks
|
|
383
|
+
|
|
384
|
+
**Goal:** No tasks yet.
|
|
385
|
+
**Demo:** Nothing.
|
|
386
|
+
|
|
387
|
+
## Must-Haves
|
|
388
|
+
|
|
389
|
+
- Something
|
|
390
|
+
|
|
391
|
+
## Tasks
|
|
392
|
+
|
|
393
|
+
## Files Likely Touched
|
|
394
|
+
|
|
395
|
+
- \`nothing.ts\`
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
const p = parsePlan(content);
|
|
399
|
+
|
|
400
|
+
assertEq(p.id, 'S04', 'plan id with empty tasks');
|
|
401
|
+
assertEq(p.tasks.length, 0, 'no tasks');
|
|
402
|
+
assertEq(p.mustHaves.length, 1, 'one must-have');
|
|
403
|
+
assertEq(p.filesLikelyTouched.length, 1, 'one file');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log('\n=== parsePlan: no H1 ===');
|
|
407
|
+
{
|
|
408
|
+
const content = `**Goal:** A plan without a heading.
|
|
409
|
+
**Demo:** Still parses.
|
|
410
|
+
|
|
411
|
+
## Tasks
|
|
412
|
+
|
|
413
|
+
- [ ] **T01: Orphan Task** \`est:5m\`
|
|
414
|
+
A task in a headingless plan.
|
|
415
|
+
`;
|
|
416
|
+
|
|
417
|
+
const p = parsePlan(content);
|
|
418
|
+
|
|
419
|
+
assertEq(p.id, '', 'empty id without H1');
|
|
420
|
+
assertEq(p.title, '', 'empty title without H1');
|
|
421
|
+
assertEq(p.goal, 'A plan without a heading.', 'goal still parsed');
|
|
422
|
+
assertEq(p.tasks.length, 1, 'task still parsed');
|
|
423
|
+
assertEq(p.tasks[0].id, 'T01', 'task id');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
console.log('\n=== parsePlan: task estimate backtick in description ===');
|
|
427
|
+
{
|
|
428
|
+
// The `est:45m` text appears after the bold closing but before the description lines
|
|
429
|
+
// It should end up as part of the description or be ignored gracefully
|
|
430
|
+
const content = `# S05: Estimate Handling
|
|
431
|
+
|
|
432
|
+
**Goal:** Test estimate text handling.
|
|
433
|
+
**Demo:** Works.
|
|
434
|
+
|
|
435
|
+
## Tasks
|
|
436
|
+
|
|
437
|
+
- [ ] **T01: With Estimate** \`est:45m\`
|
|
438
|
+
Main description here.
|
|
439
|
+
`;
|
|
440
|
+
|
|
441
|
+
const p = parsePlan(content);
|
|
442
|
+
assertEq(p.tasks.length, 1, 'one task');
|
|
443
|
+
assertEq(p.tasks[0].id, 'T01', 'task id');
|
|
444
|
+
assertEq(p.tasks[0].title, 'With Estimate', 'title excludes estimate');
|
|
445
|
+
// The `est:45m` backtick text after ** is not part of the title or description
|
|
446
|
+
// It's on the same line after the regex match captures, so it's in the remainder
|
|
447
|
+
// The description should be the continuation lines
|
|
448
|
+
assert(p.tasks[0].description.includes('Main description'), 'description from continuation line');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log('\n=== parsePlan: uppercase X for done ===');
|
|
452
|
+
{
|
|
453
|
+
const content = `# S06: Case Test
|
|
454
|
+
|
|
455
|
+
**Goal:** Test case.
|
|
456
|
+
**Demo:** Works.
|
|
457
|
+
|
|
458
|
+
## Tasks
|
|
459
|
+
|
|
460
|
+
- [X] **T01: Uppercase Done** \`est:5m\`
|
|
461
|
+
Done with uppercase X.
|
|
462
|
+
|
|
463
|
+
- [x] **T02: Lowercase Done** \`est:5m\`
|
|
464
|
+
Done with lowercase x.
|
|
465
|
+
`;
|
|
466
|
+
|
|
467
|
+
const p = parsePlan(content);
|
|
468
|
+
assertEq(p.tasks[0].done, true, 'uppercase X is done');
|
|
469
|
+
assertEq(p.tasks[1].done, true, 'lowercase x is done');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
console.log('\n=== parsePlan: no Must-Haves section ===');
|
|
473
|
+
{
|
|
474
|
+
const content = `# S07: No Must-Haves
|
|
475
|
+
|
|
476
|
+
**Goal:** Test missing must-haves.
|
|
477
|
+
**Demo:** Parser handles it.
|
|
478
|
+
|
|
479
|
+
## Tasks
|
|
480
|
+
|
|
481
|
+
- [ ] **T01: Only Task** \`est:10m\`
|
|
482
|
+
The only task.
|
|
483
|
+
`;
|
|
484
|
+
|
|
485
|
+
const p = parsePlan(content);
|
|
486
|
+
assertEq(p.mustHaves.length, 0, 'empty must-haves');
|
|
487
|
+
assertEq(p.tasks.length, 1, 'task still parsed');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
console.log('\n=== parsePlan: no Files Likely Touched section ===');
|
|
491
|
+
{
|
|
492
|
+
const content = `# S08: No Files
|
|
493
|
+
|
|
494
|
+
**Goal:** Test missing files section.
|
|
495
|
+
**Demo:** Parser handles it.
|
|
496
|
+
|
|
497
|
+
## Tasks
|
|
498
|
+
|
|
499
|
+
- [ ] **T01: Task** \`est:10m\`
|
|
500
|
+
Description.
|
|
501
|
+
`;
|
|
502
|
+
|
|
503
|
+
const p = parsePlan(content);
|
|
504
|
+
assertEq(p.filesLikelyTouched.length, 0, 'empty files likely touched');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.log('\n=== parsePlan: old-format task entries (no sublines) ===');
|
|
508
|
+
{
|
|
509
|
+
const content = `# S09: Old Format
|
|
510
|
+
|
|
511
|
+
**Goal:** Test old-format compatibility.
|
|
512
|
+
**Demo:** Parser handles entries without sublines.
|
|
513
|
+
|
|
514
|
+
## Tasks
|
|
515
|
+
|
|
516
|
+
- [ ] **T01: Classic Task** \`est:10m\`
|
|
517
|
+
Just a plain description with no labeled sublines.
|
|
518
|
+
`;
|
|
519
|
+
|
|
520
|
+
const p = parsePlan(content);
|
|
521
|
+
assertEq(p.tasks.length, 1, 'one task parsed');
|
|
522
|
+
assertEq(p.tasks[0].id, 'T01', 'task id');
|
|
523
|
+
assertEq(p.tasks[0].title, 'Classic Task', 'task title');
|
|
524
|
+
assertEq(p.tasks[0].done, false, 'task not done');
|
|
525
|
+
assertEq(p.tasks[0].files, undefined, 'files is undefined for old-format entry');
|
|
526
|
+
assertEq(p.tasks[0].verify, undefined, 'verify is undefined for old-format entry');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
console.log('\n=== parsePlan: new-format task entries with Files and Verify sublines ===');
|
|
530
|
+
{
|
|
531
|
+
const content = `# S10: New Format
|
|
532
|
+
|
|
533
|
+
**Goal:** Test new-format subline extraction.
|
|
534
|
+
**Demo:** Parser extracts Files and Verify correctly.
|
|
535
|
+
|
|
536
|
+
## Tasks
|
|
537
|
+
|
|
538
|
+
- [ ] **T01: Modern Task** \`est:15m\`
|
|
539
|
+
- Why: because we need typed plan entries
|
|
540
|
+
- Files: \`types.ts\`, \`files.ts\`
|
|
541
|
+
- Verify: run the test suite
|
|
542
|
+
`;
|
|
543
|
+
|
|
544
|
+
const p = parsePlan(content);
|
|
545
|
+
assertEq(p.tasks.length, 1, 'one task parsed');
|
|
546
|
+
assertEq(p.tasks[0].id, 'T01', 'task id');
|
|
547
|
+
assert(Array.isArray(p.tasks[0].files), 'files is an array');
|
|
548
|
+
assertEq(p.tasks[0].files!.length, 2, 'files array has two entries');
|
|
549
|
+
assertEq(p.tasks[0].files![0], 'types.ts', 'first file is types.ts');
|
|
550
|
+
assertEq(p.tasks[0].files![1], 'files.ts', 'second file is files.ts');
|
|
551
|
+
assertEq(p.tasks[0].verify, 'run the test suite', 'verify string extracted correctly');
|
|
552
|
+
assert(p.tasks[0].description.includes('Why: because we need typed plan entries'), 'Why line accumulates into description');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
556
|
+
// parseSummary tests
|
|
557
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
558
|
+
|
|
559
|
+
console.log('\n=== parseSummary: full summary with all frontmatter fields ===');
|
|
560
|
+
{
|
|
561
|
+
const content = `---
|
|
562
|
+
id: T01
|
|
563
|
+
parent: S01
|
|
564
|
+
milestone: M001
|
|
565
|
+
provides:
|
|
566
|
+
- parseRoadmap test coverage
|
|
567
|
+
- parsePlan test coverage
|
|
568
|
+
requires:
|
|
569
|
+
- slice: S00
|
|
570
|
+
provides: type definitions
|
|
571
|
+
- slice: S02
|
|
572
|
+
provides: state derivation
|
|
573
|
+
affects:
|
|
574
|
+
- auto-mode dispatch
|
|
575
|
+
key_files:
|
|
576
|
+
- tests/parsers.test.ts
|
|
577
|
+
- files.ts
|
|
578
|
+
key_decisions:
|
|
579
|
+
- Use manual assert pattern
|
|
580
|
+
patterns_established:
|
|
581
|
+
- parsers.test.ts is the canonical test location
|
|
582
|
+
drill_down_paths:
|
|
583
|
+
- tests/parsers.test.ts for assertion details
|
|
584
|
+
observability_surfaces:
|
|
585
|
+
- test pass/fail output from node --test
|
|
586
|
+
- exit code 1 on failure
|
|
587
|
+
duration: 23min
|
|
588
|
+
verification_result: pass
|
|
589
|
+
retries: 0
|
|
590
|
+
completed_at: 2025-03-10T08:00:00Z
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
# T01: Test parseRoadmap and parsePlan
|
|
594
|
+
|
|
595
|
+
**Created parsers.test.ts with 98 assertions across 16 test groups.**
|
|
596
|
+
|
|
597
|
+
## What Happened
|
|
598
|
+
|
|
599
|
+
Added comprehensive tests for parseRoadmap and parsePlan.
|
|
600
|
+
|
|
601
|
+
## Deviations
|
|
602
|
+
|
|
603
|
+
None.
|
|
604
|
+
|
|
605
|
+
## Files Created/Modified
|
|
606
|
+
|
|
607
|
+
- \`tests/parsers.test.ts\` — new test file with 98 assertions
|
|
608
|
+
- \`types.ts\` — added observability_surfaces field
|
|
609
|
+
- \`files.ts\` — updated parseSummary extraction
|
|
610
|
+
`;
|
|
611
|
+
|
|
612
|
+
const s = parseSummary(content);
|
|
613
|
+
|
|
614
|
+
// Frontmatter fields
|
|
615
|
+
assertEq(s.frontmatter.id, 'T01', 'summary id');
|
|
616
|
+
assertEq(s.frontmatter.parent, 'S01', 'summary parent');
|
|
617
|
+
assertEq(s.frontmatter.milestone, 'M001', 'summary milestone');
|
|
618
|
+
assertEq(s.frontmatter.provides.length, 2, 'provides count');
|
|
619
|
+
assertEq(s.frontmatter.provides[0], 'parseRoadmap test coverage', 'first provides');
|
|
620
|
+
assertEq(s.frontmatter.provides[1], 'parsePlan test coverage', 'second provides');
|
|
621
|
+
|
|
622
|
+
// requires (nested objects)
|
|
623
|
+
assertEq(s.frontmatter.requires.length, 2, 'requires count');
|
|
624
|
+
assertEq(s.frontmatter.requires[0].slice, 'S00', 'first requires slice');
|
|
625
|
+
assertEq(s.frontmatter.requires[0].provides, 'type definitions', 'first requires provides');
|
|
626
|
+
assertEq(s.frontmatter.requires[1].slice, 'S02', 'second requires slice');
|
|
627
|
+
assertEq(s.frontmatter.requires[1].provides, 'state derivation', 'second requires provides');
|
|
628
|
+
|
|
629
|
+
assertEq(s.frontmatter.affects.length, 1, 'affects count');
|
|
630
|
+
assertEq(s.frontmatter.affects[0], 'auto-mode dispatch', 'affects value');
|
|
631
|
+
assertEq(s.frontmatter.key_files.length, 2, 'key_files count');
|
|
632
|
+
assertEq(s.frontmatter.key_decisions.length, 1, 'key_decisions count');
|
|
633
|
+
assertEq(s.frontmatter.patterns_established.length, 1, 'patterns_established count');
|
|
634
|
+
assertEq(s.frontmatter.drill_down_paths.length, 1, 'drill_down_paths count');
|
|
635
|
+
|
|
636
|
+
// observability_surfaces extraction
|
|
637
|
+
assertEq(s.frontmatter.observability_surfaces.length, 2, 'observability_surfaces count');
|
|
638
|
+
assertEq(s.frontmatter.observability_surfaces[0], 'test pass/fail output from node --test', 'first observability surface');
|
|
639
|
+
assertEq(s.frontmatter.observability_surfaces[1], 'exit code 1 on failure', 'second observability surface');
|
|
640
|
+
|
|
641
|
+
assertEq(s.frontmatter.duration, '23min', 'duration');
|
|
642
|
+
assertEq(s.frontmatter.verification_result, 'pass', 'verification_result');
|
|
643
|
+
assertEq(s.frontmatter.completed_at, '2025-03-10T08:00:00Z', 'completed_at');
|
|
644
|
+
|
|
645
|
+
// Body fields
|
|
646
|
+
assertEq(s.title, 'T01: Test parseRoadmap and parsePlan', 'summary title');
|
|
647
|
+
assertEq(s.oneLiner, 'Created parsers.test.ts with 98 assertions across 16 test groups.', 'one-liner');
|
|
648
|
+
assert(s.whatHappened.includes('comprehensive tests'), 'whatHappened content');
|
|
649
|
+
assertEq(s.deviations, 'None.', 'deviations');
|
|
650
|
+
|
|
651
|
+
// Files modified
|
|
652
|
+
assertEq(s.filesModified.length, 3, 'filesModified count');
|
|
653
|
+
assertEq(s.filesModified[0].path, 'tests/parsers.test.ts', 'first file path');
|
|
654
|
+
assert(s.filesModified[0].description.includes('98 assertions'), 'first file description');
|
|
655
|
+
assertEq(s.filesModified[1].path, 'types.ts', 'second file path');
|
|
656
|
+
assertEq(s.filesModified[2].path, 'files.ts', 'third file path');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.log('\n=== parseSummary: one-liner extraction (bold-wrapped line after H1) ===');
|
|
660
|
+
{
|
|
661
|
+
const content = `# S01: Parser Test Suite
|
|
662
|
+
|
|
663
|
+
**All 5 parsers have test coverage with edge cases.**
|
|
664
|
+
|
|
665
|
+
## What Happened
|
|
666
|
+
|
|
667
|
+
Things happened.
|
|
668
|
+
`;
|
|
669
|
+
|
|
670
|
+
const s = parseSummary(content);
|
|
671
|
+
assertEq(s.title, 'S01: Parser Test Suite', 'title');
|
|
672
|
+
assertEq(s.oneLiner, 'All 5 parsers have test coverage with edge cases.', 'bold one-liner');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
console.log('\n=== parseSummary: non-bold paragraph after H1 (empty one-liner) ===');
|
|
676
|
+
{
|
|
677
|
+
const content = `# T02: Some Task
|
|
678
|
+
|
|
679
|
+
This is just a regular paragraph, not bold.
|
|
680
|
+
|
|
681
|
+
## What Happened
|
|
682
|
+
|
|
683
|
+
Did stuff.
|
|
684
|
+
`;
|
|
685
|
+
|
|
686
|
+
const s = parseSummary(content);
|
|
687
|
+
assertEq(s.title, 'T02: Some Task', 'title');
|
|
688
|
+
assertEq(s.oneLiner, '', 'non-bold line results in empty one-liner');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
console.log('\n=== parseSummary: files-modified parsing (backtick path — description format) ===');
|
|
692
|
+
{
|
|
693
|
+
const content = `# T03: File Changes
|
|
694
|
+
|
|
695
|
+
**One-liner.**
|
|
696
|
+
|
|
697
|
+
## Files Created/Modified
|
|
698
|
+
|
|
699
|
+
- \`src/index.ts\` — main entry point
|
|
700
|
+
- \`src/utils.ts\` — utility functions
|
|
701
|
+
- \`README.md\` — updated docs
|
|
702
|
+
`;
|
|
703
|
+
|
|
704
|
+
const s = parseSummary(content);
|
|
705
|
+
assertEq(s.filesModified.length, 3, 'three files');
|
|
706
|
+
assertEq(s.filesModified[0].path, 'src/index.ts', 'first path');
|
|
707
|
+
assertEq(s.filesModified[0].description, 'main entry point', 'first description');
|
|
708
|
+
assertEq(s.filesModified[1].path, 'src/utils.ts', 'second path');
|
|
709
|
+
assertEq(s.filesModified[2].path, 'README.md', 'third path');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
console.log('\n=== parseSummary: missing frontmatter (safe defaults) ===');
|
|
713
|
+
{
|
|
714
|
+
const content = `# T04: No Frontmatter
|
|
715
|
+
|
|
716
|
+
**Did something.**
|
|
717
|
+
|
|
718
|
+
## What Happened
|
|
719
|
+
|
|
720
|
+
No frontmatter at all.
|
|
721
|
+
`;
|
|
722
|
+
|
|
723
|
+
const s = parseSummary(content);
|
|
724
|
+
assertEq(s.frontmatter.id, '', 'default id empty');
|
|
725
|
+
assertEq(s.frontmatter.parent, '', 'default parent empty');
|
|
726
|
+
assertEq(s.frontmatter.milestone, '', 'default milestone empty');
|
|
727
|
+
assertEq(s.frontmatter.provides.length, 0, 'default provides empty');
|
|
728
|
+
assertEq(s.frontmatter.requires.length, 0, 'default requires empty');
|
|
729
|
+
assertEq(s.frontmatter.affects.length, 0, 'default affects empty');
|
|
730
|
+
assertEq(s.frontmatter.key_files.length, 0, 'default key_files empty');
|
|
731
|
+
assertEq(s.frontmatter.key_decisions.length, 0, 'default key_decisions empty');
|
|
732
|
+
assertEq(s.frontmatter.patterns_established.length, 0, 'default patterns_established empty');
|
|
733
|
+
assertEq(s.frontmatter.drill_down_paths.length, 0, 'default drill_down_paths empty');
|
|
734
|
+
assertEq(s.frontmatter.observability_surfaces.length, 0, 'default observability_surfaces empty');
|
|
735
|
+
assertEq(s.frontmatter.duration, '', 'default duration empty');
|
|
736
|
+
assertEq(s.frontmatter.verification_result, 'untested', 'default verification_result');
|
|
737
|
+
assertEq(s.frontmatter.completed_at, '', 'default completed_at empty');
|
|
738
|
+
assertEq(s.title, 'T04: No Frontmatter', 'title still parsed');
|
|
739
|
+
assertEq(s.oneLiner, 'Did something.', 'one-liner still parsed');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
console.log('\n=== parseSummary: empty body ===');
|
|
743
|
+
{
|
|
744
|
+
const content = `---
|
|
745
|
+
id: T05
|
|
746
|
+
parent: S01
|
|
747
|
+
milestone: M001
|
|
748
|
+
---
|
|
749
|
+
`;
|
|
750
|
+
|
|
751
|
+
const s = parseSummary(content);
|
|
752
|
+
assertEq(s.frontmatter.id, 'T05', 'id from frontmatter');
|
|
753
|
+
assertEq(s.title, '', 'empty title');
|
|
754
|
+
assertEq(s.oneLiner, '', 'empty one-liner');
|
|
755
|
+
assertEq(s.whatHappened, '', 'empty whatHappened');
|
|
756
|
+
assertEq(s.deviations, '', 'empty deviations');
|
|
757
|
+
assertEq(s.filesModified.length, 0, 'no files modified');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
console.log('\n=== parseSummary: summary with requires array (nested objects) ===');
|
|
761
|
+
{
|
|
762
|
+
const content = `---
|
|
763
|
+
id: T06
|
|
764
|
+
parent: S02
|
|
765
|
+
milestone: M001
|
|
766
|
+
requires:
|
|
767
|
+
- slice: S01
|
|
768
|
+
provides: parser functions
|
|
769
|
+
- slice: S00
|
|
770
|
+
provides: core types
|
|
771
|
+
- slice: S03
|
|
772
|
+
provides: state engine
|
|
773
|
+
provides: []
|
|
774
|
+
affects: []
|
|
775
|
+
key_files: []
|
|
776
|
+
key_decisions: []
|
|
777
|
+
patterns_established: []
|
|
778
|
+
drill_down_paths: []
|
|
779
|
+
observability_surfaces: []
|
|
780
|
+
duration: 10min
|
|
781
|
+
verification_result: pass
|
|
782
|
+
retries: 1
|
|
783
|
+
completed_at: 2025-03-10T09:00:00Z
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
# T06: Nested Requires
|
|
787
|
+
|
|
788
|
+
**Test nested requires parsing.**
|
|
789
|
+
|
|
790
|
+
## What Happened
|
|
791
|
+
|
|
792
|
+
Tested.
|
|
793
|
+
`;
|
|
794
|
+
|
|
795
|
+
const s = parseSummary(content);
|
|
796
|
+
assertEq(s.frontmatter.requires.length, 3, 'three requires entries');
|
|
797
|
+
assertEq(s.frontmatter.requires[0].slice, 'S01', 'first requires slice');
|
|
798
|
+
assertEq(s.frontmatter.requires[0].provides, 'parser functions', 'first requires provides');
|
|
799
|
+
assertEq(s.frontmatter.requires[1].slice, 'S00', 'second requires slice');
|
|
800
|
+
assertEq(s.frontmatter.requires[2].slice, 'S03', 'third requires slice');
|
|
801
|
+
assertEq(s.frontmatter.requires[2].provides, 'state engine', 'third requires provides');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
805
|
+
// parseContinue tests
|
|
806
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
807
|
+
|
|
808
|
+
console.log('\n=== parseContinue: full continue file with all frontmatter fields ===');
|
|
809
|
+
{
|
|
810
|
+
const content = `---
|
|
811
|
+
milestone: M001
|
|
812
|
+
slice: S01
|
|
813
|
+
task: T02
|
|
814
|
+
step: 3
|
|
815
|
+
total_steps: 5
|
|
816
|
+
status: in_progress
|
|
817
|
+
saved_at: 2025-03-10T08:30:00Z
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## Completed Work
|
|
821
|
+
|
|
822
|
+
Steps 1-3 are done. Created test file and wrote assertions.
|
|
823
|
+
|
|
824
|
+
## Remaining Work
|
|
825
|
+
|
|
826
|
+
Steps 4-5: run tests and check regressions.
|
|
827
|
+
|
|
828
|
+
## Decisions Made
|
|
829
|
+
|
|
830
|
+
Used manual assert pattern instead of node:assert.
|
|
831
|
+
|
|
832
|
+
## Context
|
|
833
|
+
|
|
834
|
+
Working in the kata-s01 worktree. All imports use .ts extensions.
|
|
835
|
+
|
|
836
|
+
## Next Action
|
|
837
|
+
|
|
838
|
+
Run the full test suite with node --test.
|
|
839
|
+
`;
|
|
840
|
+
|
|
841
|
+
const c = parseContinue(content);
|
|
842
|
+
|
|
843
|
+
// Frontmatter
|
|
844
|
+
assertEq(c.frontmatter.milestone, 'M001', 'continue milestone');
|
|
845
|
+
assertEq(c.frontmatter.slice, 'S01', 'continue slice');
|
|
846
|
+
assertEq(c.frontmatter.task, 'T02', 'continue task');
|
|
847
|
+
assertEq(c.frontmatter.step, 3, 'continue step');
|
|
848
|
+
assertEq(c.frontmatter.totalSteps, 5, 'continue totalSteps');
|
|
849
|
+
assertEq(c.frontmatter.status, 'in_progress', 'continue status');
|
|
850
|
+
assertEq(c.frontmatter.savedAt, '2025-03-10T08:30:00Z', 'continue savedAt');
|
|
851
|
+
|
|
852
|
+
// Body sections
|
|
853
|
+
assert(c.completedWork.includes('Steps 1-3 are done'), 'completedWork content');
|
|
854
|
+
assert(c.remainingWork.includes('Steps 4-5'), 'remainingWork content');
|
|
855
|
+
assert(c.decisions.includes('manual assert pattern'), 'decisions content');
|
|
856
|
+
assert(c.context.includes('kata-s01 worktree'), 'context content');
|
|
857
|
+
assert(c.nextAction.includes('node --test'), 'nextAction content');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
console.log('\n=== parseContinue: string step/totalSteps parsed as integers ===');
|
|
861
|
+
{
|
|
862
|
+
const content = `---
|
|
863
|
+
milestone: M002
|
|
864
|
+
slice: S03
|
|
865
|
+
task: T01
|
|
866
|
+
step: 7
|
|
867
|
+
total_steps: 12
|
|
868
|
+
status: in_progress
|
|
869
|
+
saved_at: 2025-03-10T10:00:00Z
|
|
870
|
+
---
|
|
871
|
+
|
|
872
|
+
## Completed Work
|
|
873
|
+
|
|
874
|
+
Some work.
|
|
875
|
+
|
|
876
|
+
## Remaining Work
|
|
877
|
+
|
|
878
|
+
More work.
|
|
879
|
+
|
|
880
|
+
## Decisions Made
|
|
881
|
+
|
|
882
|
+
None.
|
|
883
|
+
|
|
884
|
+
## Context
|
|
885
|
+
|
|
886
|
+
None.
|
|
887
|
+
|
|
888
|
+
## Next Action
|
|
889
|
+
|
|
890
|
+
Continue.
|
|
891
|
+
`;
|
|
892
|
+
|
|
893
|
+
const c = parseContinue(content);
|
|
894
|
+
assertEq(c.frontmatter.step, 7, 'step parsed as integer 7');
|
|
895
|
+
assertEq(c.frontmatter.totalSteps, 12, 'totalSteps parsed as integer 12');
|
|
896
|
+
assertEq(typeof c.frontmatter.step, 'number', 'step is number type');
|
|
897
|
+
assertEq(typeof c.frontmatter.totalSteps, 'number', 'totalSteps is number type');
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
console.log('\n=== parseContinue: NaN step values (non-numeric strings) ===');
|
|
901
|
+
{
|
|
902
|
+
const content = `---
|
|
903
|
+
milestone: M001
|
|
904
|
+
slice: S01
|
|
905
|
+
task: T01
|
|
906
|
+
step: abc
|
|
907
|
+
total_steps: xyz
|
|
908
|
+
status: in_progress
|
|
909
|
+
saved_at: 2025-03-10T10:00:00Z
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
## Completed Work
|
|
913
|
+
|
|
914
|
+
Work.
|
|
915
|
+
|
|
916
|
+
## Remaining Work
|
|
917
|
+
|
|
918
|
+
Work.
|
|
919
|
+
|
|
920
|
+
## Decisions Made
|
|
921
|
+
|
|
922
|
+
None.
|
|
923
|
+
|
|
924
|
+
## Context
|
|
925
|
+
|
|
926
|
+
None.
|
|
927
|
+
|
|
928
|
+
## Next Action
|
|
929
|
+
|
|
930
|
+
Do things.
|
|
931
|
+
`;
|
|
932
|
+
|
|
933
|
+
const c = parseContinue(content);
|
|
934
|
+
// parseInt("abc") returns NaN; the parser || 0 fallback should give 0
|
|
935
|
+
// Actually, looking at parser: typeof fm.step === 'string' ? parseInt(fm.step) : ...
|
|
936
|
+
// parseInt("abc") = NaN, and NaN || 0 doesn't work because NaN is falsy only in boolean context
|
|
937
|
+
// But the parser uses: typeof fm.step === 'string' ? parseInt(fm.step) : (fm.step as number) || 0
|
|
938
|
+
// parseInt returns NaN which is a number, not 0 — let's verify
|
|
939
|
+
const stepIsNaN = Number.isNaN(c.frontmatter.step);
|
|
940
|
+
const totalIsNaN = Number.isNaN(c.frontmatter.totalSteps);
|
|
941
|
+
// The parser does parseInt which returns NaN for non-numeric strings
|
|
942
|
+
// There's no || 0 fallback on the parseInt path, so NaN is expected
|
|
943
|
+
assert(stepIsNaN, 'NaN step when non-numeric string');
|
|
944
|
+
assert(totalIsNaN, 'NaN totalSteps when non-numeric string');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
console.log('\n=== parseContinue: all three status variants ===');
|
|
948
|
+
{
|
|
949
|
+
for (const status of ['in_progress', 'interrupted', 'compacted'] as const) {
|
|
950
|
+
const content = `---
|
|
951
|
+
milestone: M001
|
|
952
|
+
slice: S01
|
|
953
|
+
task: T01
|
|
954
|
+
step: 1
|
|
955
|
+
total_steps: 3
|
|
956
|
+
status: ${status}
|
|
957
|
+
saved_at: 2025-03-10T10:00:00Z
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
## Completed Work
|
|
961
|
+
|
|
962
|
+
Work.
|
|
963
|
+
`;
|
|
964
|
+
|
|
965
|
+
const c = parseContinue(content);
|
|
966
|
+
assertEq(c.frontmatter.status, status, `status variant: ${status}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
console.log('\n=== parseContinue: missing frontmatter ===');
|
|
971
|
+
{
|
|
972
|
+
const content = `## Completed Work
|
|
973
|
+
|
|
974
|
+
Some work done.
|
|
975
|
+
|
|
976
|
+
## Remaining Work
|
|
977
|
+
|
|
978
|
+
More to do.
|
|
979
|
+
|
|
980
|
+
## Decisions Made
|
|
981
|
+
|
|
982
|
+
A decision.
|
|
983
|
+
|
|
984
|
+
## Context
|
|
985
|
+
|
|
986
|
+
Some context.
|
|
987
|
+
|
|
988
|
+
## Next Action
|
|
989
|
+
|
|
990
|
+
Next thing.
|
|
991
|
+
`;
|
|
992
|
+
|
|
993
|
+
const c = parseContinue(content);
|
|
994
|
+
assertEq(c.frontmatter.milestone, '', 'default milestone empty');
|
|
995
|
+
assertEq(c.frontmatter.slice, '', 'default slice empty');
|
|
996
|
+
assertEq(c.frontmatter.task, '', 'default task empty');
|
|
997
|
+
assertEq(c.frontmatter.step, 0, 'default step 0');
|
|
998
|
+
assertEq(c.frontmatter.totalSteps, 0, 'default totalSteps 0');
|
|
999
|
+
assertEq(c.frontmatter.status, 'in_progress', 'default status in_progress');
|
|
1000
|
+
assertEq(c.frontmatter.savedAt, '', 'default savedAt empty');
|
|
1001
|
+
|
|
1002
|
+
// Body sections still parse
|
|
1003
|
+
assert(c.completedWork.includes('Some work done'), 'completedWork without frontmatter');
|
|
1004
|
+
assert(c.remainingWork.includes('More to do'), 'remainingWork without frontmatter');
|
|
1005
|
+
assert(c.decisions.includes('A decision'), 'decisions without frontmatter');
|
|
1006
|
+
assert(c.context.includes('Some context'), 'context without frontmatter');
|
|
1007
|
+
assert(c.nextAction.includes('Next thing'), 'nextAction without frontmatter');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
console.log('\n=== parseContinue: body section extraction ===');
|
|
1011
|
+
{
|
|
1012
|
+
const content = `---
|
|
1013
|
+
milestone: M001
|
|
1014
|
+
slice: S01
|
|
1015
|
+
task: T03
|
|
1016
|
+
step: 2
|
|
1017
|
+
total_steps: 4
|
|
1018
|
+
status: interrupted
|
|
1019
|
+
saved_at: 2025-03-10T11:00:00Z
|
|
1020
|
+
---
|
|
1021
|
+
|
|
1022
|
+
## Completed Work
|
|
1023
|
+
|
|
1024
|
+
First paragraph of completed work.
|
|
1025
|
+
Second paragraph continuing the explanation.
|
|
1026
|
+
|
|
1027
|
+
## Remaining Work
|
|
1028
|
+
|
|
1029
|
+
Need to finish step 3 and step 4.
|
|
1030
|
+
|
|
1031
|
+
## Decisions Made
|
|
1032
|
+
|
|
1033
|
+
Decided to use approach A over approach B because of performance.
|
|
1034
|
+
|
|
1035
|
+
## Context
|
|
1036
|
+
|
|
1037
|
+
Running in worktree. Node 22 required. TypeScript strict mode.
|
|
1038
|
+
|
|
1039
|
+
## Next Action
|
|
1040
|
+
|
|
1041
|
+
Pick up at step 3: run the integration tests.
|
|
1042
|
+
`;
|
|
1043
|
+
|
|
1044
|
+
const c = parseContinue(content);
|
|
1045
|
+
assert(c.completedWork.includes('First paragraph'), 'completedWork first paragraph');
|
|
1046
|
+
assert(c.completedWork.includes('Second paragraph'), 'completedWork second paragraph');
|
|
1047
|
+
assert(c.remainingWork.includes('step 3 and step 4'), 'remainingWork detail');
|
|
1048
|
+
assert(c.decisions.includes('approach A over approach B'), 'decisions detail');
|
|
1049
|
+
assert(c.context.includes('Node 22 required'), 'context detail');
|
|
1050
|
+
assert(c.nextAction.includes('step 3: run the integration tests'), 'nextAction detail');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
console.log('\n=== parseContinue: total_steps vs totalSteps key support ===');
|
|
1054
|
+
{
|
|
1055
|
+
// Test total_steps (snake_case) — the primary format
|
|
1056
|
+
const content1 = `---
|
|
1057
|
+
milestone: M001
|
|
1058
|
+
slice: S01
|
|
1059
|
+
task: T01
|
|
1060
|
+
step: 2
|
|
1061
|
+
total_steps: 8
|
|
1062
|
+
status: in_progress
|
|
1063
|
+
saved_at: 2025-03-10T12:00:00Z
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Completed Work
|
|
1067
|
+
|
|
1068
|
+
Work.
|
|
1069
|
+
`;
|
|
1070
|
+
|
|
1071
|
+
const c1 = parseContinue(content1);
|
|
1072
|
+
assertEq(c1.frontmatter.totalSteps, 8, 'total_steps snake_case works');
|
|
1073
|
+
|
|
1074
|
+
// Test totalSteps (camelCase) — the fallback
|
|
1075
|
+
const content2 = `---
|
|
1076
|
+
milestone: M001
|
|
1077
|
+
slice: S01
|
|
1078
|
+
task: T01
|
|
1079
|
+
step: 2
|
|
1080
|
+
totalSteps: 6
|
|
1081
|
+
status: in_progress
|
|
1082
|
+
saved_at: 2025-03-10T12:00:00Z
|
|
1083
|
+
---
|
|
1084
|
+
|
|
1085
|
+
## Completed Work
|
|
1086
|
+
|
|
1087
|
+
Work.
|
|
1088
|
+
`;
|
|
1089
|
+
|
|
1090
|
+
const c2 = parseContinue(content2);
|
|
1091
|
+
assertEq(c2.frontmatter.totalSteps, 6, 'totalSteps camelCase works');
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1095
|
+
// parseRequirementCounts tests
|
|
1096
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1097
|
+
|
|
1098
|
+
console.log('\n=== parseRequirementCounts: full requirements file ===');
|
|
1099
|
+
{
|
|
1100
|
+
const content = `# Requirements
|
|
1101
|
+
|
|
1102
|
+
## Active
|
|
1103
|
+
|
|
1104
|
+
### R001 — User authentication
|
|
1105
|
+
- Status: active
|
|
1106
|
+
|
|
1107
|
+
### R002 — Dashboard rendering
|
|
1108
|
+
- Status: blocked
|
|
1109
|
+
|
|
1110
|
+
### R003 — API rate limiting
|
|
1111
|
+
- Status: active
|
|
1112
|
+
|
|
1113
|
+
## Validated
|
|
1114
|
+
|
|
1115
|
+
### R010 — Parser test coverage
|
|
1116
|
+
- Status: validated
|
|
1117
|
+
|
|
1118
|
+
### R011 — Type system
|
|
1119
|
+
- Status: validated
|
|
1120
|
+
|
|
1121
|
+
## Deferred
|
|
1122
|
+
|
|
1123
|
+
### R020 — Admin panel
|
|
1124
|
+
- Status: deferred
|
|
1125
|
+
|
|
1126
|
+
## Out of Scope
|
|
1127
|
+
|
|
1128
|
+
### R030 — Mobile app
|
|
1129
|
+
- Status: out-of-scope
|
|
1130
|
+
|
|
1131
|
+
### R031 — Desktop app
|
|
1132
|
+
- Status: out-of-scope
|
|
1133
|
+
`;
|
|
1134
|
+
|
|
1135
|
+
const counts = parseRequirementCounts(content);
|
|
1136
|
+
assertEq(counts.active, 3, 'active count');
|
|
1137
|
+
assertEq(counts.validated, 2, 'validated count');
|
|
1138
|
+
assertEq(counts.deferred, 1, 'deferred count');
|
|
1139
|
+
assertEq(counts.outOfScope, 2, 'outOfScope count');
|
|
1140
|
+
assertEq(counts.blocked, 1, 'blocked count');
|
|
1141
|
+
assertEq(counts.total, 8, 'total is sum of active+validated+deferred+outOfScope');
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
console.log('\n=== parseRequirementCounts: null input returns all zeros ===');
|
|
1145
|
+
{
|
|
1146
|
+
const counts = parseRequirementCounts(null);
|
|
1147
|
+
assertEq(counts.active, 0, 'null active');
|
|
1148
|
+
assertEq(counts.validated, 0, 'null validated');
|
|
1149
|
+
assertEq(counts.deferred, 0, 'null deferred');
|
|
1150
|
+
assertEq(counts.outOfScope, 0, 'null outOfScope');
|
|
1151
|
+
assertEq(counts.blocked, 0, 'null blocked');
|
|
1152
|
+
assertEq(counts.total, 0, 'null total');
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
console.log('\n=== parseRequirementCounts: empty sections return zero counts ===');
|
|
1156
|
+
{
|
|
1157
|
+
const content = `# Requirements
|
|
1158
|
+
|
|
1159
|
+
## Active
|
|
1160
|
+
|
|
1161
|
+
## Validated
|
|
1162
|
+
|
|
1163
|
+
## Deferred
|
|
1164
|
+
|
|
1165
|
+
## Out of Scope
|
|
1166
|
+
`;
|
|
1167
|
+
|
|
1168
|
+
const counts = parseRequirementCounts(content);
|
|
1169
|
+
assertEq(counts.active, 0, 'empty active');
|
|
1170
|
+
assertEq(counts.validated, 0, 'empty validated');
|
|
1171
|
+
assertEq(counts.deferred, 0, 'empty deferred');
|
|
1172
|
+
assertEq(counts.outOfScope, 0, 'empty outOfScope');
|
|
1173
|
+
assertEq(counts.blocked, 0, 'empty blocked');
|
|
1174
|
+
assertEq(counts.total, 0, 'empty total');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
console.log('\n=== parseRequirementCounts: blocked status counting ===');
|
|
1178
|
+
{
|
|
1179
|
+
const content = `# Requirements
|
|
1180
|
+
|
|
1181
|
+
## Active
|
|
1182
|
+
|
|
1183
|
+
### R001 — Blocked thing
|
|
1184
|
+
- Status: blocked
|
|
1185
|
+
|
|
1186
|
+
### R002 — Another blocked thing
|
|
1187
|
+
- Status: blocked
|
|
1188
|
+
|
|
1189
|
+
### R003 — Active thing
|
|
1190
|
+
- Status: active
|
|
1191
|
+
|
|
1192
|
+
## Validated
|
|
1193
|
+
|
|
1194
|
+
## Deferred
|
|
1195
|
+
|
|
1196
|
+
### R020 — Blocked deferred
|
|
1197
|
+
- Status: blocked
|
|
1198
|
+
|
|
1199
|
+
## Out of Scope
|
|
1200
|
+
`;
|
|
1201
|
+
|
|
1202
|
+
const counts = parseRequirementCounts(content);
|
|
1203
|
+
assertEq(counts.active, 3, 'active includes blocked items in Active section');
|
|
1204
|
+
assertEq(counts.blocked, 3, 'blocked counts all blocked statuses across sections');
|
|
1205
|
+
assertEq(counts.deferred, 1, 'deferred section count');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
console.log('\n=== parseRequirementCounts: total is sum of all section counts ===');
|
|
1209
|
+
{
|
|
1210
|
+
const content = `# Requirements
|
|
1211
|
+
|
|
1212
|
+
## Active
|
|
1213
|
+
|
|
1214
|
+
### R001 — One
|
|
1215
|
+
- Status: active
|
|
1216
|
+
|
|
1217
|
+
## Validated
|
|
1218
|
+
|
|
1219
|
+
### R010 — Two
|
|
1220
|
+
- Status: validated
|
|
1221
|
+
|
|
1222
|
+
### R011 — Three
|
|
1223
|
+
- Status: validated
|
|
1224
|
+
|
|
1225
|
+
## Deferred
|
|
1226
|
+
|
|
1227
|
+
### R020 — Four
|
|
1228
|
+
- Status: deferred
|
|
1229
|
+
|
|
1230
|
+
### R021 — Five
|
|
1231
|
+
- Status: deferred
|
|
1232
|
+
|
|
1233
|
+
### R022 — Six
|
|
1234
|
+
- Status: deferred
|
|
1235
|
+
|
|
1236
|
+
## Out of Scope
|
|
1237
|
+
|
|
1238
|
+
### R030 — Seven
|
|
1239
|
+
- Status: out-of-scope
|
|
1240
|
+
`;
|
|
1241
|
+
|
|
1242
|
+
const counts = parseRequirementCounts(content);
|
|
1243
|
+
assertEq(counts.active, 1, 'one active');
|
|
1244
|
+
assertEq(counts.validated, 2, 'two validated');
|
|
1245
|
+
assertEq(counts.deferred, 3, 'three deferred');
|
|
1246
|
+
assertEq(counts.outOfScope, 1, 'one outOfScope');
|
|
1247
|
+
assertEq(counts.total, 7, 'total = 1 + 2 + 3 + 1');
|
|
1248
|
+
assertEq(counts.total, counts.active + counts.validated + counts.deferred + counts.outOfScope, 'total is exact sum');
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1252
|
+
// Results
|
|
1253
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1254
|
+
|
|
1255
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
1256
|
+
if (failed > 0) process.exit(1);
|
|
1257
|
+
console.log('All tests passed ✓');
|