gsdd-cli 0.26.0 → 0.28.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/README.md +55 -23
- package/agents/DISTILLATION.md +2 -2
- package/agents/README.md +4 -4
- package/agents/approach-explorer.md +4 -4
- package/agents/executor.md +14 -14
- package/agents/integration-checker.md +2 -2
- package/agents/researcher.md +2 -2
- package/agents/roadmapper.md +6 -6
- package/agents/synthesizer.md +18 -18
- package/agents/verifier.md +2 -2
- package/bin/adapters/agents.mjs +3 -3
- package/bin/adapters/claude.mjs +16 -14
- package/bin/adapters/opencode.mjs +15 -12
- package/bin/gsdd.mjs +16 -13
- package/bin/lib/{models.mjs → config.mjs} +23 -16
- package/bin/lib/control-map.mjs +17 -488
- package/bin/lib/global-install.mjs +16 -3
- package/bin/lib/health-truth.mjs +8 -13
- package/bin/lib/health.mjs +25 -39
- package/bin/lib/init-flow.mjs +44 -38
- package/bin/lib/init-prompts.mjs +3 -3
- package/bin/lib/init-runtime.mjs +17 -33
- package/bin/lib/lifecycle-preflight.mjs +297 -404
- package/bin/lib/lifecycle-state.mjs +2 -1
- package/bin/lib/next.mjs +243 -20
- package/bin/lib/phase.mjs +10 -315
- package/bin/lib/rendering.mjs +64 -44
- package/bin/lib/runtime-freshness.mjs +18 -15
- package/bin/lib/state-dir.mjs +45 -0
- package/bin/lib/templates.mjs +59 -22
- package/bin/lib/work-context.mjs +12 -1
- package/bin/lib/workflows.mjs +0 -1
- package/bin/lib/workspace-root.mjs +11 -6
- package/distilled/DESIGN.md +58 -2
- package/distilled/EVIDENCE-INDEX.md +15 -2
- package/distilled/README.md +23 -33
- package/distilled/SKILL.md +9 -10
- package/distilled/templates/agents.block.md +5 -5
- package/distilled/templates/approach.md +3 -3
- package/distilled/templates/auth-matrix.md +2 -2
- package/distilled/templates/brownfield-change/CHANGE.md +1 -1
- package/distilled/templates/delegates/approach-explorer.md +5 -5
- package/distilled/templates/delegates/mapper-arch.md +3 -3
- package/distilled/templates/delegates/mapper-concerns.md +4 -4
- package/distilled/templates/delegates/mapper-quality.md +3 -3
- package/distilled/templates/delegates/mapper-tech.md +3 -3
- package/distilled/templates/delegates/plan-checker.md +5 -5
- package/distilled/templates/delegates/researcher-architecture.md +3 -3
- package/distilled/templates/delegates/researcher-features.md +3 -3
- package/distilled/templates/delegates/researcher-pitfalls.md +3 -3
- package/distilled/templates/delegates/researcher-stack.md +3 -3
- package/distilled/templates/delegates/researcher-synthesizer.md +7 -7
- package/distilled/templates/research/architecture.md +1 -1
- package/distilled/templates/research/pitfalls.md +1 -1
- package/distilled/templates/research/stack.md +1 -1
- package/distilled/templates/roadmap.md +4 -4
- package/distilled/templates/spec.md +2 -2
- package/distilled/workflows/audit-milestone.md +22 -22
- package/distilled/workflows/complete-milestone.md +35 -35
- package/distilled/workflows/execute.md +29 -29
- package/distilled/workflows/map-codebase.md +30 -30
- package/distilled/workflows/new-milestone.md +18 -18
- package/distilled/workflows/new-project.md +45 -45
- package/distilled/workflows/pause.md +15 -15
- package/distilled/workflows/plan.md +63 -63
- package/distilled/workflows/progress.md +40 -39
- package/distilled/workflows/quick.md +43 -43
- package/distilled/workflows/resume.md +23 -22
- package/distilled/workflows/verify-work.md +7 -7
- package/distilled/workflows/verify.md +20 -20
- package/docs/BROWNFIELD-PROOF.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +16 -16
- package/docs/USER-GUIDE.md +21 -21
- package/docs/claude/context-monitor.md +1 -1
- package/docs/proof/consumer-node-cli/README.md +1 -1
- package/package.json +3 -3
- package/bin/lib/closeout-report.mjs +0 -318
- package/bin/lib/evidence-contract.mjs +0 -325
- package/bin/lib/provenance.mjs +0 -390
- package/bin/lib/session-fingerprint.mjs +0 -223
- package/bin/lib/ui-proof.mjs +0 -1007
- package/distilled/workflows/plan-milestone-gaps.md +0 -204
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'fs';
|
|
2
|
-
import { join, resolve } from 'path';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from 'path';
|
|
3
3
|
import { output } from './cli-utils.mjs';
|
|
4
4
|
import { buildControlMap } from './control-map.mjs';
|
|
5
|
-
import {
|
|
6
|
-
DELIVERY_POSTURES,
|
|
7
|
-
EVIDENCE_KINDS,
|
|
8
|
-
RELEASE_CLAIM_POSTURES,
|
|
9
|
-
describeEvidenceSurface,
|
|
10
|
-
evaluateReleaseClaimCloseoutContract,
|
|
11
|
-
getEvidenceContract,
|
|
12
|
-
} from './evidence-contract.mjs';
|
|
13
5
|
import { evaluateLifecycleState, normalizePhaseToken } from './lifecycle-state.mjs';
|
|
14
|
-
import { checkDrift } from './session-fingerprint.mjs';
|
|
15
6
|
import { resolveWorkspaceContext } from './workspace-root.mjs';
|
|
16
7
|
|
|
17
8
|
const SURFACE_POLICIES = {
|
|
@@ -53,11 +44,6 @@ const SURFACE_POLICIES = {
|
|
|
53
44
|
ownedWrites: ['spec', 'roadmap', 'phase-directories'],
|
|
54
45
|
explicitLifecycleMutation: 'none',
|
|
55
46
|
},
|
|
56
|
-
'plan-milestone-gaps': {
|
|
57
|
-
classification: 'owned_write',
|
|
58
|
-
ownedWrites: ['roadmap', 'phase-directories'],
|
|
59
|
-
explicitLifecycleMutation: 'none',
|
|
60
|
-
},
|
|
61
47
|
resume: {
|
|
62
48
|
classification: 'owned_write',
|
|
63
49
|
ownedWrites: ['checkpoint-cleanup'],
|
|
@@ -65,17 +51,7 @@ const SURFACE_POLICIES = {
|
|
|
65
51
|
},
|
|
66
52
|
};
|
|
67
53
|
|
|
68
|
-
const
|
|
69
|
-
'evidence',
|
|
70
|
-
'public_surface',
|
|
71
|
-
'runtime',
|
|
72
|
-
'delivery',
|
|
73
|
-
'planning_drift',
|
|
74
|
-
'generated_surface',
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
const RELEASE_CONTRADICTION_STATUSES = Object.freeze(['passed', 'failed', 'not_applicable']);
|
|
78
|
-
const PREFLIGHT_CONTROL_MAP_SKIP_CODES = Object.freeze(['planning_state_drift']);
|
|
54
|
+
const WORK_PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[A-Za-z]?):\s*(.+?)\*\*/i;
|
|
79
55
|
|
|
80
56
|
export function evaluateLifecyclePreflight({
|
|
81
57
|
planningDir,
|
|
@@ -95,13 +71,25 @@ export function evaluateLifecyclePreflight({
|
|
|
95
71
|
|
|
96
72
|
const lifecycle = evaluateLifecycleState({ planningDir });
|
|
97
73
|
const normalizedPhase = phaseNumber ? normalizePhaseToken(phaseNumber) : null;
|
|
74
|
+
const usesBrownfieldAuthority = surface === 'plan' && normalizedPhase === 'brownfield-change';
|
|
75
|
+
const usesPlanAmendAuthority = surface === 'plan' && normalizedPhase === 'amend';
|
|
76
|
+
const workMilestone = normalizedPhase ? evaluateWorkMilestoneState({ planningDir, phaseToken: normalizedPhase }) : null;
|
|
98
77
|
const checkpointPath = join(planningDir, '.continue-here.md');
|
|
78
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
79
|
+
const resumeWorkCheckpoint = surface === 'resume'
|
|
80
|
+
? evaluateResumeWorkCheckpoint({ planningDir, checkpointPath })
|
|
81
|
+
: null;
|
|
82
|
+
const usesWorkAuthority = Boolean(workMilestone?.phaseEntry || resumeWorkCheckpoint);
|
|
83
|
+
const usesAlternateAuthority = usesWorkAuthority || usesBrownfieldAuthority || usesPlanAmendAuthority;
|
|
84
|
+
const ownedWrites = usesPlanAmendAuthority
|
|
85
|
+
? [...policy.ownedWrites, 'roadmap', 'phase-directories']
|
|
86
|
+
: policy.ownedWrites;
|
|
99
87
|
const specPath = join(planningDir, 'SPEC.md');
|
|
100
88
|
const milestonesPath = join(planningDir, 'MILESTONES.md');
|
|
101
89
|
const blockers = [];
|
|
102
90
|
|
|
103
91
|
if (!existsSync(planningDir)) {
|
|
104
|
-
blockers.push(blocker('missing_planning_dir', '.
|
|
92
|
+
blockers.push(blocker('missing_planning_dir', `${stateLabel('.')} does not exist yet.`, [stateLabel('.')]));
|
|
105
93
|
}
|
|
106
94
|
|
|
107
95
|
if (expectsMutation !== 'none' && expectsMutation !== policy.explicitLifecycleMutation) {
|
|
@@ -118,74 +106,72 @@ export function evaluateLifecyclePreflight({
|
|
|
118
106
|
blockers.push(blocker('missing_phase_argument', `${surface} requires an explicit phase number.`, []));
|
|
119
107
|
}
|
|
120
108
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
109
|
+
if (usesBrownfieldAuthority) {
|
|
110
|
+
if (!lifecycle.brownfieldChange?.exists) {
|
|
111
|
+
blockers.push(
|
|
112
|
+
blocker(
|
|
113
|
+
'missing_brownfield_change',
|
|
114
|
+
`Brownfield-change planning requires an active ${stateLabel('brownfield-change', 'CHANGE.md')} continuity anchor.`,
|
|
115
|
+
[stateLabel('brownfield-change', 'CHANGE.md')]
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
} else if (String(lifecycle.brownfieldChange.currentStatus || '').toLowerCase() === 'closed') {
|
|
119
|
+
blockers.push(
|
|
120
|
+
blocker(
|
|
121
|
+
'brownfield_change_closed',
|
|
122
|
+
'Brownfield-change planning cannot continue because CHANGE.md marks the change closed.',
|
|
123
|
+
[stateLabel('brownfield-change', 'CHANGE.md')]
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (normalizedPhase && !usesBrownfieldAuthority && !usesPlanAmendAuthority) {
|
|
130
|
+
blockers.push(
|
|
131
|
+
...(usesWorkAuthority
|
|
132
|
+
? buildWorkPhaseBlockers({ workMilestone, phaseToken: normalizedPhase, surface })
|
|
133
|
+
: buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface, stateLabel }))
|
|
134
|
+
);
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
if (surface === 'audit-milestone') {
|
|
126
|
-
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
|
|
127
|
-
blockers.push(...buildAuditBlockers(lifecycle));
|
|
138
|
+
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
|
|
139
|
+
blockers.push(...buildAuditBlockers(lifecycle, { stateLabel }));
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
if (surface === 'complete-milestone') {
|
|
131
|
-
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
|
|
132
|
-
blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true }));
|
|
143
|
+
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
|
|
144
|
+
blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true, stateLabel }));
|
|
133
145
|
blockers.push(...buildCompletionBlockers(planningDir, lifecycle));
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
if (surface === 'new-milestone') {
|
|
137
|
-
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
|
|
149
|
+
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
|
|
138
150
|
if (!existsSync(specPath)) {
|
|
139
|
-
blockers.push(blocker('missing_spec', 'SPEC.md is required before starting a new milestone.', ['
|
|
151
|
+
blockers.push(blocker('missing_spec', 'SPEC.md is required before starting a new milestone.', [stateLabel('SPEC.md')]));
|
|
140
152
|
}
|
|
141
153
|
if (!existsSync(milestonesPath)) {
|
|
142
|
-
blockers.push(blocker('missing_milestones', 'MILESTONES.md is required before starting a new milestone.', ['
|
|
154
|
+
blockers.push(blocker('missing_milestones', 'MILESTONES.md is required before starting a new milestone.', [stateLabel('MILESTONES.md')]));
|
|
143
155
|
}
|
|
144
156
|
if (lifecycle.currentMilestone.version && lifecycle.currentMilestone.archiveState !== 'archived') {
|
|
145
157
|
blockers.push(
|
|
146
158
|
blocker(
|
|
147
159
|
'active_milestone_in_progress',
|
|
148
160
|
`Milestone ${lifecycle.currentMilestone.version} is still active. Archive or remove the active roadmap before starting the next milestone.`,
|
|
149
|
-
['
|
|
161
|
+
[stateLabel('ROADMAP.md')]
|
|
150
162
|
)
|
|
151
163
|
);
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
if (surface === 'resume' && !existsSync(checkpointPath) && lifecycle.nonPhaseState !== 'active_brownfield_change') {
|
|
156
|
-
|
|
168
|
+
const checkpointLabel = stateLabel('.continue-here.md');
|
|
169
|
+
const brownfieldLabel = stateLabel('brownfield-change', 'CHANGE.md');
|
|
170
|
+
blockers.push(blocker('missing_checkpoint', `resume requires ${checkpointLabel} unless an active ${brownfieldLabel} continuity anchor exists.`, [checkpointLabel, brownfieldLabel]));
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
const warnings = [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (existsSync(planningDir)) {
|
|
163
|
-
const drift = checkDrift(planningDir);
|
|
164
|
-
planningState = {
|
|
165
|
-
classification: drift.classification,
|
|
166
|
-
drifted: drift.drifted,
|
|
167
|
-
noBaseline: drift.noBaseline,
|
|
168
|
-
details: drift.details,
|
|
169
|
-
files: drift.files,
|
|
170
|
-
};
|
|
171
|
-
if (drift.drifted) {
|
|
172
|
-
const driftNotice = {
|
|
173
|
-
code: 'planning_state_drift',
|
|
174
|
-
message: `${surface} cannot proceed because planning state drifted since the last recorded session: ${drift.details.join('; ')}`,
|
|
175
|
-
artifacts: ['.planning/ROADMAP.md', '.planning/SPEC.md', '.planning/config.json'],
|
|
176
|
-
details: drift.details,
|
|
177
|
-
files: drift.files,
|
|
178
|
-
};
|
|
179
|
-
if (policy.classification === 'owned_write') {
|
|
180
|
-
blockers.push(driftNotice);
|
|
181
|
-
} else {
|
|
182
|
-
warnings.push({
|
|
183
|
-
...driftNotice,
|
|
184
|
-
message: `Planning state has drifted since the last recorded session: ${drift.details.join('; ')}`,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
174
|
+
const planningState = null;
|
|
189
175
|
|
|
190
176
|
const controlMap = buildPreflightControlMap({
|
|
191
177
|
planningDir,
|
|
@@ -198,11 +184,11 @@ export function evaluateLifecyclePreflight({
|
|
|
198
184
|
else warnings.push(notice);
|
|
199
185
|
}
|
|
200
186
|
|
|
201
|
-
if (lifecycle.phaseStatusAlignment.mismatches.length > 0) {
|
|
187
|
+
if (!usesAlternateAuthority && lifecycle.phaseStatusAlignment.mismatches.length > 0) {
|
|
202
188
|
warnings.push({
|
|
203
189
|
code: 'roadmap_phase_status_mismatch',
|
|
204
190
|
message: `ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
|
|
205
|
-
artifacts: ['
|
|
191
|
+
artifacts: [stateLabel('ROADMAP.md')],
|
|
206
192
|
});
|
|
207
193
|
}
|
|
208
194
|
|
|
@@ -210,10 +196,10 @@ export function evaluateLifecyclePreflight({
|
|
|
210
196
|
surface,
|
|
211
197
|
phase: normalizedPhase,
|
|
212
198
|
classification: policy.classification,
|
|
213
|
-
ownedWrites
|
|
199
|
+
ownedWrites,
|
|
214
200
|
explicitLifecycleMutation: policy.explicitLifecycleMutation,
|
|
215
|
-
closureEvidence: describeEvidenceSurface(surface),
|
|
216
201
|
mutationRequest: expectsMutation,
|
|
202
|
+
authority: usesPlanAmendAuthority ? 'plan_amend' : usesBrownfieldAuthority ? 'brownfield_change' : usesWorkAuthority ? 'work_milestone' : 'planning',
|
|
217
203
|
allowed: blockers.length === 0,
|
|
218
204
|
status: blockers.length === 0 ? 'allowed' : 'blocked',
|
|
219
205
|
reason: blockers[0]?.code ?? null,
|
|
@@ -222,10 +208,34 @@ export function evaluateLifecyclePreflight({
|
|
|
222
208
|
planningState,
|
|
223
209
|
controlMap: controlMap.summary,
|
|
224
210
|
lifecycle: {
|
|
211
|
+
authority: usesPlanAmendAuthority ? 'plan_amend' : usesBrownfieldAuthority ? 'brownfield_change' : usesWorkAuthority ? 'work_milestone' : 'planning',
|
|
225
212
|
currentMilestone: lifecycle.currentMilestone,
|
|
226
213
|
currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
|
|
227
214
|
nextPhase: lifecycle.nextPhase ? lifecycle.nextPhase.number : null,
|
|
228
215
|
counts: lifecycle.counts,
|
|
216
|
+
workMilestone: usesWorkAuthority
|
|
217
|
+
? {
|
|
218
|
+
phase: workMilestone?.phaseEntry?.number ?? null,
|
|
219
|
+
status: workMilestone?.phaseEntry?.status ?? null,
|
|
220
|
+
roadmapPath: '.work/milestone/ROADMAP.md',
|
|
221
|
+
milestoneDir: '.work/milestone',
|
|
222
|
+
source: resumeWorkCheckpoint ? 'checkpoint' : 'phase',
|
|
223
|
+
}
|
|
224
|
+
: null,
|
|
225
|
+
brownfieldChange: usesBrownfieldAuthority
|
|
226
|
+
? {
|
|
227
|
+
path: stateLabel('brownfield-change', 'CHANGE.md'),
|
|
228
|
+
status: lifecycle.brownfieldChange.currentStatus,
|
|
229
|
+
title: lifecycle.brownfieldChange.title,
|
|
230
|
+
nextAction: lifecycle.brownfieldChange.nextAction,
|
|
231
|
+
}
|
|
232
|
+
: null,
|
|
233
|
+
planAmend: usesPlanAmendAuthority
|
|
234
|
+
? {
|
|
235
|
+
target: 'amend',
|
|
236
|
+
path: stateLabel('ROADMAP.md'),
|
|
237
|
+
}
|
|
238
|
+
: null,
|
|
229
239
|
},
|
|
230
240
|
};
|
|
231
241
|
}
|
|
@@ -242,11 +252,11 @@ function buildPreflightControlMap({ planningDir, policy, existingBlockerCodes, c
|
|
|
242
252
|
planningDir,
|
|
243
253
|
});
|
|
244
254
|
const risks = (map.risks || []).filter((risk) => (
|
|
245
|
-
!
|
|
246
|
-
&& !(existingBlockerCodes.has(risk.code) && risk.severity !== 'block')
|
|
255
|
+
!(existingBlockerCodes.has(risk.code) && risk.severity !== 'block')
|
|
247
256
|
));
|
|
257
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
248
258
|
const notices = risks.map((risk) => ({
|
|
249
|
-
...controlMapNotice(risk),
|
|
259
|
+
...controlMapNotice(risk, stateLabel),
|
|
250
260
|
severity: risk.severity || 'info',
|
|
251
261
|
}));
|
|
252
262
|
|
|
@@ -262,17 +272,17 @@ function buildPreflightControlMap({ planningDir, policy, existingBlockerCodes, c
|
|
|
262
272
|
};
|
|
263
273
|
}
|
|
264
274
|
|
|
265
|
-
function controlMapNotice(risk) {
|
|
275
|
+
function controlMapNotice(risk, stateLabel) {
|
|
266
276
|
return {
|
|
267
277
|
code: risk.code,
|
|
268
278
|
source: 'control-map',
|
|
269
279
|
message: risk.message,
|
|
270
|
-
artifacts: ['gsdd control-map --json
|
|
280
|
+
artifacts: [`node ${stateLabel('bin', 'gsdd.mjs')} control-map --json`],
|
|
271
281
|
risk,
|
|
272
282
|
};
|
|
273
283
|
}
|
|
274
284
|
|
|
275
|
-
function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
285
|
+
function buildPhaseBlockers({ lifecycle, phaseToken, surface, stateLabel }) {
|
|
276
286
|
const blockers = [];
|
|
277
287
|
const phaseEntry = lifecycle.phases.find((phase) => phase.number === phaseToken);
|
|
278
288
|
if (!phaseEntry) {
|
|
@@ -280,7 +290,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
280
290
|
blocker(
|
|
281
291
|
'missing_phase',
|
|
282
292
|
`Phase ${phaseToken} was not found in the active roadmap.`,
|
|
283
|
-
['
|
|
293
|
+
[stateLabel('ROADMAP.md')]
|
|
284
294
|
)
|
|
285
295
|
);
|
|
286
296
|
return blockers;
|
|
@@ -298,7 +308,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
298
308
|
blocker(
|
|
299
309
|
'missing_plan',
|
|
300
310
|
`Phase ${phaseToken} cannot execute because no PLAN artifact exists.`,
|
|
301
|
-
['
|
|
311
|
+
[stateLabel('phases')]
|
|
302
312
|
)
|
|
303
313
|
);
|
|
304
314
|
} else if (pendingPlans.length === 0) {
|
|
@@ -317,7 +327,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
317
327
|
blocker(
|
|
318
328
|
'phase_already_complete',
|
|
319
329
|
`Phase ${phaseToken} is already complete and should not be planned again.`,
|
|
320
|
-
['
|
|
330
|
+
[stateLabel('ROADMAP.md')]
|
|
321
331
|
)
|
|
322
332
|
);
|
|
323
333
|
}
|
|
@@ -328,7 +338,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
328
338
|
blocker(
|
|
329
339
|
'missing_plan',
|
|
330
340
|
`Phase ${phaseToken} cannot be verified because no PLAN artifact exists.`,
|
|
331
|
-
['
|
|
341
|
+
[stateLabel('phases')]
|
|
332
342
|
)
|
|
333
343
|
);
|
|
334
344
|
}
|
|
@@ -337,7 +347,187 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
337
347
|
blocker(
|
|
338
348
|
'missing_summary',
|
|
339
349
|
`Phase ${phaseToken} cannot be verified because no SUMMARY artifact exists yet.`,
|
|
340
|
-
['
|
|
350
|
+
[stateLabel('phases')]
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return blockers;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function evaluateWorkMilestoneState({ planningDir, phaseToken }) {
|
|
360
|
+
const workspaceRoot = resolve(planningDir, '..');
|
|
361
|
+
const milestoneDir = join(workspaceRoot, '.work', 'milestone');
|
|
362
|
+
const roadmapPath = join(milestoneDir, 'ROADMAP.md');
|
|
363
|
+
const phasesDir = join(milestoneDir, 'phases');
|
|
364
|
+
|
|
365
|
+
if (!existsSync(roadmapPath)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const phases = parseWorkRoadmapPhases(readFileSync(roadmapPath, 'utf-8'));
|
|
370
|
+
const phaseEntry = phases.find((phase) => phase.number === phaseToken);
|
|
371
|
+
if (!phaseEntry) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
milestoneDir,
|
|
377
|
+
roadmapPath,
|
|
378
|
+
phasesDir,
|
|
379
|
+
phases,
|
|
380
|
+
phaseEntry,
|
|
381
|
+
phaseArtifacts: collectWorkPhaseArtifacts({ workspaceRoot, phasesDir, phaseToken }),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function parseWorkRoadmapPhases(content) {
|
|
386
|
+
return String(content || '')
|
|
387
|
+
.replace(/\r\n/g, '\n')
|
|
388
|
+
.split('\n')
|
|
389
|
+
.map((line) => {
|
|
390
|
+
const match = line.match(WORK_PHASE_LINE_RE);
|
|
391
|
+
if (!match) return null;
|
|
392
|
+
return {
|
|
393
|
+
status: parseWorkPhaseStatus(match[1]),
|
|
394
|
+
number: normalizePhaseToken(match[2]),
|
|
395
|
+
title: match[3].trim(),
|
|
396
|
+
};
|
|
397
|
+
})
|
|
398
|
+
.filter(Boolean);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function parseWorkPhaseStatus(rawStatus) {
|
|
402
|
+
const status = String(rawStatus || '').trim().toLowerCase();
|
|
403
|
+
if (status === 'x') return 'done';
|
|
404
|
+
if (status === '-') return 'in_progress';
|
|
405
|
+
return 'pending';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function evaluateResumeWorkCheckpoint({ planningDir, checkpointPath }) {
|
|
409
|
+
if (!existsSync(checkpointPath)) return null;
|
|
410
|
+
|
|
411
|
+
const workspaceRoot = resolve(planningDir, '..');
|
|
412
|
+
const milestoneDir = join(workspaceRoot, '.work', 'milestone');
|
|
413
|
+
const roadmapPath = join(milestoneDir, 'ROADMAP.md');
|
|
414
|
+
if (!existsSync(roadmapPath)) return null;
|
|
415
|
+
|
|
416
|
+
let content = '';
|
|
417
|
+
try {
|
|
418
|
+
content = readFileSync(checkpointPath, 'utf-8');
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!/(^|[`"'(\s])\.work[\\/]+milestone([`"')\s/]|$)/i.test(content)) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
milestoneDir,
|
|
429
|
+
roadmapPath,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function collectWorkPhaseArtifacts({ workspaceRoot, phasesDir, phaseToken }) {
|
|
434
|
+
if (!existsSync(phasesDir)) return [];
|
|
435
|
+
|
|
436
|
+
return collectMarkdownFiles(phasesDir)
|
|
437
|
+
.map((filePath) => {
|
|
438
|
+
const filename = filePath.split(/[\\/]/).pop();
|
|
439
|
+
const match = filename.match(/^(\d+(?:\.\d+)*[A-Za-z]?)-(.+?)\.md$/i);
|
|
440
|
+
if (!match || normalizePhaseToken(match[1]) !== phaseToken) return null;
|
|
441
|
+
|
|
442
|
+
const kind = parseWorkArtifactKind(match[2]);
|
|
443
|
+
if (!kind) return null;
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
phaseToken,
|
|
447
|
+
kind,
|
|
448
|
+
path: filePath,
|
|
449
|
+
displayPath: relativeDisplayPath(workspaceRoot, filePath),
|
|
450
|
+
};
|
|
451
|
+
})
|
|
452
|
+
.filter(Boolean);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function collectMarkdownFiles(dir) {
|
|
456
|
+
const files = [];
|
|
457
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
458
|
+
const fullPath = join(dir, entry.name);
|
|
459
|
+
if (entry.isDirectory()) {
|
|
460
|
+
files.push(...collectMarkdownFiles(fullPath));
|
|
461
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
|
|
462
|
+
files.push(fullPath);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return files;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function parseWorkArtifactKind(rawKind) {
|
|
469
|
+
const kind = String(rawKind || '').toLowerCase();
|
|
470
|
+
if (kind === 'plan') return 'plan';
|
|
471
|
+
if (kind === 'execute') return 'execute';
|
|
472
|
+
if (kind === 'verify' || kind === 'verification') return 'verification';
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function relativeDisplayPath(root, filePath) {
|
|
477
|
+
return filePath.slice(root.length + 1).replace(/\\/g, '/');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function buildWorkPhaseBlockers({ workMilestone, phaseToken, surface }) {
|
|
481
|
+
const blockers = [];
|
|
482
|
+
const planArtifacts = workMilestone.phaseArtifacts.filter((artifact) => artifact.kind === 'plan');
|
|
483
|
+
const executeArtifacts = workMilestone.phaseArtifacts.filter((artifact) => artifact.kind === 'execute');
|
|
484
|
+
|
|
485
|
+
if (surface === 'execute') {
|
|
486
|
+
if (planArtifacts.length === 0) {
|
|
487
|
+
blockers.push(
|
|
488
|
+
blocker(
|
|
489
|
+
'missing_plan',
|
|
490
|
+
`Phase ${phaseToken} cannot execute because no .work PLAN artifact exists.`,
|
|
491
|
+
['.work/milestone/phases/']
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
} else if (executeArtifacts.length > 0) {
|
|
495
|
+
blockers.push(
|
|
496
|
+
blocker(
|
|
497
|
+
'no_pending_plan',
|
|
498
|
+
`Phase ${phaseToken} has already been executed in .work/milestone.`,
|
|
499
|
+
executeArtifacts.map((artifact) => artifact.displayPath)
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (surface === 'plan' && workMilestone.phaseEntry.status === 'done') {
|
|
506
|
+
blockers.push(
|
|
507
|
+
blocker(
|
|
508
|
+
'phase_already_complete',
|
|
509
|
+
`Phase ${phaseToken} is already complete in .work/milestone and should not be planned again.`,
|
|
510
|
+
['.work/milestone/ROADMAP.md']
|
|
511
|
+
)
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (surface === 'verify') {
|
|
516
|
+
if (planArtifacts.length === 0) {
|
|
517
|
+
blockers.push(
|
|
518
|
+
blocker(
|
|
519
|
+
'missing_plan',
|
|
520
|
+
`Phase ${phaseToken} cannot be verified because no .work PLAN artifact exists.`,
|
|
521
|
+
['.work/milestone/phases/']
|
|
522
|
+
)
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
if (executeArtifacts.length === 0) {
|
|
526
|
+
blockers.push(
|
|
527
|
+
blocker(
|
|
528
|
+
'missing_execute',
|
|
529
|
+
`Phase ${phaseToken} cannot be verified because no .work EXECUTE artifact exists yet.`,
|
|
530
|
+
['.work/milestone/phases/']
|
|
341
531
|
)
|
|
342
532
|
);
|
|
343
533
|
}
|
|
@@ -346,21 +536,21 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
346
536
|
return blockers;
|
|
347
537
|
}
|
|
348
538
|
|
|
349
|
-
function buildRoadmapAlignmentBlockers(lifecycle) {
|
|
539
|
+
function buildRoadmapAlignmentBlockers(lifecycle, stateLabel) {
|
|
350
540
|
if (lifecycle.phaseStatusAlignment.mismatches.length === 0) return [];
|
|
351
541
|
return [
|
|
352
542
|
blocker(
|
|
353
543
|
'roadmap_phase_status_mismatch',
|
|
354
544
|
`ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
|
|
355
|
-
['
|
|
545
|
+
[stateLabel('ROADMAP.md')]
|
|
356
546
|
),
|
|
357
547
|
];
|
|
358
548
|
}
|
|
359
549
|
|
|
360
|
-
function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
550
|
+
function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false, stateLabel } = {}) {
|
|
361
551
|
const blockers = [];
|
|
362
552
|
if (!lifecycle.currentMilestone.version) {
|
|
363
|
-
blockers.push(blocker('missing_milestone', 'No active or retained milestone could be derived from ROADMAP.md.', ['
|
|
553
|
+
blockers.push(blocker('missing_milestone', 'No active or retained milestone could be derived from ROADMAP.md.', [stateLabel('ROADMAP.md')]));
|
|
364
554
|
return blockers;
|
|
365
555
|
}
|
|
366
556
|
|
|
@@ -369,19 +559,19 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
|
369
559
|
blocker(
|
|
370
560
|
allowArchivedBlocker ? 'milestone_already_archived' : 'milestone_already_archived',
|
|
371
561
|
`Milestone ${lifecycle.currentMilestone.version} is already archived-with-ROADMAP.md evidence.`,
|
|
372
|
-
['
|
|
562
|
+
[stateLabel('ROADMAP.md'), stateLabel('MILESTONES.md')]
|
|
373
563
|
)
|
|
374
564
|
);
|
|
375
565
|
}
|
|
376
566
|
|
|
377
567
|
if (lifecycle.counts.total === 0) {
|
|
378
|
-
blockers.push(blocker('missing_phases', 'No active milestone phases were found in ROADMAP.md.', ['
|
|
568
|
+
blockers.push(blocker('missing_phases', 'No active milestone phases were found in ROADMAP.md.', [stateLabel('ROADMAP.md')]));
|
|
379
569
|
} else if (lifecycle.counts.completed !== lifecycle.counts.total) {
|
|
380
570
|
blockers.push(
|
|
381
571
|
blocker(
|
|
382
572
|
'incomplete_phases',
|
|
383
573
|
`Milestone ${lifecycle.currentMilestone.version} still has incomplete phases (${lifecycle.counts.completed}/${lifecycle.counts.total} complete).`,
|
|
384
|
-
['
|
|
574
|
+
[stateLabel('ROADMAP.md')]
|
|
385
575
|
)
|
|
386
576
|
);
|
|
387
577
|
}
|
|
@@ -396,7 +586,7 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
|
396
586
|
blocker(
|
|
397
587
|
'missing_verification',
|
|
398
588
|
`Completed phases are missing VERIFICATION artifacts (${phasesMissingVerification.join(', ')}).`,
|
|
399
|
-
['
|
|
589
|
+
[stateLabel('phases')]
|
|
400
590
|
)
|
|
401
591
|
);
|
|
402
592
|
}
|
|
@@ -405,13 +595,14 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
|
405
595
|
}
|
|
406
596
|
|
|
407
597
|
function buildCompletionBlockers(planningDir, lifecycle) {
|
|
598
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
408
599
|
const auditPath = join(planningDir, `${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`);
|
|
409
600
|
if (!existsSync(auditPath)) {
|
|
410
601
|
return [
|
|
411
602
|
blocker(
|
|
412
603
|
'missing_milestone_audit',
|
|
413
604
|
`Milestone ${lifecycle.currentMilestone.version} cannot be completed without a milestone audit artifact.`,
|
|
414
|
-
[
|
|
605
|
+
[stateLabel(`${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`)]
|
|
415
606
|
),
|
|
416
607
|
];
|
|
417
608
|
}
|
|
@@ -424,194 +615,14 @@ function buildCompletionBlockers(planningDir, lifecycle) {
|
|
|
424
615
|
blocker(
|
|
425
616
|
'audit_not_passed',
|
|
426
617
|
`Milestone ${lifecycle.currentMilestone.version} requires a passed audit before completion.`,
|
|
427
|
-
[
|
|
618
|
+
[stateLabel(`${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`)]
|
|
428
619
|
),
|
|
429
620
|
];
|
|
430
621
|
}
|
|
431
622
|
|
|
432
|
-
const releaseContractBlockers = buildReleaseClaimCompletionBlockers(auditContent, auditPath);
|
|
433
|
-
if (releaseContractBlockers.length > 0) return releaseContractBlockers;
|
|
434
|
-
|
|
435
623
|
return [];
|
|
436
624
|
}
|
|
437
625
|
|
|
438
|
-
function buildReleaseClaimCompletionBlockers(auditContent, auditPath) {
|
|
439
|
-
const frontmatter = extractFrontmatter(auditContent);
|
|
440
|
-
const deliveryPosture = readTopLevelScalar(frontmatter, 'delivery_posture');
|
|
441
|
-
const releaseClaimPosture = readTopLevelScalar(frontmatter, 'release_claim_posture');
|
|
442
|
-
const evidenceBlock = extractYamlBlock(frontmatter, 'evidence_contract');
|
|
443
|
-
const releaseBlock = extractYamlBlock(frontmatter, 'release_claim_contract');
|
|
444
|
-
const missing = [];
|
|
445
|
-
|
|
446
|
-
if (!deliveryPosture) missing.push('delivery_posture');
|
|
447
|
-
if (!releaseClaimPosture) missing.push('release_claim_posture');
|
|
448
|
-
if (!evidenceBlock) missing.push('evidence_contract');
|
|
449
|
-
if (!releaseBlock) missing.push('release_claim_contract');
|
|
450
|
-
|
|
451
|
-
if (missing.length > 0) {
|
|
452
|
-
return [blocker(
|
|
453
|
-
'missing_release_claim_contract',
|
|
454
|
-
`Milestone audit is missing release closeout metadata (${missing.join(', ')}). Re-run audit before completion.`,
|
|
455
|
-
[auditPath]
|
|
456
|
-
)];
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const requiredKinds = readBlockList(evidenceBlock, 'required_kinds');
|
|
460
|
-
const observedKinds = readBlockList(evidenceBlock, 'observed_kinds');
|
|
461
|
-
const missingKinds = readBlockList(evidenceBlock, 'missing_kinds');
|
|
462
|
-
const unsupportedClaims = readBlockList(releaseBlock, 'unsupported_claims');
|
|
463
|
-
const waivedKinds = readBlockList(releaseBlock, 'waivers');
|
|
464
|
-
const deferrals = readBlockList(releaseBlock, 'deferrals');
|
|
465
|
-
const contradictionChecks = readNestedStatusBlock(releaseBlock, 'contradiction_checks');
|
|
466
|
-
const blockers = [];
|
|
467
|
-
const invalidEvidenceKinds = [
|
|
468
|
-
...findInvalidEvidenceKinds('required_kinds', requiredKinds),
|
|
469
|
-
...findInvalidEvidenceKinds('observed_kinds', observedKinds),
|
|
470
|
-
...findInvalidEvidenceKinds('missing_kinds', missingKinds),
|
|
471
|
-
...findInvalidEvidenceKinds('waivers', waivedKinds),
|
|
472
|
-
];
|
|
473
|
-
|
|
474
|
-
if (requiredKinds.length === 0 && observedKinds.length === 0) {
|
|
475
|
-
blockers.push(blocker(
|
|
476
|
-
'missing_release_evidence_contract',
|
|
477
|
-
'Milestone audit evidence_contract must include required_kinds and observed_kinds before completion.',
|
|
478
|
-
[auditPath]
|
|
479
|
-
));
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (!DELIVERY_POSTURES.includes(deliveryPosture)) {
|
|
483
|
-
blockers.push(blocker(
|
|
484
|
-
'invalid_delivery_posture',
|
|
485
|
-
`Milestone audit has invalid delivery_posture (${deliveryPosture}). Re-run audit before completion.`,
|
|
486
|
-
[auditPath]
|
|
487
|
-
));
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (!RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
|
|
491
|
-
blockers.push(blocker(
|
|
492
|
-
'invalid_release_claim_posture',
|
|
493
|
-
`Milestone audit has invalid release_claim_posture (${releaseClaimPosture}). Re-run audit before completion.`,
|
|
494
|
-
[auditPath]
|
|
495
|
-
));
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (invalidEvidenceKinds.length > 0) {
|
|
499
|
-
blockers.push(blocker(
|
|
500
|
-
'invalid_release_evidence_kinds',
|
|
501
|
-
`Milestone audit has invalid release evidence kind values (${invalidEvidenceKinds.join(', ')}). Supported values are ${EVIDENCE_KINDS.join(', ')}.`,
|
|
502
|
-
[auditPath]
|
|
503
|
-
));
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const missingContradictionChecks = RELEASE_CONTRADICTION_CHECKS.filter((name) => !(name in contradictionChecks));
|
|
507
|
-
const unknownContradictionChecks = Object.keys(contradictionChecks)
|
|
508
|
-
.filter((name) => !RELEASE_CONTRADICTION_CHECKS.includes(name));
|
|
509
|
-
const invalidContradictionChecks = Object.entries(contradictionChecks)
|
|
510
|
-
.filter(([, status]) => !RELEASE_CONTRADICTION_STATUSES.includes(status))
|
|
511
|
-
.map(([name]) => name);
|
|
512
|
-
|
|
513
|
-
if (missingContradictionChecks.length > 0) {
|
|
514
|
-
blockers.push(blocker(
|
|
515
|
-
'missing_release_contradiction_checks',
|
|
516
|
-
`Milestone audit release_claim_contract.contradiction_checks is missing required checks (${missingContradictionChecks.join(', ')}).`,
|
|
517
|
-
[auditPath]
|
|
518
|
-
));
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (invalidContradictionChecks.length > 0) {
|
|
522
|
-
blockers.push(blocker(
|
|
523
|
-
'invalid_release_contradiction_checks',
|
|
524
|
-
`Milestone audit release_claim_contract.contradiction_checks has invalid statuses (${invalidContradictionChecks.join(', ')}).`,
|
|
525
|
-
[auditPath]
|
|
526
|
-
));
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (unknownContradictionChecks.length > 0) {
|
|
530
|
-
blockers.push(blocker(
|
|
531
|
-
'unknown_release_contradiction_checks',
|
|
532
|
-
`Milestone audit release_claim_contract.contradiction_checks has unknown checks (${unknownContradictionChecks.join(', ')}). Supported checks are ${RELEASE_CONTRADICTION_CHECKS.join(', ')}.`,
|
|
533
|
-
[auditPath]
|
|
534
|
-
));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (!DELIVERY_POSTURES.includes(deliveryPosture) || !RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
|
|
538
|
-
return blockers;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const releaseEvaluation = evaluateReleaseClaimCloseoutContract({
|
|
542
|
-
surface: 'complete-milestone',
|
|
543
|
-
deliveryPosture,
|
|
544
|
-
releaseClaimPosture,
|
|
545
|
-
observedKinds,
|
|
546
|
-
waivedKinds,
|
|
547
|
-
unsupportedClaims,
|
|
548
|
-
deferrals,
|
|
549
|
-
contradictionChecks,
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
if (DELIVERY_POSTURES.includes(deliveryPosture)) {
|
|
553
|
-
const evidenceContract = getEvidenceContract('complete-milestone', deliveryPosture);
|
|
554
|
-
const enforcedRequiredKinds = [...new Set([...evidenceContract.requiredKinds, ...releaseEvaluation.requiredKinds])];
|
|
555
|
-
const undeclaredRequiredKinds = enforcedRequiredKinds.filter((kind) => !requiredKinds.includes(kind));
|
|
556
|
-
const recomputedMissingKinds = enforcedRequiredKinds.filter((kind) => !observedKinds.includes(kind));
|
|
557
|
-
|
|
558
|
-
if (undeclaredRequiredKinds.length > 0) {
|
|
559
|
-
blockers.push(blocker(
|
|
560
|
-
'invalid_release_evidence_contract',
|
|
561
|
-
`Milestone audit evidence_contract.required_kinds omits required closeout evidence (${undeclaredRequiredKinds.join(', ')}).`,
|
|
562
|
-
[auditPath]
|
|
563
|
-
));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if (recomputedMissingKinds.length > 0) {
|
|
567
|
-
blockers.push(blocker(
|
|
568
|
-
'missing_required_release_evidence',
|
|
569
|
-
`Milestone audit observed evidence is missing required closeout kinds (${recomputedMissingKinds.join(', ')}).`,
|
|
570
|
-
[auditPath]
|
|
571
|
-
));
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (missingKinds.length > 0) {
|
|
576
|
-
blockers.push(blocker(
|
|
577
|
-
'missing_required_release_evidence',
|
|
578
|
-
`Milestone audit is missing required evidence kinds for closeout (${missingKinds.join(', ')}).`,
|
|
579
|
-
[auditPath]
|
|
580
|
-
));
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (releaseEvaluation.invalidWaivers.length > 0) {
|
|
584
|
-
blockers.push(blocker(
|
|
585
|
-
'invalid_release_waivers',
|
|
586
|
-
`Milestone audit has invalid waivers for missing required evidence (${releaseEvaluation.invalidWaivers.join(', ')}).`,
|
|
587
|
-
[auditPath]
|
|
588
|
-
));
|
|
589
|
-
}
|
|
590
|
-
if (releaseEvaluation.blockers.some((releaseBlocker) => releaseBlocker.code === 'incompatible_release_claim_posture')) {
|
|
591
|
-
blockers.push(blocker(
|
|
592
|
-
'incompatible_release_claim_posture',
|
|
593
|
-
`Milestone audit release_claim_posture (${releaseClaimPosture}) is incompatible with delivery_posture (${deliveryPosture}).`,
|
|
594
|
-
[auditPath]
|
|
595
|
-
));
|
|
596
|
-
}
|
|
597
|
-
if (releaseEvaluation.unresolvedUnsupportedClaims.length > 0) {
|
|
598
|
-
blockers.push(blocker(
|
|
599
|
-
'unsupported_release_claims',
|
|
600
|
-
`Milestone audit has unsupported release claims without downgrade or deferral (${releaseEvaluation.unresolvedUnsupportedClaims.join(', ')}).`,
|
|
601
|
-
[auditPath]
|
|
602
|
-
));
|
|
603
|
-
}
|
|
604
|
-
if (releaseEvaluation.failedContradictionChecks.length > 0) {
|
|
605
|
-
blockers.push(blocker(
|
|
606
|
-
'failed_release_contradiction_checks',
|
|
607
|
-
`Milestone audit has failed release contradiction checks (${releaseEvaluation.failedContradictionChecks.join(', ')}).`,
|
|
608
|
-
[auditPath]
|
|
609
|
-
));
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return blockers;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
626
|
function extractFrontmatter(content) {
|
|
616
627
|
const match = String(content || '').replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/);
|
|
617
628
|
return match ? match[1] : '';
|
|
@@ -622,136 +633,6 @@ function readTopLevelScalar(frontmatter, key) {
|
|
|
622
633
|
return match ? cleanYamlValue(match[1]) : null;
|
|
623
634
|
}
|
|
624
635
|
|
|
625
|
-
function extractYamlBlock(frontmatter, key) {
|
|
626
|
-
const lines = String(frontmatter || '').replace(/\r\n/g, '\n').split('\n');
|
|
627
|
-
const startIndex = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line.trim()));
|
|
628
|
-
if (startIndex === -1) return '';
|
|
629
|
-
|
|
630
|
-
const collected = [];
|
|
631
|
-
for (const line of lines.slice(startIndex + 1)) {
|
|
632
|
-
if (/^[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
633
|
-
collected.push(line);
|
|
634
|
-
}
|
|
635
|
-
return collected.join('\n');
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function readBlockList(block, key) {
|
|
639
|
-
const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
|
|
640
|
-
const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
|
|
641
|
-
if (startIndex === -1) return [];
|
|
642
|
-
const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
|
|
643
|
-
|
|
644
|
-
const inline = lines[startIndex].match(/^\s+[^:]+:\s*\[([^\]]*)\]/);
|
|
645
|
-
if (inline) return splitInlineList(inline[1]);
|
|
646
|
-
|
|
647
|
-
const collected = [];
|
|
648
|
-
for (const line of lines.slice(startIndex + 1)) {
|
|
649
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
650
|
-
if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
651
|
-
collected.push(line);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return parseYamlListItems(collected, baseIndent)
|
|
655
|
-
.map((item) => item.join(' '))
|
|
656
|
-
.map(cleanYamlValue);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function parseYamlListItems(lines, baseIndent) {
|
|
660
|
-
const items = [];
|
|
661
|
-
let current = null;
|
|
662
|
-
let itemIndent = null;
|
|
663
|
-
|
|
664
|
-
for (const line of lines) {
|
|
665
|
-
const match = line.match(/^(\s*)-\s*(.+?)\s*$/);
|
|
666
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
667
|
-
|
|
668
|
-
if (match && indent > baseIndent && (itemIndent === null || indent === itemIndent)) {
|
|
669
|
-
if (current) items.push(current);
|
|
670
|
-
current = [match[2]];
|
|
671
|
-
itemIndent = indent;
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (current && line.trim()) {
|
|
676
|
-
current.push(line.trim());
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
if (current) items.push(current);
|
|
681
|
-
return items;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function findInvalidEvidenceKinds(field, kinds) {
|
|
685
|
-
return kinds
|
|
686
|
-
.filter((kind) => !EVIDENCE_KINDS.includes(kind))
|
|
687
|
-
.map((kind) => `${field}: ${kind}`);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function readNestedStatusBlock(block, key) {
|
|
691
|
-
const nested = extractIndentedBlock(block, key);
|
|
692
|
-
const statuses = {};
|
|
693
|
-
for (const line of nested.split('\n')) {
|
|
694
|
-
const match = line.match(/^\s+([A-Za-z0-9_-]+):\s*(.+)$/);
|
|
695
|
-
if (match) statuses[match[1]] = cleanYamlValue(match[2]);
|
|
696
|
-
}
|
|
697
|
-
return statuses;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function extractIndentedBlock(block, key) {
|
|
701
|
-
const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
|
|
702
|
-
const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
|
|
703
|
-
if (startIndex === -1) return '';
|
|
704
|
-
const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
|
|
705
|
-
|
|
706
|
-
const collected = [];
|
|
707
|
-
for (const line of lines.slice(startIndex + 1)) {
|
|
708
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
709
|
-
if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
710
|
-
collected.push(line);
|
|
711
|
-
}
|
|
712
|
-
return collected.join('\n');
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function splitInlineList(value) {
|
|
716
|
-
return splitCommaAware(value)
|
|
717
|
-
.map(cleanYamlValue)
|
|
718
|
-
.filter(Boolean);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
function splitCommaAware(value) {
|
|
722
|
-
const items = [];
|
|
723
|
-
let current = '';
|
|
724
|
-
let quote = null;
|
|
725
|
-
let escaped = false;
|
|
726
|
-
|
|
727
|
-
for (const char of String(value || '')) {
|
|
728
|
-
if (escaped) {
|
|
729
|
-
current += char;
|
|
730
|
-
escaped = false;
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
if (char === '\\' && quote) {
|
|
734
|
-
current += char;
|
|
735
|
-
escaped = true;
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
if ((char === '"' || char === "'") && (!quote || quote === char)) {
|
|
739
|
-
quote = quote ? null : char;
|
|
740
|
-
current += char;
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
if (char === ',' && !quote) {
|
|
744
|
-
items.push(current);
|
|
745
|
-
current = '';
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
current += char;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
items.push(current);
|
|
752
|
-
return items;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
636
|
function cleanYamlValue(value) {
|
|
756
637
|
return stripInlineYamlComment(String(value || ''))
|
|
757
638
|
.trim()
|
|
@@ -794,6 +675,17 @@ function blocker(code, message, artifacts) {
|
|
|
794
675
|
return { code, message, artifacts };
|
|
795
676
|
}
|
|
796
677
|
|
|
678
|
+
function createStateLabeler(planningDir) {
|
|
679
|
+
const workspaceRoot = resolve(planningDir, '..');
|
|
680
|
+
return (...segments) => {
|
|
681
|
+
const targetPath = resolve(join(planningDir, ...segments));
|
|
682
|
+
const label = relative(workspaceRoot, targetPath);
|
|
683
|
+
if (label === '') return '.';
|
|
684
|
+
if (!label.startsWith('..') && !isAbsolute(label)) return label.replace(/\\/g, '/');
|
|
685
|
+
return targetPath.replace(/\\/g, '/');
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
797
689
|
export function cmdLifecyclePreflight(...args) {
|
|
798
690
|
const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
|
|
799
691
|
if (invalid) {
|
|
@@ -802,9 +694,10 @@ export function cmdLifecyclePreflight(...args) {
|
|
|
802
694
|
return;
|
|
803
695
|
}
|
|
804
696
|
const [surface, maybePhase, ...rest] = normalizedArgs;
|
|
697
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
805
698
|
|
|
806
699
|
if (!surface) {
|
|
807
|
-
console.error(
|
|
700
|
+
console.error(`Usage: node ${stateLabel('bin', 'gsdd.mjs')} lifecycle-preflight <surface> [phase] [--expects-mutation <none|phase-status>]`);
|
|
808
701
|
process.exitCode = 1;
|
|
809
702
|
return;
|
|
810
703
|
}
|