gsdd-cli 0.18.4 → 0.19.0

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.
Files changed (100) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +625 -607
  3. package/agents/DISTILLATION.md +421 -421
  4. package/agents/README.md +62 -62
  5. package/agents/approach-explorer.md +361 -361
  6. package/agents/debugger.md +82 -82
  7. package/agents/executor.md +394 -394
  8. package/agents/integration-checker.md +318 -318
  9. package/agents/mapper.md +103 -103
  10. package/agents/planner.md +313 -313
  11. package/agents/researcher.md +84 -84
  12. package/agents/roadmapper.md +296 -296
  13. package/agents/synthesizer.md +236 -236
  14. package/agents/verifier.md +337 -337
  15. package/bin/adapters/agents.mjs +34 -34
  16. package/bin/adapters/claude.mjs +191 -191
  17. package/bin/adapters/codex.mjs +85 -85
  18. package/bin/adapters/index.mjs +20 -20
  19. package/bin/adapters/opencode.mjs +278 -278
  20. package/bin/gsdd.mjs +123 -116
  21. package/bin/lib/cli-utils.mjs +28 -28
  22. package/bin/lib/evidence-contract.mjs +112 -112
  23. package/bin/lib/file-ops.mjs +209 -161
  24. package/bin/lib/health-truth.mjs +181 -178
  25. package/bin/lib/health.mjs +265 -235
  26. package/bin/lib/init-flow.mjs +252 -236
  27. package/bin/lib/init-prompts.mjs +248 -247
  28. package/bin/lib/init-runtime.mjs +225 -212
  29. package/bin/lib/init.mjs +17 -17
  30. package/bin/lib/lifecycle-preflight.mjs +360 -333
  31. package/bin/lib/lifecycle-state.mjs +351 -267
  32. package/bin/lib/manifest.mjs +116 -114
  33. package/bin/lib/models.mjs +411 -411
  34. package/bin/lib/phase.mjs +397 -378
  35. package/bin/lib/plan-constants.mjs +30 -30
  36. package/bin/lib/provenance.mjs +109 -106
  37. package/bin/lib/rendering.mjs +178 -130
  38. package/bin/lib/runtime-freshness.mjs +221 -219
  39. package/bin/lib/templates.mjs +225 -224
  40. package/bin/lib/workspace-root.mjs +132 -0
  41. package/distilled/DESIGN.md +2347 -2327
  42. package/distilled/EVIDENCE-INDEX.md +397 -394
  43. package/distilled/README.md +196 -193
  44. package/distilled/SKILL.md +86 -85
  45. package/distilled/templates/agents.block.md +21 -21
  46. package/distilled/templates/agents.md +6 -6
  47. package/distilled/templates/approach.md +232 -232
  48. package/distilled/templates/auth-matrix.md +78 -78
  49. package/distilled/templates/brownfield-change/CHANGE.md +99 -0
  50. package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
  51. package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
  52. package/distilled/templates/codebase/architecture.md +110 -110
  53. package/distilled/templates/codebase/concerns.md +95 -95
  54. package/distilled/templates/codebase/conventions.md +193 -193
  55. package/distilled/templates/codebase/stack.md +96 -96
  56. package/distilled/templates/delegates/approach-explorer.md +25 -25
  57. package/distilled/templates/delegates/mapper-arch.md +26 -26
  58. package/distilled/templates/delegates/mapper-concerns.md +27 -27
  59. package/distilled/templates/delegates/mapper-quality.md +28 -28
  60. package/distilled/templates/delegates/mapper-tech.md +25 -25
  61. package/distilled/templates/delegates/plan-checker.md +68 -68
  62. package/distilled/templates/delegates/researcher-architecture.md +30 -30
  63. package/distilled/templates/delegates/researcher-features.md +30 -30
  64. package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
  65. package/distilled/templates/delegates/researcher-stack.md +30 -30
  66. package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
  67. package/distilled/templates/research/architecture.md +57 -57
  68. package/distilled/templates/research/features.md +23 -23
  69. package/distilled/templates/research/pitfalls.md +46 -46
  70. package/distilled/templates/research/stack.md +45 -45
  71. package/distilled/templates/research/summary.md +67 -67
  72. package/distilled/templates/roadmap.md +74 -62
  73. package/distilled/templates/spec.md +110 -110
  74. package/distilled/workflows/audit-milestone.md +275 -271
  75. package/distilled/workflows/complete-milestone.md +336 -332
  76. package/distilled/workflows/execute.md +454 -449
  77. package/distilled/workflows/map-codebase.md +253 -253
  78. package/distilled/workflows/new-milestone.md +242 -238
  79. package/distilled/workflows/new-project.md +398 -398
  80. package/distilled/workflows/pause.md +160 -156
  81. package/distilled/workflows/plan-milestone-gaps.md +183 -183
  82. package/distilled/workflows/plan.md +451 -447
  83. package/distilled/workflows/progress.md +227 -223
  84. package/distilled/workflows/quick.md +351 -347
  85. package/distilled/workflows/resume.md +220 -212
  86. package/distilled/workflows/verify-work.md +260 -260
  87. package/distilled/workflows/verify.md +431 -429
  88. package/docs/BROWNFIELD-PROOF.md +95 -95
  89. package/docs/RUNTIME-SUPPORT.md +93 -75
  90. package/docs/USER-GUIDE.md +440 -399
  91. package/docs/VERIFICATION-DISCIPLINE.md +59 -59
  92. package/docs/claude/context-monitor.md +98 -98
  93. package/docs/proof/consumer-node-cli/README.md +37 -37
  94. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
  95. package/docs/proof/consumer-node-cli/SPEC.md +17 -17
  96. package/docs/proof/consumer-node-cli/brief.md +9 -9
  97. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
  98. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
  99. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
  100. package/package.json +62 -61
