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