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.
- package/LICENSE +21 -21
- package/README.md +625 -607
- package/agents/DISTILLATION.md +421 -421
- package/agents/README.md +62 -62
- package/agents/approach-explorer.md +361 -361
- package/agents/debugger.md +82 -82
- package/agents/executor.md +394 -394
- package/agents/integration-checker.md +318 -318
- package/agents/mapper.md +103 -103
- package/agents/planner.md +313 -313
- package/agents/researcher.md +84 -84
- package/agents/roadmapper.md +296 -296
- package/agents/synthesizer.md +236 -236
- package/agents/verifier.md +337 -337
- package/bin/adapters/agents.mjs +34 -34
- package/bin/adapters/claude.mjs +191 -191
- package/bin/adapters/codex.mjs +85 -85
- package/bin/adapters/index.mjs +20 -20
- package/bin/adapters/opencode.mjs +278 -278
- package/bin/gsdd.mjs +123 -116
- package/bin/lib/cli-utils.mjs +28 -28
- package/bin/lib/evidence-contract.mjs +112 -112
- package/bin/lib/file-ops.mjs +209 -161
- package/bin/lib/health-truth.mjs +181 -178
- package/bin/lib/health.mjs +265 -235
- package/bin/lib/init-flow.mjs +252 -236
- package/bin/lib/init-prompts.mjs +248 -247
- package/bin/lib/init-runtime.mjs +225 -212
- package/bin/lib/init.mjs +17 -17
- package/bin/lib/lifecycle-preflight.mjs +360 -333
- package/bin/lib/lifecycle-state.mjs +351 -267
- package/bin/lib/manifest.mjs +116 -114
- package/bin/lib/models.mjs +411 -411
- package/bin/lib/phase.mjs +397 -378
- package/bin/lib/plan-constants.mjs +30 -30
- package/bin/lib/provenance.mjs +109 -106
- package/bin/lib/rendering.mjs +178 -130
- package/bin/lib/runtime-freshness.mjs +221 -219
- package/bin/lib/templates.mjs +225 -224
- package/bin/lib/workspace-root.mjs +132 -0
- package/distilled/DESIGN.md +2347 -2327
- package/distilled/EVIDENCE-INDEX.md +397 -394
- package/distilled/README.md +196 -193
- package/distilled/SKILL.md +86 -85
- package/distilled/templates/agents.block.md +21 -21
- package/distilled/templates/agents.md +6 -6
- package/distilled/templates/approach.md +232 -232
- package/distilled/templates/auth-matrix.md +78 -78
- package/distilled/templates/brownfield-change/CHANGE.md +99 -0
- package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
- package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
- package/distilled/templates/codebase/architecture.md +110 -110
- package/distilled/templates/codebase/concerns.md +95 -95
- package/distilled/templates/codebase/conventions.md +193 -193
- package/distilled/templates/codebase/stack.md +96 -96
- package/distilled/templates/delegates/approach-explorer.md +25 -25
- package/distilled/templates/delegates/mapper-arch.md +26 -26
- package/distilled/templates/delegates/mapper-concerns.md +27 -27
- package/distilled/templates/delegates/mapper-quality.md +28 -28
- package/distilled/templates/delegates/mapper-tech.md +25 -25
- package/distilled/templates/delegates/plan-checker.md +68 -68
- package/distilled/templates/delegates/researcher-architecture.md +30 -30
- package/distilled/templates/delegates/researcher-features.md +30 -30
- package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
- package/distilled/templates/delegates/researcher-stack.md +30 -30
- package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
- package/distilled/templates/research/architecture.md +57 -57
- package/distilled/templates/research/features.md +23 -23
- package/distilled/templates/research/pitfalls.md +46 -46
- package/distilled/templates/research/stack.md +45 -45
- package/distilled/templates/research/summary.md +67 -67
- package/distilled/templates/roadmap.md +74 -62
- package/distilled/templates/spec.md +110 -110
- package/distilled/workflows/audit-milestone.md +275 -271
- package/distilled/workflows/complete-milestone.md +336 -332
- package/distilled/workflows/execute.md +454 -449
- package/distilled/workflows/map-codebase.md +253 -253
- package/distilled/workflows/new-milestone.md +242 -238
- package/distilled/workflows/new-project.md +398 -398
- package/distilled/workflows/pause.md +160 -156
- package/distilled/workflows/plan-milestone-gaps.md +183 -183
- package/distilled/workflows/plan.md +451 -447
- package/distilled/workflows/progress.md +227 -223
- package/distilled/workflows/quick.md +351 -347
- package/distilled/workflows/resume.md +220 -212
- package/distilled/workflows/verify-work.md +260 -260
- package/distilled/workflows/verify.md +431 -429
- package/docs/BROWNFIELD-PROOF.md +95 -95
- package/docs/RUNTIME-SUPPORT.md +93 -75
- package/docs/USER-GUIDE.md +440 -399
- package/docs/VERIFICATION-DISCIPLINE.md +59 -59
- package/docs/claude/context-monitor.md +98 -98
- package/docs/proof/consumer-node-cli/README.md +37 -37
- package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
- package/docs/proof/consumer-node-cli/SPEC.md +17 -17
- package/docs/proof/consumer-node-cli/brief.md +9 -9
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
- package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
- 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
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
candidate.
|
|
51
|
-
candidate.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 (
|
|
134
|
-
inDetails = true;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (
|
|
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
|
};
|