@@ -2,21 +2,25 @@ import { existsSync, readFileSync, readdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
 
4
4
  const BROWNFIELD_CHANGE_DIR = 'brownfield-change';
5
-
6
- const PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[a-z]?):\s*(.+?)\*\*(?:\s+—\s+\[([^\]]+)])?/i;
7
- const ACTIVE_MILESTONE_HEADING_RE = /^###\s+(v[^\s]+)\s+(.+)$/im;
8
- const MILESTONE_LEDGER_HEADING_RE = /^##\s+(?:✅\s+)?(v[^\s]+)\s*(?:—|-)?\s*(.*)$/i;
9
-
10
- export function evaluateLifecycleState({ planningDir, provenance = null } = {}) {
11
- if (!planningDir) {
12
- throw new Error('planningDir is required');
13
- }
14
-
15
- const specPath = join(planningDir, 'SPEC.md');
16
- const roadmapPath = join(planningDir, 'ROADMAP.md');
17
- const milestonesPath = join(planningDir, 'MILESTONES.md');
18
- const phasesDir = join(planningDir, 'phases');
19
-
5
+
6
+ const PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[a-z]?):\s*(.+?)\*\*(?:\s+—\s+\[([^\]]+)])?/i;
7
+ const PHASE_DETAIL_HEADING_RE = /^(#{3,})\s+Phase\s+(\d+(?:\.\d+)*[a-z]?)(?::|\b)/i;
8
+ const PHASE_DETAIL_STATUS_RE = /^\s*\*\*Status\*\*:\s*\[([ x-])\]/i;
9
+ const ACTIVE_MILESTONE_HEADING_RE = /^###\s+(v[^\s]+)\s+(.+)$/im;
10
+ const MILESTONE_LEDGER_HEADING_RE = /^##\s+(?:✅\s+)?(v[^\s]+)\s*(?:—|-)?\s*(.*)$/i;
11
+ const DETAILS_OPEN_RE = /<details\b/i;
12
+ const DETAILS_CLOSE_RE = /<\/details>/i;
13
+
14
+ export function evaluateLifecycleState({ planningDir, provenance = null } = {}) {
15
+ if (!planningDir) {
16
+ throw new Error('planningDir is required');
17
+ }
18
+
19
+ const specPath = join(planningDir, 'SPEC.md');
20
+ const roadmapPath = join(planningDir, 'ROADMAP.md');
21
+ const milestonesPath = join(planningDir, 'MILESTONES.md');
22
+ const phasesDir = join(planningDir, 'phases');
23
+
20
24
  const spec = readTextIfExists(specPath);
21
25
  const roadmap = readTextIfExists(roadmapPath);
22
26
  const milestones = readTextIfExists(milestonesPath);
@@ -27,76 +31,156 @@ export function evaluateLifecycleState({ planningDir, provenance = null } = {})
27
31
  hasRoadmap: Boolean(roadmap.trim()),
28
32
  brownfieldChange,
29
33
  });
