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