gsdd-cli 0.27.0 → 0.29.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 +53 -22
- package/agents/DISTILLATION.md +2 -2
- package/agents/README.md +4 -4
- package/agents/approach-explorer.md +4 -4
- package/agents/executor.md +20 -20
- package/agents/integration-checker.md +2 -2
- package/agents/planner.md +10 -26
- package/agents/researcher.md +2 -2
- package/agents/roadmapper.md +6 -6
- package/agents/synthesizer.md +18 -18
- package/agents/verifier.md +3 -3
- 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/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 +11 -30
- package/bin/lib/lifecycle-preflight.mjs +97 -410
- package/bin/lib/lifecycle-state.mjs +2 -1
- package/bin/lib/next.mjs +243 -20
- package/bin/lib/phase.mjs +706 -280
- package/bin/lib/plan-constants.mjs +0 -5
- 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 +89 -31
- package/distilled/EVIDENCE-INDEX.md +18 -5
- 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 +18 -19
- 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/templates/ui-proof.md +81 -181
- package/distilled/workflows/audit-milestone.md +23 -23
- package/distilled/workflows/complete-milestone.md +35 -35
- package/distilled/workflows/execute.md +34 -35
- 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 +106 -114
- package/distilled/workflows/progress.md +40 -39
- package/distilled/workflows/quick.md +49 -50
- package/distilled/workflows/resume.md +23 -22
- package/distilled/workflows/verify-work.md +7 -7
- package/distilled/workflows/verify.md +26 -26
- package/docs/BROWNFIELD-PROOF.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +13 -13
- package/docs/USER-GUIDE.md +26 -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
1
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
-
import { join, resolve } from 'path';
|
|
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,6 @@ const SURFACE_POLICIES = {
|
|
|
65
51
|
},
|
|
66
52
|
};
|
|
67
53
|
|
|
68
|
-
const RELEASE_CONTRADICTION_CHECKS = Object.freeze([
|
|
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']);
|
|
79
54
|
const WORK_PHASE_LINE_RE = /^\s*[-*]\s*\[([ x-])\]\s*\*\*Phase\s+(\d+(?:\.\d+)*[A-Za-z]?):\s*(.+?)\*\*/i;
|
|
80
55
|
|
|
81
56
|
export function evaluateLifecyclePreflight({
|
|
@@ -96,18 +71,25 @@ export function evaluateLifecyclePreflight({
|
|
|
96
71
|
|
|
97
72
|
const lifecycle = evaluateLifecycleState({ planningDir });
|
|
98
73
|
const normalizedPhase = phaseNumber ? normalizePhaseToken(phaseNumber) : null;
|
|
74
|
+
const usesBrownfieldAuthority = surface === 'plan' && normalizedPhase === 'brownfield-change';
|
|
75
|
+
const usesPlanAmendAuthority = surface === 'plan' && normalizedPhase === 'amend';
|
|
99
76
|
const workMilestone = normalizedPhase ? evaluateWorkMilestoneState({ planningDir, phaseToken: normalizedPhase }) : null;
|
|
100
77
|
const checkpointPath = join(planningDir, '.continue-here.md');
|
|
78
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
101
79
|
const resumeWorkCheckpoint = surface === 'resume'
|
|
102
80
|
? evaluateResumeWorkCheckpoint({ planningDir, checkpointPath })
|
|
103
81
|
: null;
|
|
104
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;
|
|
105
87
|
const specPath = join(planningDir, 'SPEC.md');
|
|
106
88
|
const milestonesPath = join(planningDir, 'MILESTONES.md');
|
|
107
89
|
const blockers = [];
|
|
108
90
|
|
|
109
91
|
if (!existsSync(planningDir)) {
|
|
110
|
-
blockers.push(blocker('missing_planning_dir', '.
|
|
92
|
+
blockers.push(blocker('missing_planning_dir', `${stateLabel('.')} does not exist yet.`, [stateLabel('.')]));
|
|
111
93
|
}
|
|
112
94
|
|
|
113
95
|
if (expectsMutation !== 'none' && expectsMutation !== policy.explicitLifecycleMutation) {
|
|
@@ -124,83 +106,72 @@ export function evaluateLifecyclePreflight({
|
|
|
124
106
|
blockers.push(blocker('missing_phase_argument', `${surface} requires an explicit phase number.`, []));
|
|
125
107
|
}
|
|
126
108
|
|
|
127
|
-
if (
|
|
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) {
|
|
128
130
|
blockers.push(
|
|
129
131
|
...(usesWorkAuthority
|
|
130
132
|
? buildWorkPhaseBlockers({ workMilestone, phaseToken: normalizedPhase, surface })
|
|
131
|
-
: buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface }))
|
|
133
|
+
: buildPhaseBlockers({ lifecycle, phaseToken: normalizedPhase, surface, stateLabel }))
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
if (surface === 'audit-milestone') {
|
|
136
|
-
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
|
|
137
|
-
blockers.push(...buildAuditBlockers(lifecycle));
|
|
138
|
+
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
|
|
139
|
+
blockers.push(...buildAuditBlockers(lifecycle, { stateLabel }));
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
if (surface === 'complete-milestone') {
|
|
141
|
-
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
|
|
142
|
-
blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true }));
|
|
143
|
+
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
|
|
144
|
+
blockers.push(...buildAuditBlockers(lifecycle, { allowArchivedBlocker: true, stateLabel }));
|
|
143
145
|
blockers.push(...buildCompletionBlockers(planningDir, lifecycle));
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
if (surface === 'new-milestone') {
|
|
147
|
-
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle));
|
|
149
|
+
blockers.push(...buildRoadmapAlignmentBlockers(lifecycle, stateLabel));
|
|
148
150
|
if (!existsSync(specPath)) {
|
|
149
|
-
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')]));
|
|
150
152
|
}
|
|
151
153
|
if (!existsSync(milestonesPath)) {
|
|
152
|
-
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')]));
|
|
153
155
|
}
|
|
154
156
|
if (lifecycle.currentMilestone.version && lifecycle.currentMilestone.archiveState !== 'archived') {
|
|
155
157
|
blockers.push(
|
|
156
158
|
blocker(
|
|
157
159
|
'active_milestone_in_progress',
|
|
158
160
|
`Milestone ${lifecycle.currentMilestone.version} is still active. Archive or remove the active roadmap before starting the next milestone.`,
|
|
159
|
-
['
|
|
161
|
+
[stateLabel('ROADMAP.md')]
|
|
160
162
|
)
|
|
161
163
|
);
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
if (surface === 'resume' && !existsSync(checkpointPath) && lifecycle.nonPhaseState !== 'active_brownfield_change') {
|
|
166
|
-
|
|
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]));
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
const warnings = [];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (existsSync(planningDir)) {
|
|
173
|
-
const drift = checkDrift(planningDir);
|
|
174
|
-
planningState = {
|
|
175
|
-
classification: drift.classification,
|
|
176
|
-
drifted: drift.drifted,
|
|
177
|
-
noBaseline: drift.noBaseline,
|
|
178
|
-
details: drift.details,
|
|
179
|
-
files: drift.files,
|
|
180
|
-
};
|
|
181
|
-
if (drift.drifted) {
|
|
182
|
-
const driftNotice = {
|
|
183
|
-
code: 'planning_state_drift',
|
|
184
|
-
message: `${surface} cannot proceed because planning state drifted since the last recorded session: ${drift.details.join('; ')}`,
|
|
185
|
-
artifacts: ['.planning/ROADMAP.md', '.planning/SPEC.md', '.planning/config.json'],
|
|
186
|
-
details: drift.details,
|
|
187
|
-
files: drift.files,
|
|
188
|
-
};
|
|
189
|
-
if (policy.classification === 'owned_write' && !usesWorkAuthority) {
|
|
190
|
-
blockers.push(driftNotice);
|
|
191
|
-
} else {
|
|
192
|
-
const workWarningContext = resumeWorkCheckpoint
|
|
193
|
-
? 'because the checkpoint points at .work/milestone continuity'
|
|
194
|
-
: `by .work/milestone for Phase ${normalizedPhase}`;
|
|
195
|
-
warnings.push({
|
|
196
|
-
...driftNotice,
|
|
197
|
-
message: usesWorkAuthority
|
|
198
|
-
? `Planning state has drifted since the last recorded session, but ${surface} is using work_milestone authority ${workWarningContext}: ${drift.details.join('; ')}`
|
|
199
|
-
: `Planning state has drifted since the last recorded session: ${drift.details.join('; ')}`,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
174
|
+
const planningState = null;
|
|
204
175
|
|
|
205
176
|
const controlMap = buildPreflightControlMap({
|
|
206
177
|
planningDir,
|
|
@@ -213,11 +184,11 @@ export function evaluateLifecyclePreflight({
|
|
|
213
184
|
else warnings.push(notice);
|
|
214
185
|
}
|
|
215
186
|
|
|
216
|
-
if (!
|
|
187
|
+
if (!usesAlternateAuthority && lifecycle.phaseStatusAlignment.mismatches.length > 0) {
|
|
217
188
|
warnings.push({
|
|
218
189
|
code: 'roadmap_phase_status_mismatch',
|
|
219
190
|
message: `ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
|
|
220
|
-
artifacts: ['
|
|
191
|
+
artifacts: [stateLabel('ROADMAP.md')],
|
|
221
192
|
});
|
|
222
193
|
}
|
|
223
194
|
|
|
@@ -225,11 +196,10 @@ export function evaluateLifecyclePreflight({
|
|
|
225
196
|
surface,
|
|
226
197
|
phase: normalizedPhase,
|
|
227
198
|
classification: policy.classification,
|
|
228
|
-
ownedWrites
|
|
199
|
+
ownedWrites,
|
|
229
200
|
explicitLifecycleMutation: policy.explicitLifecycleMutation,
|
|
230
|
-
closureEvidence: describeEvidenceSurface(surface),
|
|
231
201
|
mutationRequest: expectsMutation,
|
|
232
|
-
authority: usesWorkAuthority ? 'work_milestone' : 'planning',
|
|
202
|
+
authority: usesPlanAmendAuthority ? 'plan_amend' : usesBrownfieldAuthority ? 'brownfield_change' : usesWorkAuthority ? 'work_milestone' : 'planning',
|
|
233
203
|
allowed: blockers.length === 0,
|
|
234
204
|
status: blockers.length === 0 ? 'allowed' : 'blocked',
|
|
235
205
|
reason: blockers[0]?.code ?? null,
|
|
@@ -238,7 +208,7 @@ export function evaluateLifecyclePreflight({
|
|
|
238
208
|
planningState,
|
|
239
209
|
controlMap: controlMap.summary,
|
|
240
210
|
lifecycle: {
|
|
241
|
-
authority: usesWorkAuthority ? 'work_milestone' : 'planning',
|
|
211
|
+
authority: usesPlanAmendAuthority ? 'plan_amend' : usesBrownfieldAuthority ? 'brownfield_change' : usesWorkAuthority ? 'work_milestone' : 'planning',
|
|
242
212
|
currentMilestone: lifecycle.currentMilestone,
|
|
243
213
|
currentPhase: lifecycle.currentPhase ? lifecycle.currentPhase.number : null,
|
|
244
214
|
nextPhase: lifecycle.nextPhase ? lifecycle.nextPhase.number : null,
|
|
@@ -252,6 +222,20 @@ export function evaluateLifecyclePreflight({
|
|
|
252
222
|
source: resumeWorkCheckpoint ? 'checkpoint' : 'phase',
|
|
253
223
|
}
|
|
254
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,
|
|
255
239
|
},
|
|
256
240
|
};
|
|
257
241
|
}
|
|
@@ -268,11 +252,11 @@ function buildPreflightControlMap({ planningDir, policy, existingBlockerCodes, c
|
|
|
268
252
|
planningDir,
|
|
269
253
|
});
|
|
270
254
|
const risks = (map.risks || []).filter((risk) => (
|
|
271
|
-
!
|
|
272
|
-
&& !(existingBlockerCodes.has(risk.code) && risk.severity !== 'block')
|
|
255
|
+
!(existingBlockerCodes.has(risk.code) && risk.severity !== 'block')
|
|
273
256
|
));
|
|
257
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
274
258
|
const notices = risks.map((risk) => ({
|
|
275
|
-
...controlMapNotice(risk),
|
|
259
|
+
...controlMapNotice(risk, stateLabel),
|
|
276
260
|
severity: risk.severity || 'info',
|
|
277
261
|
}));
|
|
278
262
|
|
|
@@ -288,17 +272,17 @@ function buildPreflightControlMap({ planningDir, policy, existingBlockerCodes, c
|
|
|
288
272
|
};
|
|
289
273
|
}
|
|
290
274
|
|
|
291
|
-
function controlMapNotice(risk) {
|
|
275
|
+
function controlMapNotice(risk, stateLabel) {
|
|
292
276
|
return {
|
|
293
277
|
code: risk.code,
|
|
294
278
|
source: 'control-map',
|
|
295
279
|
message: risk.message,
|
|
296
|
-
artifacts: ['gsdd control-map --json
|
|
280
|
+
artifacts: [`node ${stateLabel('bin', 'gsdd.mjs')} control-map --json`],
|
|
297
281
|
risk,
|
|
298
282
|
};
|
|
299
283
|
}
|
|
300
284
|
|
|
301
|
-
function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
285
|
+
function buildPhaseBlockers({ lifecycle, phaseToken, surface, stateLabel }) {
|
|
302
286
|
const blockers = [];
|
|
303
287
|
const phaseEntry = lifecycle.phases.find((phase) => phase.number === phaseToken);
|
|
304
288
|
if (!phaseEntry) {
|
|
@@ -306,7 +290,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
306
290
|
blocker(
|
|
307
291
|
'missing_phase',
|
|
308
292
|
`Phase ${phaseToken} was not found in the active roadmap.`,
|
|
309
|
-
['
|
|
293
|
+
[stateLabel('ROADMAP.md')]
|
|
310
294
|
)
|
|
311
295
|
);
|
|
312
296
|
return blockers;
|
|
@@ -324,7 +308,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
324
308
|
blocker(
|
|
325
309
|
'missing_plan',
|
|
326
310
|
`Phase ${phaseToken} cannot execute because no PLAN artifact exists.`,
|
|
327
|
-
['
|
|
311
|
+
[stateLabel('phases')]
|
|
328
312
|
)
|
|
329
313
|
);
|
|
330
314
|
} else if (pendingPlans.length === 0) {
|
|
@@ -343,7 +327,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
343
327
|
blocker(
|
|
344
328
|
'phase_already_complete',
|
|
345
329
|
`Phase ${phaseToken} is already complete and should not be planned again.`,
|
|
346
|
-
['
|
|
330
|
+
[stateLabel('ROADMAP.md')]
|
|
347
331
|
)
|
|
348
332
|
);
|
|
349
333
|
}
|
|
@@ -354,7 +338,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
354
338
|
blocker(
|
|
355
339
|
'missing_plan',
|
|
356
340
|
`Phase ${phaseToken} cannot be verified because no PLAN artifact exists.`,
|
|
357
|
-
['
|
|
341
|
+
[stateLabel('phases')]
|
|
358
342
|
)
|
|
359
343
|
);
|
|
360
344
|
}
|
|
@@ -363,7 +347,7 @@ function buildPhaseBlockers({ lifecycle, phaseToken, surface }) {
|
|
|
363
347
|
blocker(
|
|
364
348
|
'missing_summary',
|
|
365
349
|
`Phase ${phaseToken} cannot be verified because no SUMMARY artifact exists yet.`,
|
|
366
|
-
['
|
|
350
|
+
[stateLabel('phases')]
|
|
367
351
|
)
|
|
368
352
|
);
|
|
369
353
|
}
|
|
@@ -552,21 +536,21 @@ function buildWorkPhaseBlockers({ workMilestone, phaseToken, surface }) {
|
|
|
552
536
|
return blockers;
|
|
553
537
|
}
|
|
554
538
|
|
|
555
|
-
function buildRoadmapAlignmentBlockers(lifecycle) {
|
|
539
|
+
function buildRoadmapAlignmentBlockers(lifecycle, stateLabel) {
|
|
556
540
|
if (lifecycle.phaseStatusAlignment.mismatches.length === 0) return [];
|
|
557
541
|
return [
|
|
558
542
|
blocker(
|
|
559
543
|
'roadmap_phase_status_mismatch',
|
|
560
544
|
`ROADMAP.md overview/detail phase statuses disagree: ${lifecycle.phaseStatusAlignment.mismatches.join('; ')}`,
|
|
561
|
-
['
|
|
545
|
+
[stateLabel('ROADMAP.md')]
|
|
562
546
|
),
|
|
563
547
|
];
|
|
564
548
|
}
|
|
565
549
|
|
|
566
|
-
function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
550
|
+
function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false, stateLabel } = {}) {
|
|
567
551
|
const blockers = [];
|
|
568
552
|
if (!lifecycle.currentMilestone.version) {
|
|
569
|
-
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')]));
|
|
570
554
|
return blockers;
|
|
571
555
|
}
|
|
572
556
|
|
|
@@ -575,19 +559,19 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
|
575
559
|
blocker(
|
|
576
560
|
allowArchivedBlocker ? 'milestone_already_archived' : 'milestone_already_archived',
|
|
577
561
|
`Milestone ${lifecycle.currentMilestone.version} is already archived-with-ROADMAP.md evidence.`,
|
|
578
|
-
['
|
|
562
|
+
[stateLabel('ROADMAP.md'), stateLabel('MILESTONES.md')]
|
|
579
563
|
)
|
|
580
564
|
);
|
|
581
565
|
}
|
|
582
566
|
|
|
583
567
|
if (lifecycle.counts.total === 0) {
|
|
584
|
-
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')]));
|
|
585
569
|
} else if (lifecycle.counts.completed !== lifecycle.counts.total) {
|
|
586
570
|
blockers.push(
|
|
587
571
|
blocker(
|
|
588
572
|
'incomplete_phases',
|
|
589
573
|
`Milestone ${lifecycle.currentMilestone.version} still has incomplete phases (${lifecycle.counts.completed}/${lifecycle.counts.total} complete).`,
|
|
590
|
-
['
|
|
574
|
+
[stateLabel('ROADMAP.md')]
|
|
591
575
|
)
|
|
592
576
|
);
|
|
593
577
|
}
|
|
@@ -602,7 +586,7 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
|
602
586
|
blocker(
|
|
603
587
|
'missing_verification',
|
|
604
588
|
`Completed phases are missing VERIFICATION artifacts (${phasesMissingVerification.join(', ')}).`,
|
|
605
|
-
['
|
|
589
|
+
[stateLabel('phases')]
|
|
606
590
|
)
|
|
607
591
|
);
|
|
608
592
|
}
|
|
@@ -611,13 +595,14 @@ function buildAuditBlockers(lifecycle, { allowArchivedBlocker = false } = {}) {
|
|
|
611
595
|
}
|
|
612
596
|
|
|
613
597
|
function buildCompletionBlockers(planningDir, lifecycle) {
|
|
598
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
614
599
|
const auditPath = join(planningDir, `${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`);
|
|
615
600
|
if (!existsSync(auditPath)) {
|
|
616
601
|
return [
|
|
617
602
|
blocker(
|
|
618
603
|
'missing_milestone_audit',
|
|
619
604
|
`Milestone ${lifecycle.currentMilestone.version} cannot be completed without a milestone audit artifact.`,
|
|
620
|
-
[
|
|
605
|
+
[stateLabel(`${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`)]
|
|
621
606
|
),
|
|
622
607
|
];
|
|
623
608
|
}
|
|
@@ -630,194 +615,14 @@ function buildCompletionBlockers(planningDir, lifecycle) {
|
|
|
630
615
|
blocker(
|
|
631
616
|
'audit_not_passed',
|
|
632
617
|
`Milestone ${lifecycle.currentMilestone.version} requires a passed audit before completion.`,
|
|
633
|
-
[
|
|
618
|
+
[stateLabel(`${lifecycle.currentMilestone.version}-MILESTONE-AUDIT.md`)]
|
|
634
619
|
),
|
|
635
620
|
];
|
|
636
621
|
}
|
|
637
622
|
|
|
638
|
-
const releaseContractBlockers = buildReleaseClaimCompletionBlockers(auditContent, auditPath);
|
|
639
|
-
if (releaseContractBlockers.length > 0) return releaseContractBlockers;
|
|
640
|
-
|
|
641
623
|
return [];
|
|
642
624
|
}
|
|
643
625
|
|
|
644
|
-
function buildReleaseClaimCompletionBlockers(auditContent, auditPath) {
|
|
645
|
-
const frontmatter = extractFrontmatter(auditContent);
|
|
646
|
-
const deliveryPosture = readTopLevelScalar(frontmatter, 'delivery_posture');
|
|
647
|
-
const releaseClaimPosture = readTopLevelScalar(frontmatter, 'release_claim_posture');
|
|
648
|
-
const evidenceBlock = extractYamlBlock(frontmatter, 'evidence_contract');
|
|
649
|
-
const releaseBlock = extractYamlBlock(frontmatter, 'release_claim_contract');
|
|
650
|
-
const missing = [];
|
|
651
|
-
|
|
652
|
-
if (!deliveryPosture) missing.push('delivery_posture');
|
|
653
|
-
if (!releaseClaimPosture) missing.push('release_claim_posture');
|
|
654
|
-
if (!evidenceBlock) missing.push('evidence_contract');
|
|
655
|
-
if (!releaseBlock) missing.push('release_claim_contract');
|
|
656
|
-
|
|
657
|
-
if (missing.length > 0) {
|
|
658
|
-
return [blocker(
|
|
659
|
-
'missing_release_claim_contract',
|
|
660
|
-
`Milestone audit is missing release closeout metadata (${missing.join(', ')}). Re-run audit before completion.`,
|
|
661
|
-
[auditPath]
|
|
662
|
-
)];
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const requiredKinds = readBlockList(evidenceBlock, 'required_kinds');
|
|
666
|
-
const observedKinds = readBlockList(evidenceBlock, 'observed_kinds');
|
|
667
|
-
const missingKinds = readBlockList(evidenceBlock, 'missing_kinds');
|
|
668
|
-
const unsupportedClaims = readBlockList(releaseBlock, 'unsupported_claims');
|
|
669
|
-
const waivedKinds = readBlockList(releaseBlock, 'waivers');
|
|
670
|
-
const deferrals = readBlockList(releaseBlock, 'deferrals');
|
|
671
|
-
const contradictionChecks = readNestedStatusBlock(releaseBlock, 'contradiction_checks');
|
|
672
|
-
const blockers = [];
|
|
673
|
-
const invalidEvidenceKinds = [
|
|
674
|
-
...findInvalidEvidenceKinds('required_kinds', requiredKinds),
|
|
675
|
-
...findInvalidEvidenceKinds('observed_kinds', observedKinds),
|
|
676
|
-
...findInvalidEvidenceKinds('missing_kinds', missingKinds),
|
|
677
|
-
...findInvalidEvidenceKinds('waivers', waivedKinds),
|
|
678
|
-
];
|
|
679
|
-
|
|
680
|
-
if (requiredKinds.length === 0 && observedKinds.length === 0) {
|
|
681
|
-
blockers.push(blocker(
|
|
682
|
-
'missing_release_evidence_contract',
|
|
683
|
-
'Milestone audit evidence_contract must include required_kinds and observed_kinds before completion.',
|
|
684
|
-
[auditPath]
|
|
685
|
-
));
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (!DELIVERY_POSTURES.includes(deliveryPosture)) {
|
|
689
|
-
blockers.push(blocker(
|
|
690
|
-
'invalid_delivery_posture',
|
|
691
|
-
`Milestone audit has invalid delivery_posture (${deliveryPosture}). Re-run audit before completion.`,
|
|
692
|
-
[auditPath]
|
|
693
|
-
));
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (!RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
|
|
697
|
-
blockers.push(blocker(
|
|
698
|
-
'invalid_release_claim_posture',
|
|
699
|
-
`Milestone audit has invalid release_claim_posture (${releaseClaimPosture}). Re-run audit before completion.`,
|
|
700
|
-
[auditPath]
|
|
701
|
-
));
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (invalidEvidenceKinds.length > 0) {
|
|
705
|
-
blockers.push(blocker(
|
|
706
|
-
'invalid_release_evidence_kinds',
|
|
707
|
-
`Milestone audit has invalid release evidence kind values (${invalidEvidenceKinds.join(', ')}). Supported values are ${EVIDENCE_KINDS.join(', ')}.`,
|
|
708
|
-
[auditPath]
|
|
709
|
-
));
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const missingContradictionChecks = RELEASE_CONTRADICTION_CHECKS.filter((name) => !(name in contradictionChecks));
|
|
713
|
-
const unknownContradictionChecks = Object.keys(contradictionChecks)
|
|
714
|
-
.filter((name) => !RELEASE_CONTRADICTION_CHECKS.includes(name));
|
|
715
|
-
const invalidContradictionChecks = Object.entries(contradictionChecks)
|
|
716
|
-
.filter(([, status]) => !RELEASE_CONTRADICTION_STATUSES.includes(status))
|
|
717
|
-
.map(([name]) => name);
|
|
718
|
-
|
|
719
|
-
if (missingContradictionChecks.length > 0) {
|
|
720
|
-
blockers.push(blocker(
|
|
721
|
-
'missing_release_contradiction_checks',
|
|
722
|
-
`Milestone audit release_claim_contract.contradiction_checks is missing required checks (${missingContradictionChecks.join(', ')}).`,
|
|
723
|
-
[auditPath]
|
|
724
|
-
));
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (invalidContradictionChecks.length > 0) {
|
|
728
|
-
blockers.push(blocker(
|
|
729
|
-
'invalid_release_contradiction_checks',
|
|
730
|
-
`Milestone audit release_claim_contract.contradiction_checks has invalid statuses (${invalidContradictionChecks.join(', ')}).`,
|
|
731
|
-
[auditPath]
|
|
732
|
-
));
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
if (unknownContradictionChecks.length > 0) {
|
|
736
|
-
blockers.push(blocker(
|
|
737
|
-
'unknown_release_contradiction_checks',
|
|
738
|
-
`Milestone audit release_claim_contract.contradiction_checks has unknown checks (${unknownContradictionChecks.join(', ')}). Supported checks are ${RELEASE_CONTRADICTION_CHECKS.join(', ')}.`,
|
|
739
|
-
[auditPath]
|
|
740
|
-
));
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
if (!DELIVERY_POSTURES.includes(deliveryPosture) || !RELEASE_CLAIM_POSTURES.includes(releaseClaimPosture)) {
|
|
744
|
-
return blockers;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const releaseEvaluation = evaluateReleaseClaimCloseoutContract({
|
|
748
|
-
surface: 'complete-milestone',
|
|
749
|
-
deliveryPosture,
|
|
750
|
-
releaseClaimPosture,
|
|
751
|
-
observedKinds,
|
|
752
|
-
waivedKinds,
|
|
753
|
-
unsupportedClaims,
|
|
754
|
-
deferrals,
|
|
755
|
-
contradictionChecks,
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
if (DELIVERY_POSTURES.includes(deliveryPosture)) {
|
|
759
|
-
const evidenceContract = getEvidenceContract('complete-milestone', deliveryPosture);
|
|
760
|
-
const enforcedRequiredKinds = [...new Set([...evidenceContract.requiredKinds, ...releaseEvaluation.requiredKinds])];
|
|
761
|
-
const undeclaredRequiredKinds = enforcedRequiredKinds.filter((kind) => !requiredKinds.includes(kind));
|
|
762
|
-
const recomputedMissingKinds = enforcedRequiredKinds.filter((kind) => !observedKinds.includes(kind));
|
|
763
|
-
|
|
764
|
-
if (undeclaredRequiredKinds.length > 0) {
|
|
765
|
-
blockers.push(blocker(
|
|
766
|
-
'invalid_release_evidence_contract',
|
|
767
|
-
`Milestone audit evidence_contract.required_kinds omits required closeout evidence (${undeclaredRequiredKinds.join(', ')}).`,
|
|
768
|
-
[auditPath]
|
|
769
|
-
));
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (recomputedMissingKinds.length > 0) {
|
|
773
|
-
blockers.push(blocker(
|
|
774
|
-
'missing_required_release_evidence',
|
|
775
|
-
`Milestone audit observed evidence is missing required closeout kinds (${recomputedMissingKinds.join(', ')}).`,
|
|
776
|
-
[auditPath]
|
|
777
|
-
));
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (missingKinds.length > 0) {
|
|
782
|
-
blockers.push(blocker(
|
|
783
|
-
'missing_required_release_evidence',
|
|
784
|
-
`Milestone audit is missing required evidence kinds for closeout (${missingKinds.join(', ')}).`,
|
|
785
|
-
[auditPath]
|
|
786
|
-
));
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
if (releaseEvaluation.invalidWaivers.length > 0) {
|
|
790
|
-
blockers.push(blocker(
|
|
791
|
-
'invalid_release_waivers',
|
|
792
|
-
`Milestone audit has invalid waivers for missing required evidence (${releaseEvaluation.invalidWaivers.join(', ')}).`,
|
|
793
|
-
[auditPath]
|
|
794
|
-
));
|
|
795
|
-
}
|
|
796
|
-
if (releaseEvaluation.blockers.some((releaseBlocker) => releaseBlocker.code === 'incompatible_release_claim_posture')) {
|
|
797
|
-
blockers.push(blocker(
|
|
798
|
-
'incompatible_release_claim_posture',
|
|
799
|
-
`Milestone audit release_claim_posture (${releaseClaimPosture}) is incompatible with delivery_posture (${deliveryPosture}).`,
|
|
800
|
-
[auditPath]
|
|
801
|
-
));
|
|
802
|
-
}
|
|
803
|
-
if (releaseEvaluation.unresolvedUnsupportedClaims.length > 0) {
|
|
804
|
-
blockers.push(blocker(
|
|
805
|
-
'unsupported_release_claims',
|
|
806
|
-
`Milestone audit has unsupported release claims without downgrade or deferral (${releaseEvaluation.unresolvedUnsupportedClaims.join(', ')}).`,
|
|
807
|
-
[auditPath]
|
|
808
|
-
));
|
|
809
|
-
}
|
|
810
|
-
if (releaseEvaluation.failedContradictionChecks.length > 0) {
|
|
811
|
-
blockers.push(blocker(
|
|
812
|
-
'failed_release_contradiction_checks',
|
|
813
|
-
`Milestone audit has failed release contradiction checks (${releaseEvaluation.failedContradictionChecks.join(', ')}).`,
|
|
814
|
-
[auditPath]
|
|
815
|
-
));
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
return blockers;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
626
|
function extractFrontmatter(content) {
|
|
822
627
|
const match = String(content || '').replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/);
|
|
823
628
|
return match ? match[1] : '';
|
|
@@ -828,136 +633,6 @@ function readTopLevelScalar(frontmatter, key) {
|
|
|
828
633
|
return match ? cleanYamlValue(match[1]) : null;
|
|
829
634
|
}
|
|
830
635
|
|
|
831
|
-
function extractYamlBlock(frontmatter, key) {
|
|
832
|
-
const lines = String(frontmatter || '').replace(/\r\n/g, '\n').split('\n');
|
|
833
|
-
const startIndex = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line.trim()));
|
|
834
|
-
if (startIndex === -1) return '';
|
|
835
|
-
|
|
836
|
-
const collected = [];
|
|
837
|
-
for (const line of lines.slice(startIndex + 1)) {
|
|
838
|
-
if (/^[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
839
|
-
collected.push(line);
|
|
840
|
-
}
|
|
841
|
-
return collected.join('\n');
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function readBlockList(block, key) {
|
|
845
|
-
const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
|
|
846
|
-
const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
|
|
847
|
-
if (startIndex === -1) return [];
|
|
848
|
-
const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
|
|
849
|
-
|
|
850
|
-
const inline = lines[startIndex].match(/^\s+[^:]+:\s*\[([^\]]*)\]/);
|
|
851
|
-
if (inline) return splitInlineList(inline[1]);
|
|
852
|
-
|
|
853
|
-
const collected = [];
|
|
854
|
-
for (const line of lines.slice(startIndex + 1)) {
|
|
855
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
856
|
-
if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
857
|
-
collected.push(line);
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
return parseYamlListItems(collected, baseIndent)
|
|
861
|
-
.map((item) => item.join(' '))
|
|
862
|
-
.map(cleanYamlValue);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function parseYamlListItems(lines, baseIndent) {
|
|
866
|
-
const items = [];
|
|
867
|
-
let current = null;
|
|
868
|
-
let itemIndent = null;
|
|
869
|
-
|
|
870
|
-
for (const line of lines) {
|
|
871
|
-
const match = line.match(/^(\s*)-\s*(.+?)\s*$/);
|
|
872
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
873
|
-
|
|
874
|
-
if (match && indent > baseIndent && (itemIndent === null || indent === itemIndent)) {
|
|
875
|
-
if (current) items.push(current);
|
|
876
|
-
current = [match[2]];
|
|
877
|
-
itemIndent = indent;
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if (current && line.trim()) {
|
|
882
|
-
current.push(line.trim());
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (current) items.push(current);
|
|
887
|
-
return items;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function findInvalidEvidenceKinds(field, kinds) {
|
|
891
|
-
return kinds
|
|
892
|
-
.filter((kind) => !EVIDENCE_KINDS.includes(kind))
|
|
893
|
-
.map((kind) => `${field}: ${kind}`);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function readNestedStatusBlock(block, key) {
|
|
897
|
-
const nested = extractIndentedBlock(block, key);
|
|
898
|
-
const statuses = {};
|
|
899
|
-
for (const line of nested.split('\n')) {
|
|
900
|
-
const match = line.match(/^\s+([A-Za-z0-9_-]+):\s*(.+)$/);
|
|
901
|
-
if (match) statuses[match[1]] = cleanYamlValue(match[2]);
|
|
902
|
-
}
|
|
903
|
-
return statuses;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function extractIndentedBlock(block, key) {
|
|
907
|
-
const lines = String(block || '').replace(/\r\n/g, '\n').split('\n');
|
|
908
|
-
const startIndex = lines.findIndex((line) => new RegExp(`^\\s+${key}:`).test(line));
|
|
909
|
-
if (startIndex === -1) return '';
|
|
910
|
-
const baseIndent = lines[startIndex].match(/^\s*/)[0].length;
|
|
911
|
-
|
|
912
|
-
const collected = [];
|
|
913
|
-
for (const line of lines.slice(startIndex + 1)) {
|
|
914
|
-
const indent = line.match(/^\s*/)[0].length;
|
|
915
|
-
if (line.trim() && indent <= baseIndent && /^\s*[A-Za-z0-9_-]+:\s*/.test(line)) break;
|
|
916
|
-
collected.push(line);
|
|
917
|
-
}
|
|
918
|
-
return collected.join('\n');
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function splitInlineList(value) {
|
|
922
|
-
return splitCommaAware(value)
|
|
923
|
-
.map(cleanYamlValue)
|
|
924
|
-
.filter(Boolean);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function splitCommaAware(value) {
|
|
928
|
-
const items = [];
|
|
929
|
-
let current = '';
|
|
930
|
-
let quote = null;
|
|
931
|
-
let escaped = false;
|
|
932
|
-
|
|
933
|
-
for (const char of String(value || '')) {
|
|
934
|
-
if (escaped) {
|
|
935
|
-
current += char;
|
|
936
|
-
escaped = false;
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
if (char === '\\' && quote) {
|
|
940
|
-
current += char;
|
|
941
|
-
escaped = true;
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
if ((char === '"' || char === "'") && (!quote || quote === char)) {
|
|
945
|
-
quote = quote ? null : char;
|
|
946
|
-
current += char;
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
if (char === ',' && !quote) {
|
|
950
|
-
items.push(current);
|
|
951
|
-
current = '';
|
|
952
|
-
continue;
|
|
953
|
-
}
|
|
954
|
-
current += char;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
items.push(current);
|
|
958
|
-
return items;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
636
|
function cleanYamlValue(value) {
|
|
962
637
|
return stripInlineYamlComment(String(value || ''))
|
|
963
638
|
.trim()
|
|
@@ -1000,6 +675,17 @@ function blocker(code, message, artifacts) {
|
|
|
1000
675
|
return { code, message, artifacts };
|
|
1001
676
|
}
|
|
1002
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
|
+
|
|
1003
689
|
export function cmdLifecyclePreflight(...args) {
|
|
1004
690
|
const { args: normalizedArgs, planningDir, invalid, error } = resolveWorkspaceContext(args);
|
|
1005
691
|
if (invalid) {
|
|
@@ -1008,9 +694,10 @@ export function cmdLifecyclePreflight(...args) {
|
|
|
1008
694
|
return;
|
|
1009
695
|
}
|
|
1010
696
|
const [surface, maybePhase, ...rest] = normalizedArgs;
|
|
697
|
+
const stateLabel = createStateLabeler(planningDir);
|
|
1011
698
|
|
|
1012
699
|
if (!surface) {
|
|
1013
|
-
console.error(
|
|
700
|
+
console.error(`Usage: node ${stateLabel('bin', 'gsdd.mjs')} lifecycle-preflight <surface> [phase] [--expects-mutation <none|phase-status>]`);
|
|
1014
701
|
process.exitCode = 1;
|
|
1015
702
|
return;
|
|
1016
703
|
}
|