30
-
31
- const phases = parseActiveRoadmapPhases(roadmap);
32
- const phaseArtifacts = collectPhaseArtifacts(phasesDir);
33
- const enrichedPhases = phases.map((phase) => {
34
- const matchingArtifacts = phaseArtifacts.filter((artifact) => artifact.phaseToken === phase.number);
35
- const hasPlan = matchingArtifacts.some((artifact) => artifact.kind === 'plan');
36
- const hasSummary = matchingArtifacts.some((artifact) => artifact.kind === 'summary');
37
- return {
38
- ...phase,
39
- hasArtifacts: matchingArtifacts.length > 0,
40
- hasPlan,
41
- hasSummary,
42
- artifacts: matchingArtifacts,
43
- };
44
- });
45
-
46
- const incompletePlans = phaseArtifacts.filter((artifact) => {
47
- if (artifact.kind !== 'plan') return false;
48
- return !phaseArtifacts.some((candidate) =>
49
- candidate.dir === artifact.dir &&
50
- candidate.baseId === artifact.baseId &&
51
- candidate.kind === 'summary'
52
- );
53
- });
54
-
55
- const counts = {
56
- total: enrichedPhases.length,
57
- completed: enrichedPhases.filter((phase) => phase.status === 'done').length,
58
- inProgress: enrichedPhases.filter((phase) => phase.status === 'in_progress').length,
59
- notStarted: enrichedPhases.filter((phase) => phase.status === 'not_started').length,
60
- };
61
-
62
- const milestoneLedger = parseMilestoneLedger(milestones);
63
- const currentMilestone = deriveCurrentMilestone({
64
- roadmap,
65
- planningDir,
66
- milestoneLedger,
67
- counts,
68
- });
69
-
70
- return {
71
- paths: {
72
- specPath,
73
- roadmapPath,
74
- milestonesPath,
75
- phasesDir,
76
- },
77
- currentMilestone,
78
- phases: enrichedPhases,
79
- currentPhase: enrichedPhases.find((phase) => phase.status === 'in_progress') || null,
80
- nextPhase: enrichedPhases.find((phase) => phase.status === 'not_started') || null,
34
+
35
+ const phases = parseActiveRoadmapPhases(roadmap);
36
+ const phaseStatusAlignment = evaluateRoadmapPhaseStatusAlignment(roadmap);
37
+ const phaseArtifacts = collectPhaseArtifacts(phasesDir);
38
+ const enrichedPhases = phases.map((phase) => {
39
+ const matchingArtifacts = phaseArtifacts.filter((artifact) => artifact.phaseToken === phase.number);
40
+ const hasPlan = matchingArtifacts.some((artifact) => artifact.kind === 'plan');
41
+ const hasSummary = matchingArtifacts.some((artifact) => artifact.kind === 'summary');
42
+ return {
43
+ ...phase,
44
+ hasArtifacts: matchingArtifacts.length > 0,
45
+ hasPlan,
46
+ hasSummary,
47
+ artifacts: matchingArtifacts,
48
+ };
49
+ });
50
+
51
+ const incompletePlans = phaseArtifacts.filter((artifact) => {
52
+ if (artifact.kind !== 'plan') return false;
53
+ return !phaseArtifacts.some((candidate) =>
54
+ candidate.dir === artifact.dir &&
55
+ candidate.baseId === artifact.baseId &&
56
+ candidate.kind === 'summary'
57
+ );
58
+ });
59
+
60
+ const counts = {
61
+ total: enrichedPhases.length,
62
+ completed: enrichedPhases.filter((phase) => phase.status === 'done').length,
63
+ inProgress: enrichedPhases.filter((phase) => phase.status === 'in_progress').length,
64
+ notStarted: enrichedPhases.filter((phase) => phase.status === 'not_started').length,
65
+ };
66
+
67
+ const milestoneLedger = parseMilestoneLedger(milestones);
68
+ const currentMilestone = deriveCurrentMilestone({
69
+ roadmap,
70
+ planningDir,
71
+ milestoneLedger,
72
+ counts,
73
+ });
74
+
75
+ return {
76
+ paths: {
77
+ specPath,
78
+ roadmapPath,
79
+ milestonesPath,
80
+ phasesDir,
81
+ },
82
+ currentMilestone,
83
+ phases: enrichedPhases,
84
+ currentPhase: enrichedPhases.find((phase) => phase.status === 'in_progress') || null,
85
+ nextPhase: enrichedPhases.find((phase) => phase.status === 'not_started') || null,
81
86
  counts,
82
87
  phaseArtifacts,
83
88
  incompletePlans,
84
89
  brownfieldChange,
85
90
  nonPhaseState,
86
- requirementAlignment: evaluateRequirementAlignment(spec, enrichedPhases),
87
- provenance: provenance
88
- ? {
89
- provided: true,
90
- ...provenance,
91
- }
92
- : { provided: false },
93
- };
94
- }
95
-
96
- function readTextIfExists(filePath) {
97
- return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
98
- }
99
-
91
+ phaseStatusAlignment,
92
+ requirementAlignment: evaluateRequirementAlignment(spec, enrichedPhases, phaseStatusAlignment),
93
+ provenance: provenance
94
+ ? {
95
+ provided: true,
96
+ ...provenance,
97
+ }
98
+ : { provided: false },
99
+ };
100
+ }
101
+
102
+ function evaluateRoadmapPhaseStatusAlignment(roadmap) {
103
+ if (!roadmap) return { mismatches: [] };
104
+
105
+ const overview = new Map();
106
+ const details = new Map();
107
+ const lines = normalizeContent(roadmap).split('\n');
108
+ let inDetails = false;
109
+ let currentDetailPhase = null;
110
+
111
+ for (const line of lines) {
112
+ if (DETAILS_OPEN_RE.test(line) && !DETAILS_CLOSE_RE.test(line)) {
113
+ inDetails = true;
114
+ currentDetailPhase = null;
115
+ continue;
116
+ }
117
+ if (DETAILS_CLOSE_RE.test(line)) {
118
+ inDetails = false;
119
+ currentDetailPhase = null;
120
+ continue;
121
+ }
122
+ if (inDetails) continue;
123
+
124
+ const phaseMatch = line.match(PHASE_LINE_RE);
125
+ if (phaseMatch) {
126
+ const phaseToken = normalizePhaseToken(phaseMatch[2]);
127
+ const status = normalizePhaseStatus(phaseMatch[1]);
128
+ if (!overview.has(phaseToken)) overview.set(phaseToken, []);
129
+ overview.get(phaseToken).push(status);
130
+ continue;
131
+ }
132
+
133
+ const headingMatch = line.match(PHASE_DETAIL_HEADING_RE);
134
+ if (headingMatch) {
135
+ currentDetailPhase = normalizePhaseToken(headingMatch[2]);
136
+ if (!details.has(currentDetailPhase)) details.set(currentDetailPhase, []);
137
+ continue;
138
+ }
139
+
140
+ if (/^#+\s+/.test(line)) {
141
+ currentDetailPhase = null;
142
+ continue;
143
+ }
144
+
145
+ const statusMatch = line.match(PHASE_DETAIL_STATUS_RE);
146
+ if (statusMatch && currentDetailPhase) {
147
+ details.get(currentDetailPhase).push(normalizePhaseStatus(statusMatch[1]));
148
+ }
149
+ }
150
+
151
+ const mismatches = [];
152
+ const phaseTokens = new Set([...overview.keys(), ...details.keys()]);
153
+ for (const phaseToken of [...phaseTokens].sort(comparePhaseTokens)) {
154
+ const overviewStatuses = overview.get(phaseToken) || [];
155
+ const detailStatuses = details.get(phaseToken) || [];
156
+ if (overviewStatuses.length > 1) {
157
+ mismatches.push(`Phase ${phaseToken} has multiple overview status entries`);
158
+ }
159
+ if (detailStatuses.length > 1) {
160
+ mismatches.push(`Phase ${phaseToken} has multiple Phase Details status entries`);
161
+ }
162
+ if (overviewStatuses.length === 1 && details.has(phaseToken) && detailStatuses.length === 0) {
163
+ mismatches.push(`Phase ${phaseToken} overview exists but Phase Details status is missing`);
164
+ }
165
+ if (detailStatuses.length > 0 && overviewStatuses.length === 0) {
166
+ mismatches.push(`Phase ${phaseToken} Phase Details status exists but overview entry is missing`);
167
+ }
168
+ if (overviewStatuses.length === 1 && detailStatuses.length === 1 && overviewStatuses[0] !== detailStatuses[0]) {
169
+ mismatches.push(`Phase ${phaseToken} overview status ${overviewStatuses[0]} disagrees with Phase Details status ${detailStatuses[0]}`);
170
+ }
171
+ }
172
+
173
+ return { mismatches };
174
+ }
175
+
176
+ function comparePhaseTokens(a, b) {
177
+ return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
178
+ }
179
+
180
+ function readTextIfExists(filePath) {
181
+ return existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '';
182
+ }
183
+
100
184
  function normalizeContent(content) {
101
185
  return String(content || '').replace(/\r\n/g, '\n');
102
186
  }
