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.
- package/LICENSE +21 -21
- package/README.md +610 -608
- package/agents/DISTILLATION.md +421 -421
- package/agents/README.md +62 -62
- package/agents/approach-explorer.md +370 -361
- package/agents/debugger.md +82 -82
- package/agents/executor.md +473 -394
- package/agents/integration-checker.md +318 -318
- package/agents/mapper.md +103 -103
- package/agents/planner.md +342 -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 +193 -191
- package/bin/adapters/codex.mjs +85 -85
- package/bin/adapters/index.mjs +20 -20
- package/bin/adapters/opencode.mjs +280 -278
- package/bin/gsdd.mjs +123 -116
- package/bin/lib/cli-utils.mjs +28 -28
- package/bin/lib/evidence-contract.mjs +325 -112
- package/bin/lib/file-ops.mjs +186 -144
- package/bin/lib/health-truth.mjs +196 -178
- package/bin/lib/health.mjs +246 -226
- package/bin/lib/init-flow.mjs +247 -231
- package/bin/lib/init-prompts.mjs +248 -247
- package/bin/lib/init-runtime.mjs +193 -190
- package/bin/lib/init.mjs +17 -17
- package/bin/lib/lifecycle-preflight.mjs +760 -326
- package/bin/lib/lifecycle-state.mjs +356 -267
- package/bin/lib/manifest.mjs +116 -114
- package/bin/lib/models.mjs +411 -411
- package/bin/lib/phase.mjs +365 -358
- package/bin/lib/plan-constants.mjs +35 -30
- package/bin/lib/provenance.mjs +109 -106
- package/bin/lib/rendering.mjs +119 -83
- package/bin/lib/runtime-freshness.mjs +214 -214
- package/bin/lib/session-fingerprint.mjs +91 -14
- package/bin/lib/templates.mjs +225 -224
- package/bin/lib/workspace-root.mjs +2 -1
- package/distilled/DESIGN.md +2461 -2323
- package/distilled/EVIDENCE-INDEX.md +418 -392
- 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 +272 -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 +28 -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 +78 -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 +303 -271
- package/distilled/workflows/complete-milestone.md +349 -332
- package/distilled/workflows/execute.md +457 -450
- 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 +454 -448
- 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 +80 -69
- package/docs/USER-GUIDE.md +394 -386
- 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,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
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
hasSummary,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
candidate.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 (
|
|
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
|
-
}
|
|
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
|
|
259
|
-
else if (name
|
|
260
|
-
else if (name
|
|
261
|
-
else if (name
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
};
|