@@ -104,52 +188,52 @@ function normalizeContent(content) {
104
188
  function escapeRegExp(value) {
105
189
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
106
190
  }
107
-
108
- export function normalizePhaseToken(value) {
109
- const raw = String(value || '').trim().toLowerCase();
110
- const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
111
- if (!match) return raw;
112
-
113
- const numericSegments = match[1]
114
- .split('.')
115
- .map((segment) => String(parseInt(segment, 10)));
116
- return `${numericSegments.join('.')}${match[2] || ''}`;
117
- }
118
-
119
- function normalizePhaseStatus(marker) {
120
- const normalized = String(marker || '').trim().toLowerCase();
121
- if (normalized === 'x') return 'done';
122
- if (normalized === '-') return 'in_progress';
123
- return 'not_started';
124
- }
125
-
191
+
192
+ export function normalizePhaseToken(value) {
193
+ const raw = String(value || '').trim().toLowerCase();
194
+ const match = raw.match(/^(\d+(?:\.\d+)*)([a-z]?)$/i);
195
+ if (!match) return raw;
196
+
197
+ const numericSegments = match[1]
198
+ .split('.')
199
+ .map((segment) => String(parseInt(segment, 10)));
200
+ return `${numericSegments.join('.')}${match[2] || ''}`;
201
+ }
202
+
203
+ function normalizePhaseStatus(marker) {
204
+ const normalized = String(marker || '').trim().toLowerCase();
205
+ if (normalized === 'x') return 'done';
206
+ if (normalized === '-') return 'in_progress';
207
+ return 'not_started';
208
+ }
209
+
126
210
  function parseActiveRoadmapPhases(roadmap) {
127
211
  if (!roadmap) return [];
128
-
129
- const phases = [];
130
- let inDetails = false;
131
-
132
- for (const line of normalizeContent(roadmap).split('\n')) {
133
- if (line.includes('<details>') && !line.includes('</details>')) {
134
- inDetails = true;
135
- continue;
136
- }
137
- if (line.includes('</details>')) {
138
- inDetails = false;
139
- continue;
140
- }
141
- if (inDetails) continue;
142
-
143
- const match = line.match(PHASE_LINE_RE);
144
- if (!match) continue;
145
-
146
- phases.push({
147
- number: normalizePhaseToken(match[2]),
148
- title: match[3].trim(),
149
- status: normalizePhaseStatus(match[1]),
150
- requirements: splitRequirementList(match[4]),
151
- });
152
- }
212
+
213
+ const phases = [];
214
+ let inDetails = false;
215
+
216
+ for (const line of normalizeContent(roadmap).split('\n')) {
217
+ if (DETAILS_OPEN_RE.test(line) && !DETAILS_CLOSE_RE.test(line)) {
218
+ inDetails = true;
219
+ continue;
220
+ }
221
+ if (DETAILS_CLOSE_RE.test(line)) {
222
+ inDetails = false;
223
+ continue;
224
+ }
225
+ if (inDetails) continue;
226
+
227
+ const match = line.match(PHASE_LINE_RE);
228
+ if (!match) continue;
229
+
230
+ phases.push({
231
+ number: normalizePhaseToken(match[2]),
232
+ title: match[3].trim(),
233
+ status: normalizePhaseStatus(match[1]),
234
+ requirements: splitRequirementList(match[4]),
235
+ });
236
+ }
153
237
 
154
238
  return phases;
155
239
  }
@@ -218,152 +302,152 @@ export function deriveNonPhaseState({ planningDir, hasSpec, hasRoadmap, brownfie
218
302
 
219
303
  function splitRequirementList(rawRequirements = '') {
220
304
  return String(rawRequirements)
221
- .split(',')
222
- .map((entry) => entry.trim())
223
- .filter(Boolean);
224
- }
225
-
226
- function collectPhaseArtifacts(phasesDir) {
227
- if (!existsSync(phasesDir)) return [];
228
-
229
- const artifacts = [];
230
- for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
231
- const entryPath = join(phasesDir, entry.name);
232
- if (entry.isFile()) {
233
- const artifact = classifyPhaseArtifact('', entry.name);
234
- if (artifact) artifacts.push(artifact);
235
- continue;
236
- }
237
-
238
- if (!entry.isDirectory()) continue;
239
-
240
- for (const child of readdirSync(entryPath, { withFileTypes: true })) {
241
- if (!child.isFile()) continue;
242
- const artifact = classifyPhaseArtifact(entry.name, child.name);
243
- if (artifact) artifacts.push(artifact);
244
- }
245
- }
246
-
247
- return artifacts;
248
- }
249
-
250
- function classifyPhaseArtifact(dir, name) {
251
- const baseIdMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?(?:-\d+)?)/i);
252
- const phaseTokenMatch = (dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null)
253
- || name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
254
-
255
- if (!baseIdMatch || !phaseTokenMatch) return null;
256
-
257
- let kind = 'other';
258
- if (name.includes('PLAN')) kind = 'plan';
259
- else if (name.includes('SUMMARY')) kind = 'summary';
260
- else if (name.includes('VERIFICATION')) kind = 'verification';
261
- else if (name.includes('APPROACH')) kind = 'approach';
262
-
263
- return {
264
- dir,
265
- name,
266
- displayPath: dir ? `${dir}/${name}` : name,
267
- baseId: baseIdMatch[1],
268
- phaseToken: normalizePhaseToken(phaseTokenMatch[1]),
269
- kind,
270
- };
271
- }
272
-
273
- function parseMilestoneLedger(content) {
274
- if (!content) return [];
275
-
276
- const entries = [];
277
- let current = null;
278
-
279
- for (const line of normalizeContent(content).split('\n')) {
280
- const headingMatch = line.match(MILESTONE_LEDGER_HEADING_RE);
281
- if (headingMatch) {
282
- if (current) entries.push(current);
283
- current = {
284
- version: headingMatch[1],
285
- title: headingMatch[2].trim(),
286
- lines: [],
287
- };
288
- continue;
289
- }
290
-
291
- if (current) {
292
- current.lines.push(line);
293
- }
294
- }
295
-
296
- if (current) entries.push(current);
297
-
298
- return entries.map((entry) => {
299
- const section = entry.lines.join('\n');
300
- const statusMatch = section.match(/^- Status:\s*(.+)$/im);
301
- const shipped = /(^- Status:\s*shipped$)|(^- Shipped:\s+)/im.test(section) || /^✅/.test(entry.title);
302
- return {
303
- version: entry.version,
304
- title: entry.title.replace(/^✅\s*/, ''),
305
- status: statusMatch ? statusMatch[1].trim().toLowerCase() : shipped ? 'shipped' : 'unknown',
306
- shipped,
307
- };
308
- });
309
- }
310
-
311
- function deriveCurrentMilestone({ roadmap, planningDir, milestoneLedger, counts }) {
312
- const normalizedRoadmap = normalizeContent(roadmap);
313
- const headingMatch = normalizedRoadmap.match(ACTIVE_MILESTONE_HEADING_RE);
314
- if (!headingMatch) {
315
- return {
316
- version: null,
317
- title: null,
318
- shippedInLedger: false,
319
- hasMatchingAudit: false,
320
- archiveState: 'unknown',
321
- counts,
322
- };
323
- }
324
-
325
- const version = headingMatch[1];
326
- const title = headingMatch[2].trim();
327
- const ledgerEntry = milestoneLedger.find((entry) => entry.version === version) || null;
328
- const auditPath = join(planningDir, `${version}-MILESTONE-AUDIT.md`);
329
- const hasMatchingAudit = existsSync(auditPath);
330
- const shippedInLedger = Boolean(ledgerEntry?.shipped);
331
-
332
- return {
333
- version,
334
- title,
335
- shippedInLedger,
336
- hasMatchingAudit,
337
- archiveState: shippedInLedger && hasMatchingAudit ? 'archived' : 'active',
338
- counts,
339
- };
340
- }
341
-
342
- function evaluateRequirementAlignment(spec, phases) {
305
+ .split(',')
306
+ .map((entry) => entry.trim())
307
+ .filter(Boolean);
308
+ }
309
+
310
+ function collectPhaseArtifacts(phasesDir) {
311
+ if (!existsSync(phasesDir)) return [];
312
+
313
+ const artifacts = [];
314
+ for (const entry of readdirSync(phasesDir, { withFileTypes: true })) {
315
+ const entryPath = join(phasesDir, entry.name);
316
+ if (entry.isFile()) {
317
+ const artifact = classifyPhaseArtifact('', entry.name);
318
+ if (artifact) artifacts.push(artifact);
319
+ continue;
320
+ }
321
+
322
+ if (!entry.isDirectory()) continue;
323
+
324
+ for (const child of readdirSync(entryPath, { withFileTypes: true })) {
325
+ if (!child.isFile()) continue;
326
+ const artifact = classifyPhaseArtifact(entry.name, child.name);
327
+ if (artifact) artifacts.push(artifact);
328
+ }
329
+ }
330
+
331
+ return artifacts;
332
+ }
333
+
334
+ function classifyPhaseArtifact(dir, name) {
335
+ const baseIdMatch = name.match(/^(\d+(?:\.\d+)*[a-z]?(?:-\d+)?)/i);
336
+ const phaseTokenMatch = (dir ? dir.match(/^(\d+(?:\.\d+)*[a-z]?)-/i) : null)
337
+ || name.match(/^(\d+(?:\.\d+)*[a-z]?)/i);
338
+
339
+ if (!baseIdMatch || !phaseTokenMatch) return null;
340
+
341
+ let kind = 'other';
342
+ if (name.includes('PLAN')) kind = 'plan';
343
+ else if (name.includes('SUMMARY')) kind = 'summary';
344
+ else if (name.includes('VERIFICATION')) kind = 'verification';
345
+ else if (name.includes('APPROACH')) kind = 'approach';
346
+
347
+ return {
348
+ dir,
349
+ name,
350
+ displayPath: dir ? `${dir}/${name}` : name,
351
+ baseId: baseIdMatch[1],
352
+ phaseToken: normalizePhaseToken(phaseTokenMatch[1]),
353
+ kind,
354
+ };
355
+ }
356
+
357
+ function parseMilestoneLedger(content) {
358
+ if (!content) return [];
359
+
360
+ const entries = [];
361
+ let current = null;
362
+
363
+ for (const line of normalizeContent(content).split('\n')) {
364
+ const headingMatch = line.match(MILESTONE_LEDGER_HEADING_RE);
365
+ if (headingMatch) {
366
+ if (current) entries.push(current);
367
+ current = {
368
+ version: headingMatch[1],
369
+ title: headingMatch[2].trim(),
370
+ lines: [],
371
+ };
372
+ continue;
373
+ }
374
+
375
+ if (current) {
376
+ current.lines.push(line);
377
+ }
378
+ }
379
+
380
+ if (current) entries.push(current);
381
+
382
+ return entries.map((entry) => {
383
+ const section = entry.lines.join('\n');
384
+ const statusMatch = section.match(/^- Status:\s*(.+)$/im);
385
+ const shipped = /(^- Status:\s*shipped$)|(^- Shipped:\s+)/im.test(section) || /^✅/.test(entry.title);
386
+ return {
387
+ version: entry.version,
388
+ title: entry.title.replace(/^✅\s*/, ''),
389
+ status: statusMatch ? statusMatch[1].trim().toLowerCase() : shipped ? 'shipped' : 'unknown',
390
+ shipped,
391
+ };
392
+ });
393
+ }
394
+
395
+ function deriveCurrentMilestone({ roadmap, planningDir, milestoneLedger, counts }) {
396
+ const normalizedRoadmap = normalizeContent(roadmap);
397
+ const headingMatch = normalizedRoadmap.match(ACTIVE_MILESTONE_HEADING_RE);
398
+ if (!headingMatch) {
399
+ return {
400
+ version: null,
401
+ title: null,
402
+ shippedInLedger: false,
403
+ hasMatchingAudit: false,
404
+ archiveState: 'unknown',
405
+ counts,
406
+ };
407
+ }
408
+
409
+ const version = headingMatch[1];
410
+ const title = headingMatch[2].trim();
411
+ const ledgerEntry = milestoneLedger.find((entry) => entry.version === version) || null;
412
+ const auditPath = join(planningDir, `${version}-MILESTONE-AUDIT.md`);
413
+ const hasMatchingAudit = existsSync(auditPath);
414
+ const shippedInLedger = Boolean(ledgerEntry?.shipped);
415
+
416
+ return {
417
+ version,
418
+ title,
419
+ shippedInLedger,
420
+ hasMatchingAudit,
421
+ archiveState: shippedInLedger && hasMatchingAudit ? 'archived' : 'active',
422
+ counts,
423
+ };
424
+ }
425
+
426
+ function evaluateRequirementAlignment(spec, phases, phaseStatusAlignment = { mismatches: [] }) {
343
427
  const checkedRequirements = new Set(
344
428
  [...normalizeContent(spec).matchAll(/- \[x\] \*\*\[([A-Z0-9-]+)]\*\*/g)].map((result) => result[1])
345
429
  );
346
-
347
- const roadmapRequirements = new Map();
348
- for (const phase of phases) {
349
- for (const requirementId of phase.requirements) {
350
- roadmapRequirements.set(requirementId, phase.status === 'done');
351
- }
352
- }
353
-
354
- const mismatches = [];
355
- for (const [requirementId, roadmapStatus] of roadmapRequirements.entries()) {
356
- const specChecked = checkedRequirements.has(requirementId);
357
- if (roadmapStatus && !specChecked) {
358
- mismatches.push(`${requirementId} phase complete but SPEC unchecked`);
359
- }
360
- if (!roadmapStatus && specChecked) {
361
- mismatches.push(`${requirementId} SPEC checked but phase incomplete`);
362
- }
363
- }
364
-
365
- return {
366
- checkedRequirements,
430
+
431
+ const roadmapRequirements = new Map();
432
+ for (const phase of phases) {
433
+ for (const requirementId of phase.requirements) {
434
+ roadmapRequirements.set(requirementId, phase.status === 'done');
435
+ }
436
+ }
437
+
438
+ const mismatches = [];
439
+ for (const [requirementId, roadmapStatus] of roadmapRequirements.entries()) {
440
+ const specChecked = checkedRequirements.has(requirementId);
441
+ if (roadmapStatus && !specChecked) {
442
+ mismatches.push(`${requirementId} phase complete but SPEC unchecked`);
443
+ }
444
+ if (!roadmapStatus && specChecked) {
445
+ mismatches.push(`${requirementId} SPEC checked but phase incomplete`);
446
+ }
447
+ }
448
+
449
+ return {
450
+ checkedRequirements,
367
451
  roadmapRequirements,
368
452
  mismatches,
369
453
  };