@xenonbyte/da-vinci-workflow 0.2.8 → 0.2.9
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/CHANGELOG.md +14 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/lib/async-offload.js +39 -2
- package/lib/cli/command-handlers-core.js +132 -0
- package/lib/cli/command-handlers-design.js +129 -0
- package/lib/cli/command-handlers-pen.js +231 -0
- package/lib/cli/command-handlers-workflow.js +221 -0
- package/lib/cli/command-handlers.js +49 -0
- package/lib/cli/helpers.js +62 -0
- package/lib/cli.js +98 -542
- package/lib/execution-signals.js +33 -0
- package/lib/fs-safety.js +1 -12
- package/lib/path-inside.js +17 -0
- package/lib/utils.js +2 -7
- package/lib/workflow-base-view.js +134 -0
- package/lib/workflow-overlay.js +1033 -0
- package/lib/workflow-persisted-state.js +4 -0
- package/lib/workflow-stage.js +244 -0
- package/lib/workflow-state.js +359 -1998
- package/lib/workflow-task-groups.js +881 -0
- package/lib/worktree-preflight.js +31 -11
- package/package.json +1 -1
package/lib/workflow-state.js
CHANGED
|
@@ -1,46 +1,41 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
1
|
const path = require("path");
|
|
3
2
|
const { auditProject } = require("./audit");
|
|
4
|
-
const {
|
|
5
|
-
parseCheckpointStatusMap,
|
|
6
|
-
normalizeCheckpointLabel,
|
|
7
|
-
parseDisciplineMarkers,
|
|
8
|
-
DISCIPLINE_MARKER_NAMES
|
|
9
|
-
} = require("./audit-parsers");
|
|
10
3
|
const { pathExists, readTextIfExists, dedupeMessages } = require("./utils");
|
|
11
4
|
const {
|
|
12
|
-
parseTasksArtifact,
|
|
13
5
|
listImmediateDirs,
|
|
14
6
|
hasAnyFile,
|
|
15
7
|
pickLatestChange,
|
|
16
8
|
detectSpecFiles
|
|
17
9
|
} = require("./planning-parsers");
|
|
10
|
+
const { STATUS, getStageById } = require("./workflow-contract");
|
|
11
|
+
const { readCheckpointStatuses } = require("./workflow-stage");
|
|
12
|
+
const {
|
|
13
|
+
buildPersistedWorkflowBaseView,
|
|
14
|
+
buildDerivedWorkflowBaseView
|
|
15
|
+
} = require("./workflow-base-view");
|
|
18
16
|
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} = require("./workflow-
|
|
17
|
+
deriveTaskGroupMetadata,
|
|
18
|
+
deriveTaskGroupRuntimeState,
|
|
19
|
+
buildTaskGroupMetadataPayload,
|
|
20
|
+
resolvePersistedTaskGroupSeed,
|
|
21
|
+
selectFocusedTaskGroup
|
|
22
|
+
} = require("./workflow-task-groups");
|
|
25
23
|
const {
|
|
26
24
|
selectPersistedStateForChange,
|
|
27
25
|
persistDerivedWorkflowResult,
|
|
28
|
-
resolveWorkflowStatePath,
|
|
29
|
-
resolveTaskGroupMetadataPath,
|
|
30
26
|
writeTaskGroupMetadata,
|
|
31
|
-
readTaskGroupMetadata,
|
|
32
|
-
digestForPath,
|
|
33
27
|
sanitizePersistedNotes
|
|
34
28
|
} = require("./workflow-persisted-state");
|
|
35
29
|
const {
|
|
36
30
|
readExecutionSignals,
|
|
37
|
-
summarizeSignalsBySurface
|
|
38
|
-
listSignalsBySurfacePrefix
|
|
31
|
+
summarizeSignalsBySurface
|
|
39
32
|
} = require("./execution-signals");
|
|
40
|
-
const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness");
|
|
41
|
-
const { deriveExecutionProfile } = require("./execution-profile");
|
|
42
33
|
const { collectVerificationFreshness } = require("./verify");
|
|
43
|
-
const {
|
|
34
|
+
const {
|
|
35
|
+
collectPlanningSignalFreshnessState,
|
|
36
|
+
inspectDisciplineState,
|
|
37
|
+
applyWorkflowOverlays
|
|
38
|
+
} = require("./workflow-overlay");
|
|
44
39
|
const {
|
|
45
40
|
formatPathRef,
|
|
46
41
|
emitWorkflowDecisionTraces,
|
|
@@ -48,18 +43,6 @@ const {
|
|
|
48
43
|
} = require("./workflow-decision-trace");
|
|
49
44
|
|
|
50
45
|
const MAX_REPORTED_MESSAGES = 3;
|
|
51
|
-
// Task-group metadata is versioned independently from workflow route snapshots.
|
|
52
|
-
const TASK_GROUP_METADATA_VERSION = 2;
|
|
53
|
-
const TRACEABLE_TASK_GROUP_FOCUS_REASONS = new Set([
|
|
54
|
-
"implementer_block",
|
|
55
|
-
"implementer_warn",
|
|
56
|
-
"spec_review_missing",
|
|
57
|
-
"spec_review_block",
|
|
58
|
-
"spec_review_warn",
|
|
59
|
-
"quality_review_missing",
|
|
60
|
-
"quality_review_block",
|
|
61
|
-
"quality_review_warn"
|
|
62
|
-
]);
|
|
63
46
|
const PERSISTED_STATE_TRACE_KEYS = Object.freeze({
|
|
64
47
|
missing: "fallback_missing",
|
|
65
48
|
"parse-error": "fallback_parse_error",
|
|
@@ -67,29 +50,14 @@ const PERSISTED_STATE_TRACE_KEYS = Object.freeze({
|
|
|
67
50
|
"change-missing": "fallback_change_missing",
|
|
68
51
|
"fingerprint-mismatch": "fallback_fingerprint_mismatch"
|
|
69
52
|
});
|
|
70
|
-
const TASK_GROUP_SEED_TRACE_KEYS = Object.freeze({
|
|
71
|
-
missing: "seed_missing",
|
|
72
|
-
unreadable: "seed_unreadable",
|
|
73
|
-
"digest-mismatch": "seed_digest_mismatch",
|
|
74
|
-
legacy: "seed_legacy_embedded"
|
|
75
|
-
});
|
|
76
|
-
const BLOCKING_GATE_PRIORITY = Object.freeze([
|
|
77
|
-
"clarify",
|
|
78
|
-
"scenarioQuality",
|
|
79
|
-
"analyze",
|
|
80
|
-
"taskCheckpoint",
|
|
81
|
-
"stalePlanningSignal",
|
|
82
|
-
"principleInheritance",
|
|
83
|
-
"lint-tasks",
|
|
84
|
-
"lint-spec",
|
|
85
|
-
"scope-check"
|
|
86
|
-
]);
|
|
87
|
-
const PLANNING_SIGNAL_PROMOTION_FALLBACKS = Object.freeze({
|
|
88
|
-
"lint-spec": "breakdown",
|
|
89
|
-
"scope-check": "tasks",
|
|
90
|
-
"lint-tasks": "tasks"
|
|
91
|
-
});
|
|
92
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Append a workflow decision-trace record when tracing is enabled for the current run.
|
|
56
|
+
*
|
|
57
|
+
* @param {Array<object> | null} records
|
|
58
|
+
* @param {object} record
|
|
59
|
+
* @returns {void}
|
|
60
|
+
*/
|
|
93
61
|
function recordWorkflowDecision(records, record) {
|
|
94
62
|
if (!Array.isArray(records) || !record || typeof record !== "object") {
|
|
95
63
|
return;
|
|
@@ -97,59 +65,13 @@ function recordWorkflowDecision(records, record) {
|
|
|
97
65
|
records.push(record);
|
|
98
66
|
}
|
|
99
67
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
) {
|
|
108
|
-
return [`signal:task-review.${taskGroupId}.spec`];
|
|
109
|
-
}
|
|
110
|
-
if (
|
|
111
|
-
reason === "quality_review_block" ||
|
|
112
|
-
reason === "quality_review_warn"
|
|
113
|
-
) {
|
|
114
|
-
return [`signal:task-review.${taskGroupId}.quality`];
|
|
115
|
-
}
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function buildTaskGroupFocusReasonSummary(taskGroupId, reason) {
|
|
120
|
-
switch (reason) {
|
|
121
|
-
case "implementer_block":
|
|
122
|
-
return `Implementer BLOCK overrides planned checklist focus for task group ${taskGroupId}.`;
|
|
123
|
-
case "implementer_warn":
|
|
124
|
-
return `Implementer WARN overrides planned checklist focus for task group ${taskGroupId}.`;
|
|
125
|
-
case "spec_review_missing":
|
|
126
|
-
return `Missing spec review takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
127
|
-
case "spec_review_block":
|
|
128
|
-
return `Spec review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
129
|
-
case "spec_review_warn":
|
|
130
|
-
return `Spec review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
131
|
-
case "quality_review_missing":
|
|
132
|
-
return `Missing quality review takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
133
|
-
case "quality_review_block":
|
|
134
|
-
return `Quality review BLOCK takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
135
|
-
case "quality_review_warn":
|
|
136
|
-
return `Quality review WARN follow-up takes focus over planned checklist work for task group ${taskGroupId}.`;
|
|
137
|
-
default:
|
|
138
|
-
return "";
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function collectVerificationFreshnessEvidenceRefs(verificationFreshness) {
|
|
143
|
-
const surfaces =
|
|
144
|
-
verificationFreshness && verificationFreshness.surfaces && typeof verificationFreshness.surfaces === "object"
|
|
145
|
-
? verificationFreshness.surfaces
|
|
146
|
-
: {};
|
|
147
|
-
return Object.keys(surfaces)
|
|
148
|
-
.filter((surface) => surfaces[surface] && surfaces[surface].stale && surfaces[surface].present)
|
|
149
|
-
.sort()
|
|
150
|
-
.map((surface) => `signal:${surface}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Flush decision-trace records and attach non-fatal trace diagnostics to the workflow result.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} result
|
|
72
|
+
* @param {{ env?: object, surface?: string, projectRoot?: string, records?: Array<object> | null }} [options]
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
153
75
|
function finalizeResultWithWorkflowDecisionTracing(result, options = {}) {
|
|
154
76
|
const traceResult = emitWorkflowDecisionTraces({
|
|
155
77
|
env: options.env,
|
|
@@ -177,1866 +99,298 @@ function finalizeResultWithWorkflowDecisionTracing(result, options = {}) {
|
|
|
177
99
|
? traceResult.error.message
|
|
178
100
|
: traceResult.error
|
|
179
101
|
? String(traceResult.error)
|
|
180
|
-
: null
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function summarizeAudit(result) {
|
|
187
|
-
if (!result) {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
status: result.status,
|
|
193
|
-
failures: Array.isArray(result.failures) ? result.failures.slice(0, MAX_REPORTED_MESSAGES) : [],
|
|
194
|
-
warnings: Array.isArray(result.warnings) ? result.warnings.slice(0, MAX_REPORTED_MESSAGES) : [],
|
|
195
|
-
notes: Array.isArray(result.notes) ? result.notes.slice(0, MAX_REPORTED_MESSAGES) : []
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function dedupeFindings(findings) {
|
|
200
|
-
findings.blockers = dedupeMessages(findings.blockers);
|
|
201
|
-
findings.warnings = dedupeMessages(findings.warnings);
|
|
202
|
-
findings.notes = dedupeMessages(findings.notes);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function resolveDisciplineGateMode() {
|
|
206
|
-
const strictEnv = String(process.env.DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL || "").trim();
|
|
207
|
-
const strict = strictEnv === "1" || /^true$/i.test(strictEnv);
|
|
208
|
-
return {
|
|
209
|
-
strict
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function isStrictPromotionEnabled() {
|
|
214
|
-
const raw = String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim();
|
|
215
|
-
return raw === "1" || /^true$/i.test(raw);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function fallbackStageIfBeyond(currentStageId, targetStageId) {
|
|
219
|
-
const current = getStageById(currentStageId);
|
|
220
|
-
const target = getStageById(targetStageId);
|
|
221
|
-
if (!current || !target) {
|
|
222
|
-
return currentStageId;
|
|
223
|
-
}
|
|
224
|
-
if (current.order > target.order) {
|
|
225
|
-
return target.id;
|
|
226
|
-
}
|
|
227
|
-
return current.id;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function ensureGateTracking(findings) {
|
|
231
|
-
if (!Array.isArray(findings.blockingGates)) {
|
|
232
|
-
findings.blockingGates = [];
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function collectGateEvidenceRefs(gate) {
|
|
237
|
-
return Array.isArray(gate && gate.evidence) ? gate.evidence.slice(0, MAX_REPORTED_MESSAGES) : [];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function addBlockingGateRecord(findings, gateId, surface, gate, reason) {
|
|
241
|
-
ensureGateTracking(findings);
|
|
242
|
-
findings.blockingGates.push({
|
|
243
|
-
id: gateId,
|
|
244
|
-
surface,
|
|
245
|
-
reason: String(reason || "").trim(),
|
|
246
|
-
evidence: collectGateEvidenceRefs(gate)
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function collectPlanningSignalFreshnessState(projectRoot, changeId, signalSummary) {
|
|
251
|
-
const effectiveSignalSummary =
|
|
252
|
-
signalSummary && typeof signalSummary === "object"
|
|
253
|
-
? { ...signalSummary }
|
|
254
|
-
: {};
|
|
255
|
-
const stalePlanningSignals = {};
|
|
256
|
-
|
|
257
|
-
if (!changeId) {
|
|
258
|
-
return {
|
|
259
|
-
effectiveSignalSummary,
|
|
260
|
-
stalePlanningSignals,
|
|
261
|
-
needsRerunSurfaces: []
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
for (const surface of Object.keys(PLANNING_SIGNAL_PROMOTION_FALLBACKS)) {
|
|
266
|
-
const signal = effectiveSignalSummary[surface];
|
|
267
|
-
if (!signal) {
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
const freshness = evaluatePlanningSignalFreshness(projectRoot, {
|
|
271
|
-
changeId,
|
|
272
|
-
surface,
|
|
273
|
-
signal
|
|
274
|
-
});
|
|
275
|
-
if (!freshness.applicable || freshness.fresh) {
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
stalePlanningSignals[surface] = {
|
|
279
|
-
surface,
|
|
280
|
-
reasons: Array.isArray(freshness.reasons) ? freshness.reasons.slice() : [],
|
|
281
|
-
signalStatus: normalizeSignalStatus(signal.status),
|
|
282
|
-
signalTimestamp: signal && signal.timestamp ? String(signal.timestamp) : null,
|
|
283
|
-
signalTimestampMs: freshness.signalTimestampMs,
|
|
284
|
-
latestArtifactMtimeMs: freshness.latestArtifactMtimeMs,
|
|
285
|
-
latestArtifactTimestamp:
|
|
286
|
-
freshness.latestArtifactMtimeMs > 0 ? new Date(freshness.latestArtifactMtimeMs).toISOString() : null,
|
|
287
|
-
staleByMs: freshness.staleByMs
|
|
288
|
-
};
|
|
289
|
-
delete effectiveSignalSummary[surface];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
effectiveSignalSummary,
|
|
294
|
-
stalePlanningSignals,
|
|
295
|
-
needsRerunSurfaces: Object.keys(stalePlanningSignals).sort()
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function applyPlanningSignalFreshnessFindings(stageId, findings, planningSignalFreshness, decisionTraceRecords) {
|
|
300
|
-
let nextStageId = stageId;
|
|
301
|
-
const strictPromotion = isStrictPromotionEnabled();
|
|
302
|
-
const stalePlanningSignals =
|
|
303
|
-
planningSignalFreshness && planningSignalFreshness.stalePlanningSignals
|
|
304
|
-
? planningSignalFreshness.stalePlanningSignals
|
|
305
|
-
: {};
|
|
306
|
-
|
|
307
|
-
for (const surface of Object.keys(stalePlanningSignals).sort()) {
|
|
308
|
-
const freshness = stalePlanningSignals[surface];
|
|
309
|
-
const reasonText =
|
|
310
|
-
Array.isArray(freshness.reasons) && freshness.reasons.length > 0
|
|
311
|
-
? freshness.reasons.join(", ")
|
|
312
|
-
: "unknown_staleness_reason";
|
|
313
|
-
findings.warnings.push(
|
|
314
|
-
`Stale ${surface} planning signal requires rerun before routing can rely on it (${reasonText}).`
|
|
315
|
-
);
|
|
316
|
-
if (!strictPromotion) {
|
|
317
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
318
|
-
decisionFamily: "planning_signal_freshness",
|
|
319
|
-
decisionKey: "stale_signal_rerun_required",
|
|
320
|
-
outcome: "rerun_required",
|
|
321
|
-
reasonSummary: `Stale ${surface} planning signal requires rerun before routing can rely on it.`,
|
|
322
|
-
context: {
|
|
323
|
-
planningSurface: surface,
|
|
324
|
-
strictPromotion: false,
|
|
325
|
-
signalStatus: freshness.signalStatus || null,
|
|
326
|
-
staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
|
|
327
|
-
reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
|
|
328
|
-
},
|
|
329
|
-
evidenceRefs: [`signal:${surface}`]
|
|
330
|
-
});
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
findings.blockers.push(
|
|
334
|
-
`DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; stale ${surface} planning signal blocks promotion until ${surface} is rerun.`
|
|
335
|
-
);
|
|
336
|
-
addBlockingGateRecord(
|
|
337
|
-
findings,
|
|
338
|
-
"stalePlanningSignal",
|
|
339
|
-
surface,
|
|
340
|
-
null,
|
|
341
|
-
`strict promotion requires rerun for stale ${surface} planning signal`
|
|
342
|
-
);
|
|
343
|
-
const fallbackStageId = PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId;
|
|
344
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
345
|
-
decisionFamily: "planning_signal_freshness",
|
|
346
|
-
decisionKey: "stale_signal_strict_fallback",
|
|
347
|
-
outcome: "downgraded",
|
|
348
|
-
reasonSummary: `Strict promotion forces routing fallback because ${surface} planning signal is stale.`,
|
|
349
|
-
context: {
|
|
350
|
-
planningSurface: surface,
|
|
351
|
-
strictPromotion: true,
|
|
352
|
-
signalStatus: freshness.signalStatus || null,
|
|
353
|
-
fallbackStage: fallbackStageId,
|
|
354
|
-
staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
|
|
355
|
-
reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
|
|
356
|
-
},
|
|
357
|
-
evidenceRefs: [`signal:${surface}`]
|
|
358
|
-
});
|
|
359
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, fallbackStageId);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return nextStageId;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function selectBlockingGateIdentity(findings) {
|
|
366
|
-
const candidates = Array.isArray(findings && findings.blockingGates) ? findings.blockingGates : [];
|
|
367
|
-
if (candidates.length === 0) {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
const sorted = candidates
|
|
371
|
-
.slice()
|
|
372
|
-
.sort((left, right) => {
|
|
373
|
-
const leftPriority = BLOCKING_GATE_PRIORITY.indexOf(left.id);
|
|
374
|
-
const rightPriority = BLOCKING_GATE_PRIORITY.indexOf(right.id);
|
|
375
|
-
const normalizedLeft = leftPriority === -1 ? Number.MAX_SAFE_INTEGER : leftPriority;
|
|
376
|
-
const normalizedRight = rightPriority === -1 ? Number.MAX_SAFE_INTEGER : rightPriority;
|
|
377
|
-
if (normalizedLeft !== normalizedRight) {
|
|
378
|
-
return normalizedLeft - normalizedRight;
|
|
379
|
-
}
|
|
380
|
-
return String(left.surface || "").localeCompare(String(right.surface || ""));
|
|
381
|
-
});
|
|
382
|
-
return sorted[0];
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function getSignalGate(signal, gateKey) {
|
|
386
|
-
if (!signal || !signal.details || !signal.details.gates) {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
const gates = signal.details.gates;
|
|
390
|
-
if (!gates || typeof gates !== "object") {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
if (!gateKey) {
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
return gates[gateKey] && typeof gates[gateKey] === "object" ? gates[gateKey] : null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function normalizeSignalStatus(status) {
|
|
400
|
-
const normalized = String(status || "").trim().toUpperCase();
|
|
401
|
-
if (normalized === STATUS.BLOCK || normalized === STATUS.WARN || normalized === STATUS.PASS) {
|
|
402
|
-
return normalized;
|
|
403
|
-
}
|
|
404
|
-
return "";
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function statusSeverity(status) {
|
|
408
|
-
if (status === STATUS.BLOCK) {
|
|
409
|
-
return 2;
|
|
410
|
-
}
|
|
411
|
-
if (status === STATUS.WARN) {
|
|
412
|
-
return 1;
|
|
413
|
-
}
|
|
414
|
-
if (status === STATUS.PASS) {
|
|
415
|
-
return 0;
|
|
416
|
-
}
|
|
417
|
-
return -1;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function clampGateStatusBySignal(gateStatus, signalStatus) {
|
|
421
|
-
const normalizedGate = normalizeSignalStatus(gateStatus);
|
|
422
|
-
const normalizedSignal = normalizeSignalStatus(signalStatus);
|
|
423
|
-
if (!normalizedGate && normalizedSignal) {
|
|
424
|
-
return normalizedSignal;
|
|
425
|
-
}
|
|
426
|
-
if (normalizedGate && !normalizedSignal) {
|
|
427
|
-
return normalizedGate;
|
|
428
|
-
}
|
|
429
|
-
if (!normalizedGate && !normalizedSignal) {
|
|
430
|
-
return STATUS.PASS;
|
|
431
|
-
}
|
|
432
|
-
return statusSeverity(normalizedGate) >= statusSeverity(normalizedSignal)
|
|
433
|
-
? normalizedGate
|
|
434
|
-
: normalizedSignal;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function resolveEffectiveGateStatus(gate, signal) {
|
|
438
|
-
const gateStatus = normalizeSignalStatus(gate && gate.status);
|
|
439
|
-
if (gateStatus) {
|
|
440
|
-
return gateStatus;
|
|
441
|
-
}
|
|
442
|
-
const signalStatus = normalizeSignalStatus(signal && signal.status);
|
|
443
|
-
return signalStatus || STATUS.PASS;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function statusTokenMatches(status, acceptedTokens) {
|
|
447
|
-
const normalized = String(status || "").trim().toUpperCase();
|
|
448
|
-
return acceptedTokens.includes(normalized);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function inspectDisciplineState(changeDir) {
|
|
452
|
-
const designPath = path.join(changeDir, "design.md");
|
|
453
|
-
const pencilDesignPath = path.join(changeDir, "pencil-design.md");
|
|
454
|
-
const pencilBindingsPath = path.join(changeDir, "pencil-bindings.md");
|
|
455
|
-
const tasksPath = path.join(changeDir, "tasks.md");
|
|
456
|
-
const designText = readTextIfExists(designPath);
|
|
457
|
-
const tasksText = readTextIfExists(tasksPath);
|
|
458
|
-
const designMarkers = parseDisciplineMarkers(designText);
|
|
459
|
-
const taskMarkers = parseDisciplineMarkers(tasksText);
|
|
460
|
-
const gateMode = resolveDisciplineGateMode();
|
|
461
|
-
const hasAnyMarker =
|
|
462
|
-
designMarkers.ordered.length > 0 ||
|
|
463
|
-
taskMarkers.ordered.length > 0 ||
|
|
464
|
-
designMarkers.malformed.length > 0 ||
|
|
465
|
-
taskMarkers.malformed.length > 0;
|
|
466
|
-
|
|
467
|
-
const designApproval =
|
|
468
|
-
designMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
|
|
469
|
-
taskMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
|
|
470
|
-
null;
|
|
471
|
-
const planSelfReview = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.planSelfReview] || null;
|
|
472
|
-
const operatorReviewAck = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.operatorReviewAck] || null;
|
|
473
|
-
|
|
474
|
-
const blockers = [];
|
|
475
|
-
const warnings = [];
|
|
476
|
-
const notes = [];
|
|
477
|
-
const designApprovalArtifacts = [
|
|
478
|
-
{ label: "design.md", path: designPath },
|
|
479
|
-
{ label: "pencil-design.md", path: pencilDesignPath },
|
|
480
|
-
{ label: "pencil-bindings.md", path: pencilBindingsPath }
|
|
481
|
-
];
|
|
482
|
-
|
|
483
|
-
for (const malformed of [...designMarkers.malformed, ...taskMarkers.malformed]) {
|
|
484
|
-
warnings.push(`Malformed discipline marker at line ${malformed.line}: ${malformed.reason}`);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
let designApprovalState = "missing";
|
|
488
|
-
let designApprovalStale = false;
|
|
489
|
-
if (designApproval) {
|
|
490
|
-
if (!statusTokenMatches(designApproval.status, ["APPROVED", "PASS", "ACCEPTED"])) {
|
|
491
|
-
designApprovalState = "rejected";
|
|
492
|
-
blockers.push(
|
|
493
|
-
`Design approval marker is not approved (${designApproval.status}); keep workflow in tasks/design.`
|
|
494
|
-
);
|
|
495
|
-
} else {
|
|
496
|
-
designApprovalState = "approved";
|
|
497
|
-
if (designApproval.time) {
|
|
498
|
-
const approvalMs = Date.parse(designApproval.time);
|
|
499
|
-
const staleArtifacts = [];
|
|
500
|
-
if (Number.isFinite(approvalMs)) {
|
|
501
|
-
for (const artifact of designApprovalArtifacts) {
|
|
502
|
-
let artifactMtimeMs = 0;
|
|
503
|
-
try {
|
|
504
|
-
artifactMtimeMs = fs.statSync(artifact.path).mtimeMs;
|
|
505
|
-
} catch (_error) {
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
if (artifactMtimeMs > approvalMs) {
|
|
509
|
-
staleArtifacts.push(artifact.label);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
if (staleArtifacts.length > 0) {
|
|
514
|
-
designApprovalState = "stale";
|
|
515
|
-
designApprovalStale = true;
|
|
516
|
-
blockers.push(
|
|
517
|
-
`Design approval marker is stale because design artifacts changed after approval timestamp: ${staleArtifacts.join(", ")}.`
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
} else {
|
|
521
|
-
warnings.push("Design approval marker is missing timestamp; staleness checks are limited.");
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
} else if (hasAnyMarker || gateMode.strict) {
|
|
525
|
-
blockers.push(
|
|
526
|
-
"Missing required design approval marker (`- design approval: APPROVED @ <ISO8601>`) for disciplined handoff."
|
|
527
|
-
);
|
|
528
|
-
} else {
|
|
529
|
-
warnings.push(
|
|
530
|
-
"Design approval marker is missing; legacy compatibility mode keeps this advisory. Set DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL=1 to enforce."
|
|
531
|
-
);
|
|
532
|
-
notes.push("Legacy compatibility mode: missing discipline markers are advisory.");
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (!planSelfReview) {
|
|
536
|
-
warnings.push("Missing plan self-review marker in tasks.md (`- plan self review: PASS @ <ISO8601>`).");
|
|
537
|
-
} else if (!statusTokenMatches(planSelfReview.status, ["PASS", "APPROVED", "DONE"])) {
|
|
538
|
-
warnings.push(`Plan self-review marker is not PASS (${planSelfReview.status}).`);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (!operatorReviewAck) {
|
|
542
|
-
warnings.push("Missing operator review acknowledgment marker in tasks.md (`- operator review ack: ACKNOWLEDGED @ <ISO8601>`).");
|
|
543
|
-
} else if (!statusTokenMatches(operatorReviewAck.status, ["ACKNOWLEDGED", "ACK", "CONFIRMED", "APPROVED"])) {
|
|
544
|
-
warnings.push(`Operator review acknowledgment marker is not acknowledged (${operatorReviewAck.status}).`);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return {
|
|
548
|
-
strictMode: gateMode.strict,
|
|
549
|
-
hasAnyMarker,
|
|
550
|
-
designApproval: {
|
|
551
|
-
state: designApprovalState,
|
|
552
|
-
stale: designApprovalStale,
|
|
553
|
-
marker: designApproval
|
|
554
|
-
},
|
|
555
|
-
planSelfReview: {
|
|
556
|
-
marker: planSelfReview
|
|
557
|
-
},
|
|
558
|
-
operatorReviewAck: {
|
|
559
|
-
marker: operatorReviewAck
|
|
560
|
-
},
|
|
561
|
-
malformed: [...designMarkers.malformed, ...taskMarkers.malformed],
|
|
562
|
-
blockers,
|
|
563
|
-
warnings,
|
|
564
|
-
notes
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function mergeSignalBySurface(signals) {
|
|
569
|
-
const summary = {};
|
|
570
|
-
for (const signal of signals || []) {
|
|
571
|
-
const key = String(signal.surface || "").trim();
|
|
572
|
-
if (!key || summary[key]) {
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
summary[key] = signal;
|
|
576
|
-
}
|
|
577
|
-
return summary;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function applyTaskExecutionAndReviewFindings(findings, signals) {
|
|
581
|
-
const taskExecutionSignals = listSignalsBySurfacePrefix(signals, "task-execution.");
|
|
582
|
-
const taskReviewSignals = listSignalsBySurfacePrefix(signals, "task-review.");
|
|
583
|
-
const latestTaskExecution = mergeSignalBySurface(taskExecutionSignals);
|
|
584
|
-
const latestTaskReview = mergeSignalBySurface(taskReviewSignals);
|
|
585
|
-
|
|
586
|
-
for (const signal of Object.values(latestTaskExecution)) {
|
|
587
|
-
const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
588
|
-
const outOfScopeWrites =
|
|
589
|
-
signal.details && Array.isArray(signal.details.outOfScopeWrites)
|
|
590
|
-
? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
|
|
591
|
-
: [];
|
|
592
|
-
const taskGroupId =
|
|
593
|
-
(envelope && envelope.taskGroupId) ||
|
|
594
|
-
String(signal.surface || "").replace(/^task-execution\./, "") ||
|
|
595
|
-
"unknown";
|
|
596
|
-
if (signal.status === STATUS.BLOCK) {
|
|
597
|
-
findings.blockers.push(`Task group ${taskGroupId} is BLOCKED in implementer status envelope.`);
|
|
598
|
-
} else if (signal.status === STATUS.WARN) {
|
|
599
|
-
findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
|
|
600
|
-
}
|
|
601
|
-
if (outOfScopeWrites.length > 0) {
|
|
602
|
-
findings.blockers.push(
|
|
603
|
-
`Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
if (envelope && envelope.summary) {
|
|
607
|
-
findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const reviewStateByGroup = {};
|
|
612
|
-
for (const signal of Object.values(latestTaskReview)) {
|
|
613
|
-
const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
614
|
-
const taskGroupId =
|
|
615
|
-
(envelope && envelope.taskGroupId) ||
|
|
616
|
-
String(signal.surface || "").replace(/^task-review\./, "").split(".")[0] ||
|
|
617
|
-
"unknown";
|
|
618
|
-
const stage = envelope && envelope.stage ? envelope.stage : String(signal.surface || "").split(".").pop();
|
|
619
|
-
if (!reviewStateByGroup[taskGroupId]) {
|
|
620
|
-
reviewStateByGroup[taskGroupId] = {};
|
|
621
|
-
}
|
|
622
|
-
reviewStateByGroup[taskGroupId][stage] = signal.status;
|
|
623
|
-
if (signal.status === STATUS.BLOCK) {
|
|
624
|
-
findings.blockers.push(`Task review ${taskGroupId}/${stage} is BLOCK.`);
|
|
625
|
-
} else if (signal.status === STATUS.WARN) {
|
|
626
|
-
findings.warnings.push(`Task review ${taskGroupId}/${stage} is WARN and requires follow-up.`);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
|
|
631
|
-
if (state.quality && !state.spec) {
|
|
632
|
-
findings.blockers.push(
|
|
633
|
-
`Task review ordering violation for ${taskGroupId}: quality review exists without a prior spec review result.`
|
|
634
|
-
);
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
if (state.quality && state.spec === STATUS.WARN) {
|
|
638
|
-
findings.blockers.push(
|
|
639
|
-
`Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
|
|
640
|
-
);
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
if (state.quality && state.spec === STATUS.BLOCK) {
|
|
644
|
-
findings.blockers.push(
|
|
645
|
-
`Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function normalizeCheckpointStatus(status) {
|
|
652
|
-
const normalized = String(status || "").toUpperCase();
|
|
653
|
-
if (normalized === STATUS.PASS || normalized === STATUS.WARN || normalized === STATUS.BLOCK) {
|
|
654
|
-
return normalized;
|
|
655
|
-
}
|
|
656
|
-
return STATUS.PASS;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
|
|
660
|
-
if (!pathExists(workflowRoot)) {
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
return summarizeAudit(
|
|
664
|
-
auditProject(projectRoot, {
|
|
665
|
-
mode: "integrity",
|
|
666
|
-
preloadedSignalSummary: signalSummary
|
|
667
|
-
})
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function collectCompletionAudit(projectRoot, workflowRoot, changeId, signalSummary) {
|
|
672
|
-
if (!changeId || !pathExists(workflowRoot)) {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
return summarizeAudit(
|
|
676
|
-
auditProject(projectRoot, {
|
|
677
|
-
mode: "completion",
|
|
678
|
-
changeId,
|
|
679
|
-
preloadedSignalSummary: signalSummary
|
|
680
|
-
})
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function applyAuditFindings(stageId, findings, integrityAudit, completionAudit) {
|
|
685
|
-
let nextStageId = stageId;
|
|
686
|
-
|
|
687
|
-
if (integrityAudit && integrityAudit.status === "FAIL") {
|
|
688
|
-
findings.warnings.push("Integrity audit currently reports FAIL.");
|
|
689
|
-
} else if (integrityAudit && integrityAudit.status === "WARN") {
|
|
690
|
-
findings.warnings.push("Integrity audit currently reports WARN.");
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if ((nextStageId === "verify" || nextStageId === "complete") && completionAudit) {
|
|
694
|
-
if (completionAudit.status === "PASS") {
|
|
695
|
-
if (nextStageId === "verify") {
|
|
696
|
-
nextStageId = "complete";
|
|
697
|
-
}
|
|
698
|
-
findings.notes.push("Completion audit reports PASS for the active change.");
|
|
699
|
-
} else if (completionAudit.status === "WARN") {
|
|
700
|
-
findings.warnings.push("Completion audit currently reports WARN.");
|
|
701
|
-
if (nextStageId === "complete") {
|
|
702
|
-
nextStageId = "verify";
|
|
703
|
-
}
|
|
704
|
-
} else if (completionAudit.status === "FAIL") {
|
|
705
|
-
findings.blockers.push("Completion audit currently reports FAIL.");
|
|
706
|
-
if (nextStageId === "complete") {
|
|
707
|
-
nextStageId = "verify";
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
return nextStageId;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function applyExecutionSignalFindings(stageId, findings, signalSummary) {
|
|
716
|
-
let nextStageId = stageId;
|
|
717
|
-
const strictPromotion = isStrictPromotionEnabled();
|
|
718
|
-
const signalParseIssue = signalSummary["signal-file-parse"];
|
|
719
|
-
if (signalParseIssue) {
|
|
720
|
-
const warningText =
|
|
721
|
-
Array.isArray(signalParseIssue.warnings) && signalParseIssue.warnings[0]
|
|
722
|
-
? String(signalParseIssue.warnings[0])
|
|
723
|
-
: "Malformed execution signal files were detected.";
|
|
724
|
-
findings.warnings.push(warningText);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const diffSignal = signalSummary["diff-spec"];
|
|
728
|
-
if (diffSignal && diffSignal.status && diffSignal.status !== STATUS.PASS) {
|
|
729
|
-
findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
const lintSpecSignal = signalSummary["lint-spec"];
|
|
733
|
-
const lintSpecGateConfigs = [
|
|
734
|
-
{ id: "principleInheritance", label: "principleInheritance", fallbackStage: "breakdown" },
|
|
735
|
-
{ id: "clarify", label: "clarify", fallbackStage: "breakdown" },
|
|
736
|
-
{ id: "scenarioQuality", label: "scenarioQuality", fallbackStage: "breakdown" }
|
|
737
|
-
];
|
|
738
|
-
let lintSpecGateObserved = false;
|
|
739
|
-
let strongestLintSpecGateStatus = "";
|
|
740
|
-
for (const config of lintSpecGateConfigs) {
|
|
741
|
-
const gate = getSignalGate(lintSpecSignal, config.id);
|
|
742
|
-
if (!gate) {
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
lintSpecGateObserved = true;
|
|
746
|
-
const gateStatus = resolveEffectiveGateStatus(gate, lintSpecSignal);
|
|
747
|
-
if (statusSeverity(gateStatus) > statusSeverity(strongestLintSpecGateStatus)) {
|
|
748
|
-
strongestLintSpecGateStatus = gateStatus;
|
|
749
|
-
}
|
|
750
|
-
const evidenceRefs = collectGateEvidenceRefs(gate);
|
|
751
|
-
const evidenceSuffix =
|
|
752
|
-
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
753
|
-
if (gateStatus === STATUS.BLOCK) {
|
|
754
|
-
findings.blockers.push(
|
|
755
|
-
`lint-spec gate ${config.label} is BLOCK and prevents planning promotion.${evidenceSuffix}`
|
|
756
|
-
);
|
|
757
|
-
addBlockingGateRecord(
|
|
758
|
-
findings,
|
|
759
|
-
config.id,
|
|
760
|
-
"lint-spec",
|
|
761
|
-
gate,
|
|
762
|
-
`lint-spec gate ${config.label} is BLOCK`
|
|
763
|
-
);
|
|
764
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
|
|
765
|
-
} else if (gateStatus === STATUS.WARN) {
|
|
766
|
-
findings.warnings.push(`lint-spec gate ${config.label} is WARN.${evidenceSuffix}`);
|
|
767
|
-
if (strictPromotion) {
|
|
768
|
-
findings.blockers.push(
|
|
769
|
-
`DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec gate ${config.label} WARN blocks promotion.`
|
|
770
|
-
);
|
|
771
|
-
addBlockingGateRecord(
|
|
772
|
-
findings,
|
|
773
|
-
config.id,
|
|
774
|
-
"lint-spec",
|
|
775
|
-
gate,
|
|
776
|
-
`strict promotion escalated lint-spec gate ${config.label} WARN`
|
|
777
|
-
);
|
|
778
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
for (const message of Array.isArray(gate.compatibility) ? gate.compatibility : []) {
|
|
782
|
-
findings.notes.push(`lint-spec gate ${config.label} compatibility: ${message}`);
|
|
783
|
-
}
|
|
784
|
-
if (config.id === "clarify") {
|
|
785
|
-
for (const bounded of Array.isArray(gate.bounded) ? gate.bounded : []) {
|
|
786
|
-
findings.notes.push(`lint-spec gate clarify bounded ambiguity: ${bounded}`);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
const lintSpecSignalStatus = normalizeSignalStatus(lintSpecSignal && lintSpecSignal.status);
|
|
791
|
-
if (
|
|
792
|
-
lintSpecSignal &&
|
|
793
|
-
(!lintSpecGateObserved || statusSeverity(lintSpecSignalStatus) > statusSeverity(strongestLintSpecGateStatus))
|
|
794
|
-
) {
|
|
795
|
-
if (lintSpecSignalStatus === STATUS.BLOCK) {
|
|
796
|
-
findings.blockers.push("lint-spec signal is BLOCK.");
|
|
797
|
-
addBlockingGateRecord(
|
|
798
|
-
findings,
|
|
799
|
-
"lint-spec",
|
|
800
|
-
"lint-spec",
|
|
801
|
-
lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
|
|
802
|
-
"lint-spec signal is BLOCK"
|
|
803
|
-
);
|
|
804
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
|
|
805
|
-
} else if (lintSpecSignalStatus === STATUS.WARN) {
|
|
806
|
-
findings.warnings.push("lint-spec signal is WARN.");
|
|
807
|
-
if (strictPromotion) {
|
|
808
|
-
findings.blockers.push(
|
|
809
|
-
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec WARN blocks promotion."
|
|
810
|
-
);
|
|
811
|
-
addBlockingGateRecord(
|
|
812
|
-
findings,
|
|
813
|
-
"lint-spec",
|
|
814
|
-
"lint-spec",
|
|
815
|
-
lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
|
|
816
|
-
"strict promotion escalated lint-spec WARN"
|
|
817
|
-
);
|
|
818
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const analyzeSignal = signalSummary["scope-check"];
|
|
824
|
-
const analyzeGate = getSignalGate(analyzeSignal, "analyze");
|
|
825
|
-
let analyzeGateStatus = "";
|
|
826
|
-
if (analyzeGate) {
|
|
827
|
-
const evidenceRefs = collectGateEvidenceRefs(analyzeGate);
|
|
828
|
-
const evidenceSuffix =
|
|
829
|
-
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
830
|
-
analyzeGateStatus = resolveEffectiveGateStatus(analyzeGate, analyzeSignal);
|
|
831
|
-
if (analyzeGateStatus === STATUS.BLOCK) {
|
|
832
|
-
findings.blockers.push(`scope-check gate analyze is BLOCK.${evidenceSuffix}`);
|
|
833
|
-
addBlockingGateRecord(
|
|
834
|
-
findings,
|
|
835
|
-
"analyze",
|
|
836
|
-
"scope-check",
|
|
837
|
-
analyzeGate,
|
|
838
|
-
"scope-check gate analyze is BLOCK"
|
|
839
|
-
);
|
|
840
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
841
|
-
} else if (analyzeGateStatus === STATUS.WARN) {
|
|
842
|
-
findings.warnings.push(`scope-check gate analyze is WARN.${evidenceSuffix}`);
|
|
843
|
-
if (strictPromotion) {
|
|
844
|
-
findings.blockers.push(
|
|
845
|
-
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check gate analyze WARN blocks promotion."
|
|
846
|
-
);
|
|
847
|
-
addBlockingGateRecord(
|
|
848
|
-
findings,
|
|
849
|
-
"analyze",
|
|
850
|
-
"scope-check",
|
|
851
|
-
analyzeGate,
|
|
852
|
-
"strict promotion escalated scope-check gate analyze WARN"
|
|
853
|
-
);
|
|
854
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
for (const message of Array.isArray(analyzeGate.compatibility) ? analyzeGate.compatibility : []) {
|
|
858
|
-
findings.notes.push(`scope-check gate analyze compatibility: ${message}`);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
const analyzeSignalStatus = normalizeSignalStatus(analyzeSignal && analyzeSignal.status);
|
|
862
|
-
if (
|
|
863
|
-
analyzeSignal &&
|
|
864
|
-
(!analyzeGate || statusSeverity(analyzeSignalStatus) > statusSeverity(analyzeGateStatus))
|
|
865
|
-
) {
|
|
866
|
-
if (analyzeSignalStatus === STATUS.BLOCK) {
|
|
867
|
-
findings.blockers.push("scope-check signal is BLOCK.");
|
|
868
|
-
addBlockingGateRecord(
|
|
869
|
-
findings,
|
|
870
|
-
"scope-check",
|
|
871
|
-
"scope-check",
|
|
872
|
-
analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
|
|
873
|
-
"scope-check signal is BLOCK"
|
|
874
|
-
);
|
|
875
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
876
|
-
} else if (analyzeSignalStatus === STATUS.WARN) {
|
|
877
|
-
findings.warnings.push("scope-check signal is WARN.");
|
|
878
|
-
if (strictPromotion) {
|
|
879
|
-
findings.blockers.push(
|
|
880
|
-
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check WARN blocks promotion."
|
|
881
|
-
);
|
|
882
|
-
addBlockingGateRecord(
|
|
883
|
-
findings,
|
|
884
|
-
"scope-check",
|
|
885
|
-
"scope-check",
|
|
886
|
-
analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
|
|
887
|
-
"strict promotion escalated scope-check WARN"
|
|
888
|
-
);
|
|
889
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
const lintTasksSignal = signalSummary["lint-tasks"];
|
|
895
|
-
const taskCheckpointGate = getSignalGate(lintTasksSignal, "taskCheckpoint");
|
|
896
|
-
const taskCheckpointGateStatus = taskCheckpointGate
|
|
897
|
-
? resolveEffectiveGateStatus(taskCheckpointGate, lintTasksSignal)
|
|
898
|
-
: "";
|
|
899
|
-
const lintTasksSignalStatus = normalizeSignalStatus(lintTasksSignal && lintTasksSignal.status);
|
|
900
|
-
if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.BLOCK) {
|
|
901
|
-
const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
|
|
902
|
-
const evidenceSuffix =
|
|
903
|
-
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
904
|
-
findings.blockers.push(`lint-tasks task-checkpoint is BLOCK and prevents promotion into build.${evidenceSuffix}`);
|
|
905
|
-
addBlockingGateRecord(
|
|
906
|
-
findings,
|
|
907
|
-
"taskCheckpoint",
|
|
908
|
-
"lint-tasks",
|
|
909
|
-
taskCheckpointGate,
|
|
910
|
-
"lint-tasks task-checkpoint is BLOCK"
|
|
911
|
-
);
|
|
912
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
913
|
-
} else if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.WARN) {
|
|
914
|
-
const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
|
|
915
|
-
const evidenceSuffix =
|
|
916
|
-
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
917
|
-
findings.warnings.push(`lint-tasks task-checkpoint is WARN.${evidenceSuffix}`);
|
|
918
|
-
if (strictPromotion) {
|
|
919
|
-
findings.blockers.push(
|
|
920
|
-
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
|
|
921
|
-
);
|
|
922
|
-
addBlockingGateRecord(
|
|
923
|
-
findings,
|
|
924
|
-
"taskCheckpoint",
|
|
925
|
-
"lint-tasks",
|
|
926
|
-
taskCheckpointGate,
|
|
927
|
-
"strict promotion escalated lint-tasks task-checkpoint WARN"
|
|
928
|
-
);
|
|
929
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
if (
|
|
933
|
-
lintTasksSignal &&
|
|
934
|
-
(!taskCheckpointGate || statusSeverity(lintTasksSignalStatus) > statusSeverity(taskCheckpointGateStatus))
|
|
935
|
-
) {
|
|
936
|
-
if (lintTasksSignalStatus === STATUS.BLOCK) {
|
|
937
|
-
findings.blockers.push("lint-tasks signal is BLOCK.");
|
|
938
|
-
addBlockingGateRecord(
|
|
939
|
-
findings,
|
|
940
|
-
"lint-tasks",
|
|
941
|
-
"lint-tasks",
|
|
942
|
-
lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
|
|
943
|
-
"lint-tasks signal is BLOCK"
|
|
944
|
-
);
|
|
945
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
946
|
-
} else if (lintTasksSignalStatus === STATUS.WARN) {
|
|
947
|
-
findings.warnings.push("lint-tasks signal is WARN.");
|
|
948
|
-
if (strictPromotion) {
|
|
949
|
-
findings.blockers.push(
|
|
950
|
-
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
|
|
951
|
-
);
|
|
952
|
-
addBlockingGateRecord(
|
|
953
|
-
findings,
|
|
954
|
-
"lint-tasks",
|
|
955
|
-
"lint-tasks",
|
|
956
|
-
lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
|
|
957
|
-
"strict promotion escalated lint-tasks WARN"
|
|
958
|
-
);
|
|
959
|
-
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (taskCheckpointGate) {
|
|
964
|
-
for (const message of Array.isArray(taskCheckpointGate.compatibility)
|
|
965
|
-
? taskCheckpointGate.compatibility
|
|
966
|
-
: []) {
|
|
967
|
-
findings.notes.push(`lint-tasks gate taskCheckpoint compatibility: ${message}`);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
const verificationSignal = signalSummary["verify-coverage"];
|
|
972
|
-
if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
|
|
973
|
-
findings.blockers.push("verify-coverage signal is BLOCK.");
|
|
974
|
-
if (nextStageId === "complete") {
|
|
975
|
-
nextStageId = "verify";
|
|
976
|
-
}
|
|
977
|
-
} else if (verificationSignal && verificationSignal.status === STATUS.WARN) {
|
|
978
|
-
findings.warnings.push("verify-coverage signal is WARN.");
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
return nextStageId;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function buildRouteRecommendation(stageId, context = {}) {
|
|
985
|
-
const projectRoot = context.projectRoot || process.cwd();
|
|
986
|
-
const changeId = context.changeId || "change-001";
|
|
987
|
-
|
|
988
|
-
if (context.ambiguousChangeSelection) {
|
|
989
|
-
return {
|
|
990
|
-
route: "select-change",
|
|
991
|
-
command: `da-vinci workflow-status --project ${projectRoot} --change <change-id>`,
|
|
992
|
-
reason: "Multiple change directories were found. Choose one change id first."
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
switch (stageId) {
|
|
997
|
-
case "bootstrap":
|
|
998
|
-
return {
|
|
999
|
-
route: "bootstrap-project",
|
|
1000
|
-
command: `da-vinci bootstrap-project --project ${projectRoot} --change ${changeId}`,
|
|
1001
|
-
reason: "Workflow base artifacts are missing."
|
|
1002
|
-
};
|
|
1003
|
-
case "breakdown":
|
|
1004
|
-
return {
|
|
1005
|
-
route: "/dv:breakdown",
|
|
1006
|
-
command: "/dv:breakdown",
|
|
1007
|
-
reason: "Proposal or spec artifacts are incomplete."
|
|
1008
|
-
};
|
|
1009
|
-
case "design":
|
|
1010
|
-
return {
|
|
1011
|
-
route: "/dv:design",
|
|
1012
|
-
command: "/dv:design",
|
|
1013
|
-
reason: "Design artifacts or design checkpoints are not ready."
|
|
1014
|
-
};
|
|
1015
|
-
case "tasks":
|
|
1016
|
-
return {
|
|
1017
|
-
route: "/dv:tasks",
|
|
1018
|
-
command: "/dv:tasks",
|
|
1019
|
-
reason: "Task planning artifacts are missing or blocked."
|
|
1020
|
-
};
|
|
1021
|
-
case "build":
|
|
1022
|
-
return {
|
|
1023
|
-
route: "/dv:build",
|
|
1024
|
-
command: "/dv:build",
|
|
1025
|
-
reason: "Implementation should proceed before verification."
|
|
1026
|
-
};
|
|
1027
|
-
case "verify":
|
|
1028
|
-
return {
|
|
1029
|
-
route: "/dv:verify",
|
|
1030
|
-
command: "/dv:verify",
|
|
1031
|
-
reason: "Verification and completion readiness are not satisfied yet."
|
|
1032
|
-
};
|
|
1033
|
-
default:
|
|
1034
|
-
return {
|
|
1035
|
-
route: null,
|
|
1036
|
-
command: null,
|
|
1037
|
-
reason: "No next route. Workflow appears complete."
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
function readCheckpointStatuses(changeDir) {
|
|
1043
|
-
const collected = {};
|
|
1044
|
-
const files = [
|
|
1045
|
-
path.join(changeDir, "design.md"),
|
|
1046
|
-
path.join(changeDir, "pencil-design.md"),
|
|
1047
|
-
path.join(changeDir, "tasks.md"),
|
|
1048
|
-
path.join(changeDir, "verification.md")
|
|
1049
|
-
];
|
|
1050
|
-
|
|
1051
|
-
for (const artifactPath of files) {
|
|
1052
|
-
const text = readTextIfExists(artifactPath);
|
|
1053
|
-
if (!text) {
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
const statuses = parseCheckpointStatusMap(text);
|
|
1057
|
-
for (const [label, value] of Object.entries(statuses)) {
|
|
1058
|
-
const normalized = normalizeCheckpointLabel(label);
|
|
1059
|
-
if (!normalized) {
|
|
1060
|
-
continue;
|
|
1061
|
-
}
|
|
1062
|
-
collected[normalized] = String(value || "").toUpperCase();
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
return collected;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function deriveStageFromArtifacts(artifactState, checkpointStatuses, findings) {
|
|
1070
|
-
if (!artifactState.workflowRootReady) {
|
|
1071
|
-
findings.blockers.push("Missing `.da-vinci/` directory.");
|
|
1072
|
-
return "bootstrap";
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if (!artifactState.changeSelected) {
|
|
1076
|
-
findings.blockers.push("No active change selected under `.da-vinci/changes/`.");
|
|
1077
|
-
return "breakdown";
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
if (!artifactState.proposal || artifactState.specFiles.length === 0) {
|
|
1081
|
-
if (!artifactState.proposal) {
|
|
1082
|
-
findings.blockers.push("Missing `proposal.md` for the active change.");
|
|
1083
|
-
}
|
|
1084
|
-
if (artifactState.specFiles.length === 0) {
|
|
1085
|
-
findings.blockers.push("Missing `specs/*/spec.md` for the active change.");
|
|
1086
|
-
}
|
|
1087
|
-
return "breakdown";
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
if (
|
|
1091
|
-
!artifactState.design ||
|
|
1092
|
-
!artifactState.pencilDesign ||
|
|
1093
|
-
!artifactState.pencilBindings
|
|
1094
|
-
) {
|
|
1095
|
-
if (!artifactState.design) {
|
|
1096
|
-
findings.blockers.push("Missing `design.md` for the active change.");
|
|
1097
|
-
}
|
|
1098
|
-
if (!artifactState.pencilDesign) {
|
|
1099
|
-
findings.blockers.push("Missing `pencil-design.md` for the active change.");
|
|
1100
|
-
}
|
|
1101
|
-
if (!artifactState.pencilBindings) {
|
|
1102
|
-
findings.blockers.push("Missing `pencil-bindings.md` for the active change.");
|
|
1103
|
-
}
|
|
1104
|
-
return "design";
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
const designCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN];
|
|
1108
|
-
if (designCheckpoint === STATUS.BLOCK) {
|
|
1109
|
-
findings.blockers.push("`design checkpoint` is BLOCK.");
|
|
1110
|
-
return "design";
|
|
1111
|
-
}
|
|
1112
|
-
if (designCheckpoint === STATUS.WARN) {
|
|
1113
|
-
findings.warnings.push("`design checkpoint` is WARN.");
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
const designSourceCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE];
|
|
1117
|
-
if (designSourceCheckpoint === STATUS.BLOCK) {
|
|
1118
|
-
findings.blockers.push("`design source checkpoint` is BLOCK.");
|
|
1119
|
-
return "design";
|
|
1120
|
-
}
|
|
1121
|
-
if (designSourceCheckpoint === STATUS.WARN) {
|
|
1122
|
-
findings.warnings.push("`design source checkpoint` is WARN.");
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
const runtimeGateCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE];
|
|
1126
|
-
if (runtimeGateCheckpoint === STATUS.BLOCK) {
|
|
1127
|
-
findings.blockers.push("`mcp runtime gate` is BLOCK.");
|
|
1128
|
-
return "design";
|
|
1129
|
-
}
|
|
1130
|
-
if (runtimeGateCheckpoint === STATUS.WARN) {
|
|
1131
|
-
findings.warnings.push("`mcp runtime gate` is WARN.");
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
const mappingCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.MAPPING];
|
|
1135
|
-
if (mappingCheckpoint === STATUS.BLOCK) {
|
|
1136
|
-
findings.blockers.push("`mapping checkpoint` is BLOCK.");
|
|
1137
|
-
return "design";
|
|
1138
|
-
}
|
|
1139
|
-
if (mappingCheckpoint === STATUS.WARN) {
|
|
1140
|
-
findings.warnings.push("`mapping checkpoint` is WARN.");
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
if (!artifactState.tasks) {
|
|
1144
|
-
findings.blockers.push("Missing `tasks.md` for the active change.");
|
|
1145
|
-
return "tasks";
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
const taskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK];
|
|
1149
|
-
if (taskCheckpoint === STATUS.BLOCK) {
|
|
1150
|
-
findings.blockers.push("`task checkpoint` is BLOCK.");
|
|
1151
|
-
return "tasks";
|
|
1152
|
-
}
|
|
1153
|
-
if (taskCheckpoint === STATUS.WARN) {
|
|
1154
|
-
findings.warnings.push("`task checkpoint` is WARN.");
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
if (!artifactState.verification) {
|
|
1158
|
-
findings.blockers.push("Missing `verification.md` for the active change.");
|
|
1159
|
-
return "build";
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
return "verify";
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
function deriveTaskGroupMetadata(tasksMarkdownText, checkpointStatuses) {
|
|
1166
|
-
if (!tasksMarkdownText) {
|
|
1167
|
-
return [];
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const taskArtifact = parseTasksArtifact(tasksMarkdownText);
|
|
1171
|
-
const lines = String(tasksMarkdownText || "").replace(/\r\n?/g, "\n").split("\n");
|
|
1172
|
-
const sections = [];
|
|
1173
|
-
let current = null;
|
|
1174
|
-
for (const line of lines) {
|
|
1175
|
-
const headingMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
|
|
1176
|
-
if (headingMatch) {
|
|
1177
|
-
if (current) {
|
|
1178
|
-
sections.push(current);
|
|
1179
|
-
}
|
|
1180
|
-
current = {
|
|
1181
|
-
id: headingMatch[1],
|
|
1182
|
-
title: String(headingMatch[2] || "").trim(),
|
|
1183
|
-
checklistItems: []
|
|
1184
|
-
};
|
|
1185
|
-
continue;
|
|
1186
|
-
}
|
|
1187
|
-
if (!current) {
|
|
1188
|
-
continue;
|
|
1189
|
-
}
|
|
1190
|
-
const checklistMatch = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
|
|
1191
|
-
if (checklistMatch) {
|
|
1192
|
-
current.checklistItems.push({
|
|
1193
|
-
checked: String(checklistMatch[1] || "").toLowerCase() === "x",
|
|
1194
|
-
text: String(checklistMatch[2] || "").trim()
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
if (current) {
|
|
1199
|
-
sections.push(current);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const normalizedTaskCheckpoint = checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN;
|
|
1203
|
-
const groupMetadataById = new Map(
|
|
1204
|
-
(taskArtifact.taskGroups || []).map((group) => [
|
|
1205
|
-
group.id,
|
|
1206
|
-
{
|
|
1207
|
-
targetFiles: Array.isArray(group.targetFiles) ? group.targetFiles : [],
|
|
1208
|
-
fileReferences: Array.isArray(group.fileReferences) ? group.fileReferences : [],
|
|
1209
|
-
verificationActions: Array.isArray(group.verificationActions) ? group.verificationActions : [],
|
|
1210
|
-
verificationCommands: Array.isArray(group.verificationCommands) ? group.verificationCommands : [],
|
|
1211
|
-
executionIntent: Array.isArray(group.executionIntent) ? group.executionIntent : [],
|
|
1212
|
-
reviewIntent: group.reviewIntent === true,
|
|
1213
|
-
testingIntent: group.testingIntent === true,
|
|
1214
|
-
codeChangeLikely: group.codeChangeLikely === true
|
|
1215
|
-
}
|
|
1216
|
-
])
|
|
1217
|
-
);
|
|
1218
|
-
const metadata = sections.map((section, index) => {
|
|
1219
|
-
const total = section.checklistItems.length;
|
|
1220
|
-
const done = section.checklistItems.filter((item) => item.checked).length;
|
|
1221
|
-
const completion = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
1222
|
-
const sectionStatus =
|
|
1223
|
-
total === 0 ? "pending" : done === 0 ? "pending" : done < total ? "in_progress" : "completed";
|
|
1224
|
-
const nextAction =
|
|
1225
|
-
sectionStatus === "completed"
|
|
1226
|
-
? "advance to next task group"
|
|
1227
|
-
: section.checklistItems.find((item) => !item.checked)?.text || "continue group work";
|
|
1228
|
-
const groupMetadata = groupMetadataById.get(section.id) || {};
|
|
1229
|
-
|
|
1230
|
-
return {
|
|
1231
|
-
taskGroupId: section.id,
|
|
1232
|
-
title: section.title,
|
|
1233
|
-
status: sectionStatus,
|
|
1234
|
-
completion,
|
|
1235
|
-
checkpointOutcome: normalizedTaskCheckpoint,
|
|
1236
|
-
evidence: section.checklistItems.filter((item) => item.checked).map((item) => item.text),
|
|
1237
|
-
nextAction,
|
|
1238
|
-
targetFiles: groupMetadata.targetFiles || [],
|
|
1239
|
-
fileReferences: groupMetadata.fileReferences || [],
|
|
1240
|
-
verificationActions: groupMetadata.verificationActions || [],
|
|
1241
|
-
verificationCommands: groupMetadata.verificationCommands || [],
|
|
1242
|
-
executionIntent: groupMetadata.executionIntent || [],
|
|
1243
|
-
reviewIntent: groupMetadata.reviewIntent === true,
|
|
1244
|
-
testingIntent: groupMetadata.testingIntent === true,
|
|
1245
|
-
codeChangeLikely: groupMetadata.codeChangeLikely === true,
|
|
1246
|
-
resumeCursor: {
|
|
1247
|
-
groupIndex: index,
|
|
1248
|
-
nextUncheckedItem:
|
|
1249
|
-
section.checklistItems.find((item) => !item.checked)?.text ||
|
|
1250
|
-
section.checklistItems[section.checklistItems.length - 1]?.text ||
|
|
1251
|
-
null
|
|
1252
|
-
}
|
|
1253
|
-
};
|
|
1254
|
-
});
|
|
1255
|
-
|
|
1256
|
-
if (metadata.length === 0 && taskArtifact.taskGroups.length > 0) {
|
|
1257
|
-
return taskArtifact.taskGroups.map((group, index) => {
|
|
1258
|
-
const groupMetadata = groupMetadataById.get(group.id) || {};
|
|
1259
|
-
return {
|
|
1260
|
-
taskGroupId: group.id,
|
|
1261
|
-
title: group.title,
|
|
1262
|
-
status: "pending",
|
|
1263
|
-
completion: 0,
|
|
1264
|
-
checkpointOutcome: normalizedTaskCheckpoint,
|
|
1265
|
-
evidence: [],
|
|
1266
|
-
nextAction: "start task group",
|
|
1267
|
-
targetFiles: groupMetadata.targetFiles || [],
|
|
1268
|
-
fileReferences: groupMetadata.fileReferences || [],
|
|
1269
|
-
verificationActions: groupMetadata.verificationActions || [],
|
|
1270
|
-
verificationCommands: groupMetadata.verificationCommands || [],
|
|
1271
|
-
executionIntent: groupMetadata.executionIntent || [],
|
|
1272
|
-
reviewIntent: groupMetadata.reviewIntent === true,
|
|
1273
|
-
testingIntent: groupMetadata.testingIntent === true,
|
|
1274
|
-
codeChangeLikely: groupMetadata.codeChangeLikely === true,
|
|
1275
|
-
resumeCursor: {
|
|
1276
|
-
groupIndex: index,
|
|
1277
|
-
nextUncheckedItem: null
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
return metadata;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function normalizeTaskGroupId(value) {
|
|
1287
|
-
return String(value || "").trim();
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function normalizeResumeCursor(cursor, fallbackGroupIndex = null) {
|
|
1291
|
-
const groupIndex =
|
|
1292
|
-
cursor && Number.isInteger(cursor.groupIndex)
|
|
1293
|
-
? cursor.groupIndex
|
|
1294
|
-
: Number.isInteger(fallbackGroupIndex)
|
|
1295
|
-
? fallbackGroupIndex
|
|
1296
|
-
: null;
|
|
1297
|
-
return {
|
|
1298
|
-
groupIndex,
|
|
1299
|
-
nextUncheckedItem:
|
|
1300
|
-
cursor && Object.prototype.hasOwnProperty.call(cursor, "nextUncheckedItem")
|
|
1301
|
-
? cursor.nextUncheckedItem
|
|
1302
|
-
: null
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function normalizeTaskGroupSeedMap(taskGroups) {
|
|
1307
|
-
const byId = new Map();
|
|
1308
|
-
for (const group of Array.isArray(taskGroups) ? taskGroups : []) {
|
|
1309
|
-
const taskGroupId = normalizeTaskGroupId(group && (group.taskGroupId || group.id));
|
|
1310
|
-
if (!taskGroupId || byId.has(taskGroupId)) {
|
|
1311
|
-
continue;
|
|
1312
|
-
}
|
|
1313
|
-
byId.set(taskGroupId, group);
|
|
1314
|
-
}
|
|
1315
|
-
return byId;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function findLatestSignalBySurface(signals, surface) {
|
|
1319
|
-
const normalizedSurface = String(surface || "").trim();
|
|
1320
|
-
if (!normalizedSurface) {
|
|
1321
|
-
return null;
|
|
1322
|
-
}
|
|
1323
|
-
return (signals || []).find((signal) => String(signal.surface || "").trim() === normalizedSurface) || null;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
function summarizeSignalIssues(signal, envelopeItems) {
|
|
1327
|
-
if (!signal) {
|
|
1328
|
-
return [];
|
|
102
|
+
: null
|
|
103
|
+
};
|
|
1329
104
|
}
|
|
1330
|
-
|
|
1331
|
-
const fromSignal = [
|
|
1332
|
-
...(Array.isArray(signal.failures) ? signal.failures : []),
|
|
1333
|
-
...(Array.isArray(signal.warnings) ? signal.warnings : [])
|
|
1334
|
-
];
|
|
1335
|
-
return dedupeMessages([...fromEnvelope, ...fromSignal].map((item) => String(item || "").trim()).filter(Boolean));
|
|
105
|
+
return result;
|
|
1336
106
|
}
|
|
1337
107
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
summary: fallback.summary || null,
|
|
1348
|
-
changedFiles: Array.isArray(fallback.changedFiles) ? fallback.changedFiles : [],
|
|
1349
|
-
testEvidence: Array.isArray(fallback.testEvidence) ? fallback.testEvidence : [],
|
|
1350
|
-
concerns: Array.isArray(fallback.concerns) ? fallback.concerns : [],
|
|
1351
|
-
blockers: Array.isArray(fallback.blockers) ? fallback.blockers : [],
|
|
1352
|
-
outOfScopeWrites: Array.isArray(fallback.outOfScopeWrites) ? fallback.outOfScopeWrites : [],
|
|
1353
|
-
recordedAt: fallback.recordedAt || null
|
|
1354
|
-
};
|
|
108
|
+
/**
|
|
109
|
+
* Reduce full audit payloads to the capped summary stored on workflow results.
|
|
110
|
+
*
|
|
111
|
+
* @param {object | null} result
|
|
112
|
+
* @returns {{ status: string, failures: string[], warnings: string[], notes: string[] } | null}
|
|
113
|
+
*/
|
|
114
|
+
function summarizeAudit(result) {
|
|
115
|
+
if (!result) {
|
|
116
|
+
return null;
|
|
1355
117
|
}
|
|
1356
118
|
|
|
1357
119
|
return {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
changedFiles:
|
|
1363
|
-
envelope && Array.isArray(envelope.changedFiles)
|
|
1364
|
-
? envelope.changedFiles
|
|
1365
|
-
: Array.isArray(fallback.changedFiles)
|
|
1366
|
-
? fallback.changedFiles
|
|
1367
|
-
: [],
|
|
1368
|
-
testEvidence:
|
|
1369
|
-
envelope && Array.isArray(envelope.testEvidence)
|
|
1370
|
-
? envelope.testEvidence
|
|
1371
|
-
: Array.isArray(fallback.testEvidence)
|
|
1372
|
-
? fallback.testEvidence
|
|
1373
|
-
: [],
|
|
1374
|
-
concerns: summarizeSignalIssues(signal, envelope && envelope.concerns),
|
|
1375
|
-
blockers: summarizeSignalIssues(signal, envelope && envelope.blockers),
|
|
1376
|
-
outOfScopeWrites:
|
|
1377
|
-
signal.details && Array.isArray(signal.details.outOfScopeWrites)
|
|
1378
|
-
? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
|
|
1379
|
-
: Array.isArray(fallback.outOfScopeWrites)
|
|
1380
|
-
? fallback.outOfScopeWrites
|
|
1381
|
-
: [],
|
|
1382
|
-
recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
|
|
120
|
+
status: result.status,
|
|
121
|
+
failures: Array.isArray(result.failures) ? result.failures.slice(0, MAX_REPORTED_MESSAGES) : [],
|
|
122
|
+
warnings: Array.isArray(result.warnings) ? result.warnings.slice(0, MAX_REPORTED_MESSAGES) : [],
|
|
123
|
+
notes: Array.isArray(result.notes) ? result.notes.slice(0, MAX_REPORTED_MESSAGES) : []
|
|
1383
124
|
};
|
|
1384
125
|
}
|
|
1385
126
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
recordedAt: fallback.recordedAt || null
|
|
1398
|
-
};
|
|
127
|
+
/**
|
|
128
|
+
* Collect the integrity audit with the already-computed routing signal summary preloaded.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} projectRoot
|
|
131
|
+
* @param {string} workflowRoot
|
|
132
|
+
* @param {Object.<string, object>} signalSummary
|
|
133
|
+
* @returns {object | null}
|
|
134
|
+
*/
|
|
135
|
+
function collectIntegrityAudit(projectRoot, workflowRoot, signalSummary) {
|
|
136
|
+
if (!pathExists(workflowRoot)) {
|
|
137
|
+
return null;
|
|
1399
138
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
issues: summarizeSignalIssues(signal, envelope && envelope.issues),
|
|
1407
|
-
recordedAt: (envelope && envelope.recordedAt) || signal.timestamp || fallback.recordedAt || null
|
|
1408
|
-
};
|
|
139
|
+
return summarizeAudit(
|
|
140
|
+
auditProject(projectRoot, {
|
|
141
|
+
mode: "integrity",
|
|
142
|
+
preloadedSignalSummary: signalSummary
|
|
143
|
+
})
|
|
144
|
+
);
|
|
1409
145
|
}
|
|
1410
146
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
}
|
|
147
|
+
/**
|
|
148
|
+
* Collect the completion audit for an active change with the already-computed routing signal summary.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} projectRoot
|
|
151
|
+
* @param {string} workflowRoot
|
|
152
|
+
* @param {string | null} changeId
|
|
153
|
+
* @param {Object.<string, object>} signalSummary
|
|
154
|
+
* @returns {object | null}
|
|
155
|
+
*/
|
|
156
|
+
function collectCompletionAudit(projectRoot, workflowRoot, changeId, signalSummary) {
|
|
157
|
+
if (!changeId || !pathExists(workflowRoot)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return summarizeAudit(
|
|
161
|
+
auditProject(projectRoot, {
|
|
162
|
+
mode: "completion",
|
|
163
|
+
changeId,
|
|
164
|
+
preloadedSignalSummary: signalSummary
|
|
165
|
+
})
|
|
166
|
+
);
|
|
1424
167
|
}
|
|
1425
168
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
if (implementer.present && implementer.outOfScopeWrites.length > 0) {
|
|
1437
|
-
return {
|
|
1438
|
-
status: "blocked",
|
|
1439
|
-
nextAction:
|
|
1440
|
-
`resolve out-of-scope writes for task group ${group.taskGroupId}: ${implementer.outOfScopeWrites.join(", ")}`,
|
|
1441
|
-
resumeCursor: {
|
|
1442
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1443
|
-
nextUncheckedItem: null,
|
|
1444
|
-
liveFocus: "out_of_scope_write"
|
|
1445
|
-
},
|
|
1446
|
-
source: "implementer",
|
|
1447
|
-
reason: "out_of_scope_write"
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
169
|
+
/**
|
|
170
|
+
* Map the effective workflow stage to the next operator-facing route suggestion.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} stageId
|
|
173
|
+
* @param {{ projectRoot?: string, changeId?: string, ambiguousChangeSelection?: boolean }} [context]
|
|
174
|
+
* @returns {{ route: string | null, command: string | null, reason: string }}
|
|
175
|
+
*/
|
|
176
|
+
function buildRouteRecommendation(stageId, context = {}) {
|
|
177
|
+
const projectRoot = context.projectRoot || process.cwd();
|
|
178
|
+
const changeId = context.changeId || "change-001";
|
|
1450
179
|
|
|
1451
|
-
if (
|
|
180
|
+
if (context.ambiguousChangeSelection) {
|
|
1452
181
|
return {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
implementer.summary ||
|
|
1457
|
-
`resolve implementer blocker for task group ${group.taskGroupId}`,
|
|
1458
|
-
resumeCursor: {
|
|
1459
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1460
|
-
nextUncheckedItem: null,
|
|
1461
|
-
liveFocus: "implementer_block"
|
|
1462
|
-
},
|
|
1463
|
-
source: "implementer",
|
|
1464
|
-
reason: "implementer_block"
|
|
182
|
+
route: "select-change",
|
|
183
|
+
command: `da-vinci workflow-status --project ${projectRoot} --change <change-id>`,
|
|
184
|
+
reason: "Multiple change directories were found. Choose one change id first."
|
|
1465
185
|
};
|
|
1466
186
|
}
|
|
1467
187
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
? Number(planned.completion)
|
|
1471
|
-
: 0;
|
|
1472
|
-
const reviewNearComplete = planned.status !== "completed" && plannedCompletion >= 75;
|
|
1473
|
-
const reviewHardDue = planned.status === "completed" || reviewNearComplete;
|
|
1474
|
-
const reviewContextReady = review.required && (reviewSignalsPresent || reviewHardDue || implementer.present);
|
|
1475
|
-
|
|
1476
|
-
if (reviewContextReady) {
|
|
1477
|
-
if (
|
|
1478
|
-
review.quality.present &&
|
|
1479
|
-
(!review.spec.present || review.spec.status === "missing" || review.spec.status === STATUS.WARN)
|
|
1480
|
-
) {
|
|
188
|
+
switch (stageId) {
|
|
189
|
+
case "bootstrap":
|
|
1481
190
|
return {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
resumeCursor: {
|
|
1486
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1487
|
-
nextUncheckedItem: null,
|
|
1488
|
-
liveFocus: "review_ordering_violation"
|
|
1489
|
-
},
|
|
1490
|
-
source: "review",
|
|
1491
|
-
reason: "review_ordering_violation"
|
|
191
|
+
route: "bootstrap-project",
|
|
192
|
+
command: `da-vinci bootstrap-project --project ${projectRoot} --change ${changeId}`,
|
|
193
|
+
reason: "Workflow base artifacts are missing."
|
|
1492
194
|
};
|
|
1493
|
-
|
|
1494
|
-
if (review.spec.status === STATUS.BLOCK) {
|
|
195
|
+
case "breakdown":
|
|
1495
196
|
return {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
resumeCursor: {
|
|
1500
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1501
|
-
nextUncheckedItem: null,
|
|
1502
|
-
liveFocus: "spec_review_block"
|
|
1503
|
-
},
|
|
1504
|
-
source: "review",
|
|
1505
|
-
reason: "spec_review_block"
|
|
197
|
+
route: "/dv:breakdown",
|
|
198
|
+
command: "/dv:breakdown",
|
|
199
|
+
reason: "Proposal or spec artifacts are incomplete."
|
|
1506
200
|
};
|
|
1507
|
-
|
|
1508
|
-
if (!review.spec.present || review.spec.status === "missing") {
|
|
1509
|
-
if (reviewHardDue || reviewSignalsPresent) {
|
|
1510
|
-
return {
|
|
1511
|
-
status: "review_pending",
|
|
1512
|
-
nextAction: `record spec review PASS or WARN for task group ${group.taskGroupId} before quality review`,
|
|
1513
|
-
resumeCursor: {
|
|
1514
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1515
|
-
nextUncheckedItem: null,
|
|
1516
|
-
liveFocus: "spec_review_missing"
|
|
1517
|
-
},
|
|
1518
|
-
source: "review",
|
|
1519
|
-
reason: "spec_review_missing"
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
} else {
|
|
1523
|
-
const specWarn = review.spec.status === STATUS.WARN;
|
|
1524
|
-
if (review.quality.status === STATUS.BLOCK) {
|
|
1525
|
-
return {
|
|
1526
|
-
status: "blocked",
|
|
1527
|
-
nextAction:
|
|
1528
|
-
review.quality.issues[0] ||
|
|
1529
|
-
review.quality.summary ||
|
|
1530
|
-
`resolve quality review BLOCK for task group ${group.taskGroupId}`,
|
|
1531
|
-
resumeCursor: {
|
|
1532
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1533
|
-
nextUncheckedItem: null,
|
|
1534
|
-
liveFocus: "quality_review_block"
|
|
1535
|
-
},
|
|
1536
|
-
source: "review",
|
|
1537
|
-
reason: "quality_review_block"
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
if (!review.quality.present || review.quality.status === "missing") {
|
|
1541
|
-
if (reviewHardDue) {
|
|
1542
|
-
return {
|
|
1543
|
-
status: "review_pending",
|
|
1544
|
-
nextAction: `record quality review PASS or WARN for task group ${group.taskGroupId}`,
|
|
1545
|
-
resumeCursor: {
|
|
1546
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1547
|
-
nextUncheckedItem: null,
|
|
1548
|
-
liveFocus: "quality_review_missing"
|
|
1549
|
-
},
|
|
1550
|
-
source: "review",
|
|
1551
|
-
reason: "quality_review_missing"
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1554
|
-
if (specWarn) {
|
|
1555
|
-
return {
|
|
1556
|
-
status: "in_progress",
|
|
1557
|
-
nextAction:
|
|
1558
|
-
review.spec.issues[0] ||
|
|
1559
|
-
review.spec.summary ||
|
|
1560
|
-
`resolve spec review follow-up for task group ${group.taskGroupId}`,
|
|
1561
|
-
resumeCursor: {
|
|
1562
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1563
|
-
nextUncheckedItem: null,
|
|
1564
|
-
liveFocus: "spec_review_warn"
|
|
1565
|
-
},
|
|
1566
|
-
source: "review",
|
|
1567
|
-
reason: "spec_review_warn"
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
} else if (review.quality.status === STATUS.WARN) {
|
|
1571
|
-
return {
|
|
1572
|
-
status: "in_progress",
|
|
1573
|
-
nextAction:
|
|
1574
|
-
review.quality.issues[0] ||
|
|
1575
|
-
review.quality.summary ||
|
|
1576
|
-
`resolve quality review follow-up for task group ${group.taskGroupId}`,
|
|
1577
|
-
resumeCursor: {
|
|
1578
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1579
|
-
nextUncheckedItem: null,
|
|
1580
|
-
liveFocus: "quality_review_warn"
|
|
1581
|
-
},
|
|
1582
|
-
source: "review",
|
|
1583
|
-
reason: "quality_review_warn"
|
|
1584
|
-
};
|
|
1585
|
-
} else if (specWarn) {
|
|
1586
|
-
return {
|
|
1587
|
-
status: "in_progress",
|
|
1588
|
-
nextAction:
|
|
1589
|
-
review.spec.issues[0] ||
|
|
1590
|
-
review.spec.summary ||
|
|
1591
|
-
`resolve spec review follow-up for task group ${group.taskGroupId}`,
|
|
1592
|
-
resumeCursor: {
|
|
1593
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1594
|
-
nextUncheckedItem: null,
|
|
1595
|
-
liveFocus: "spec_review_warn"
|
|
1596
|
-
},
|
|
1597
|
-
source: "review",
|
|
1598
|
-
reason: "spec_review_warn"
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
if (implementer.present && implementer.signalStatus === STATUS.WARN) {
|
|
1605
|
-
return {
|
|
1606
|
-
status: "in_progress",
|
|
1607
|
-
nextAction:
|
|
1608
|
-
implementer.concerns[0] ||
|
|
1609
|
-
implementer.summary ||
|
|
1610
|
-
`resolve implementer concerns for task group ${group.taskGroupId}`,
|
|
1611
|
-
resumeCursor: {
|
|
1612
|
-
groupIndex: fallbackCursor.groupIndex,
|
|
1613
|
-
nextUncheckedItem: null,
|
|
1614
|
-
liveFocus: "implementer_warn"
|
|
1615
|
-
},
|
|
1616
|
-
source: "implementer",
|
|
1617
|
-
reason: "implementer_warn"
|
|
1618
|
-
};
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
return effective;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
function deriveTaskGroupRuntimeState(plannedTaskGroups, signals, seedTaskGroups, decisionTraceRecords) {
|
|
1625
|
-
const plannedGroups = Array.isArray(plannedTaskGroups) ? plannedTaskGroups : [];
|
|
1626
|
-
const seedMap = normalizeTaskGroupSeedMap(seedTaskGroups);
|
|
1627
|
-
|
|
1628
|
-
return plannedGroups.map((plannedGroup, index) => {
|
|
1629
|
-
const taskGroupId = normalizeTaskGroupId(plannedGroup.taskGroupId || plannedGroup.id);
|
|
1630
|
-
const seed = seedMap.get(taskGroupId) || {};
|
|
1631
|
-
const planned = {
|
|
1632
|
-
status: plannedGroup.status,
|
|
1633
|
-
completion: plannedGroup.completion,
|
|
1634
|
-
checkpointOutcome: plannedGroup.checkpointOutcome,
|
|
1635
|
-
evidence: Array.isArray(plannedGroup.evidence) ? plannedGroup.evidence : [],
|
|
1636
|
-
nextAction: plannedGroup.nextAction,
|
|
1637
|
-
resumeCursor: normalizeResumeCursor(plannedGroup.resumeCursor, index)
|
|
1638
|
-
};
|
|
1639
|
-
const implementer = buildTaskGroupImplementerState(taskGroupId, signals, seed.implementer);
|
|
1640
|
-
const review = buildTaskGroupReviewState(plannedGroup, signals, seed.review);
|
|
1641
|
-
const effective = buildEffectiveTaskGroupState(plannedGroup, planned, implementer, review);
|
|
1642
|
-
if (TRACEABLE_TASK_GROUP_FOCUS_REASONS.has(effective.reason)) {
|
|
1643
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
1644
|
-
decisionFamily: "task_group_focus_resolution",
|
|
1645
|
-
decisionKey: effective.reason,
|
|
1646
|
-
outcome: "selected_focus",
|
|
1647
|
-
reasonSummary: buildTaskGroupFocusReasonSummary(taskGroupId, effective.reason),
|
|
1648
|
-
context: {
|
|
1649
|
-
taskGroupId,
|
|
1650
|
-
plannedStatus: planned.status || null,
|
|
1651
|
-
effectiveStatus: effective.status || null,
|
|
1652
|
-
liveFocus: effective.resumeCursor ? effective.resumeCursor.liveFocus || null : null,
|
|
1653
|
-
nextAction: effective.nextAction || null
|
|
1654
|
-
},
|
|
1655
|
-
evidenceRefs: buildTaskGroupFocusEvidenceRefs(taskGroupId, effective.reason)
|
|
1656
|
-
});
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
return {
|
|
1660
|
-
taskGroupId,
|
|
1661
|
-
title: plannedGroup.title,
|
|
1662
|
-
status: effective.status,
|
|
1663
|
-
completion: planned.completion,
|
|
1664
|
-
checkpointOutcome: planned.checkpointOutcome,
|
|
1665
|
-
evidence: planned.evidence,
|
|
1666
|
-
nextAction: effective.nextAction,
|
|
1667
|
-
targetFiles: Array.isArray(plannedGroup.targetFiles) ? plannedGroup.targetFiles : [],
|
|
1668
|
-
fileReferences: Array.isArray(plannedGroup.fileReferences) ? plannedGroup.fileReferences : [],
|
|
1669
|
-
verificationActions: Array.isArray(plannedGroup.verificationActions)
|
|
1670
|
-
? plannedGroup.verificationActions
|
|
1671
|
-
: [],
|
|
1672
|
-
verificationCommands: Array.isArray(plannedGroup.verificationCommands)
|
|
1673
|
-
? plannedGroup.verificationCommands
|
|
1674
|
-
: [],
|
|
1675
|
-
executionIntent: Array.isArray(plannedGroup.executionIntent) ? plannedGroup.executionIntent : [],
|
|
1676
|
-
reviewIntent: plannedGroup.reviewIntent === true,
|
|
1677
|
-
testingIntent: plannedGroup.testingIntent === true,
|
|
1678
|
-
codeChangeLikely: plannedGroup.codeChangeLikely === true,
|
|
1679
|
-
resumeCursor: effective.resumeCursor,
|
|
1680
|
-
planned,
|
|
1681
|
-
implementer,
|
|
1682
|
-
review,
|
|
1683
|
-
effective
|
|
1684
|
-
};
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
function buildTaskGroupMetadataPayload(changeId, checkpointStatuses, taskGroups) {
|
|
1689
|
-
return {
|
|
1690
|
-
version: TASK_GROUP_METADATA_VERSION,
|
|
1691
|
-
changeId,
|
|
1692
|
-
checkpointOutcome: checkpointStatuses[CHECKPOINT_LABELS.TASK] || STATUS.WARN,
|
|
1693
|
-
taskGroups: Array.isArray(taskGroups) ? taskGroups : [],
|
|
1694
|
-
updatedAt: new Date().toISOString()
|
|
1695
|
-
};
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function loadTaskGroupMetadataFromPath(targetPath) {
|
|
1699
|
-
if (!targetPath || !pathExists(targetPath)) {
|
|
1700
|
-
return null;
|
|
1701
|
-
}
|
|
1702
|
-
try {
|
|
1703
|
-
return JSON.parse(readTextIfExists(targetPath));
|
|
1704
|
-
} catch (_error) {
|
|
1705
|
-
return null;
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function resolvePersistedTaskGroupSeed(
|
|
1710
|
-
projectRoot,
|
|
1711
|
-
changeId,
|
|
1712
|
-
persistedRecord,
|
|
1713
|
-
plannedTaskGroups,
|
|
1714
|
-
decisionTraceRecords
|
|
1715
|
-
) {
|
|
1716
|
-
const metadataRefs =
|
|
1717
|
-
persistedRecord && persistedRecord.metadataRefs && typeof persistedRecord.metadataRefs === "object"
|
|
1718
|
-
? persistedRecord.metadataRefs
|
|
1719
|
-
: {};
|
|
1720
|
-
const canonicalPath =
|
|
1721
|
-
metadataRefs.taskGroupsPath || resolveTaskGroupMetadataPath(projectRoot, changeId);
|
|
1722
|
-
const notes = [];
|
|
1723
|
-
|
|
1724
|
-
if (canonicalPath && pathExists(canonicalPath)) {
|
|
1725
|
-
const actualDigest = digestForPath(canonicalPath);
|
|
1726
|
-
const expectedDigest = metadataRefs.taskGroupsDigest || null;
|
|
1727
|
-
if (expectedDigest && actualDigest && expectedDigest !== actualDigest) {
|
|
1728
|
-
const message =
|
|
1729
|
-
"Canonical task-group runtime state digest mismatch; rebuilding task-group state from artifacts.";
|
|
1730
|
-
notes.push(message);
|
|
1731
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
1732
|
-
decisionFamily: "task_group_seed_fallback",
|
|
1733
|
-
decisionKey: TASK_GROUP_SEED_TRACE_KEYS["digest-mismatch"],
|
|
1734
|
-
outcome: "fallback",
|
|
1735
|
-
reasonSummary: message,
|
|
1736
|
-
context: {
|
|
1737
|
-
metadataPath: formatPathRef(projectRoot, canonicalPath),
|
|
1738
|
-
taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0,
|
|
1739
|
-
expectedDigestPresent: true
|
|
1740
|
-
},
|
|
1741
|
-
evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
|
|
1742
|
-
});
|
|
201
|
+
case "design":
|
|
1743
202
|
return {
|
|
1744
|
-
|
|
1745
|
-
|
|
203
|
+
route: "/dv:design",
|
|
204
|
+
command: "/dv:design",
|
|
205
|
+
reason: "Design artifacts or design checkpoints are not ready."
|
|
1746
206
|
};
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
207
|
+
case "tasks":
|
|
208
|
+
return {
|
|
209
|
+
route: "/dv:tasks",
|
|
210
|
+
command: "/dv:tasks",
|
|
211
|
+
reason: "Task planning artifacts are missing or blocked."
|
|
212
|
+
};
|
|
213
|
+
case "build":
|
|
1753
214
|
return {
|
|
1754
|
-
|
|
1755
|
-
|
|
215
|
+
route: "/dv:build",
|
|
216
|
+
command: "/dv:build",
|
|
217
|
+
reason: "Implementation should proceed before verification."
|
|
218
|
+
};
|
|
219
|
+
case "verify":
|
|
220
|
+
return {
|
|
221
|
+
route: "/dv:verify",
|
|
222
|
+
command: "/dv:verify",
|
|
223
|
+
reason: "Verification and completion readiness are not satisfied yet."
|
|
224
|
+
};
|
|
225
|
+
default:
|
|
226
|
+
return {
|
|
227
|
+
route: null,
|
|
228
|
+
command: null,
|
|
229
|
+
reason: "No next route. Workflow appears complete."
|
|
1756
230
|
};
|
|
1757
|
-
}
|
|
1758
|
-
{
|
|
1759
|
-
const message =
|
|
1760
|
-
"Canonical task-group runtime state is unreadable; rebuilding task-group state from artifacts.";
|
|
1761
|
-
notes.push(message);
|
|
1762
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
1763
|
-
decisionFamily: "task_group_seed_fallback",
|
|
1764
|
-
decisionKey: TASK_GROUP_SEED_TRACE_KEYS.unreadable,
|
|
1765
|
-
outcome: "fallback",
|
|
1766
|
-
reasonSummary: message,
|
|
1767
|
-
context: {
|
|
1768
|
-
metadataPath: formatPathRef(projectRoot, canonicalPath),
|
|
1769
|
-
taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
|
|
1770
|
-
},
|
|
1771
|
-
evidenceRefs: [`state:${formatPathRef(projectRoot, canonicalPath)}`]
|
|
1772
|
-
});
|
|
1773
|
-
}
|
|
1774
|
-
return {
|
|
1775
|
-
taskGroups: plannedTaskGroups,
|
|
1776
|
-
notes
|
|
1777
|
-
};
|
|
1778
231
|
}
|
|
232
|
+
}
|
|
1779
233
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
context: {
|
|
1790
|
-
metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
|
|
1791
|
-
taskGroupCount: persistedRecord.taskGroups.length
|
|
1792
|
-
},
|
|
1793
|
-
evidenceRefs: [`state:${formatPathRef(projectRoot, resolveWorkflowStatePath(projectRoot))}`]
|
|
1794
|
-
});
|
|
1795
|
-
}
|
|
1796
|
-
return {
|
|
1797
|
-
taskGroups: persistedRecord.taskGroups,
|
|
1798
|
-
notes
|
|
1799
|
-
};
|
|
234
|
+
/**
|
|
235
|
+
* Collapse findings into the top-level workflow status token.
|
|
236
|
+
*
|
|
237
|
+
* @param {{ blockers: string[], warnings: string[] }} findings
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
function statusFromFindings(findings) {
|
|
241
|
+
if (findings.blockers.length > 0) {
|
|
242
|
+
return STATUS.BLOCK;
|
|
1800
243
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
const message = "Canonical task-group runtime state is missing; rebuilding task-group state from artifacts.";
|
|
1804
|
-
notes.push(message);
|
|
1805
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
1806
|
-
decisionFamily: "task_group_seed_fallback",
|
|
1807
|
-
decisionKey: TASK_GROUP_SEED_TRACE_KEYS.missing,
|
|
1808
|
-
outcome: "fallback",
|
|
1809
|
-
reasonSummary: message,
|
|
1810
|
-
context: {
|
|
1811
|
-
metadataPath: canonicalPath ? formatPathRef(projectRoot, canonicalPath) : null,
|
|
1812
|
-
taskGroupCount: Array.isArray(plannedTaskGroups) ? plannedTaskGroups.length : 0
|
|
1813
|
-
},
|
|
1814
|
-
evidenceRefs:
|
|
1815
|
-
canonicalPath && String(canonicalPath).trim()
|
|
1816
|
-
? [`state:${formatPathRef(projectRoot, canonicalPath)}`]
|
|
1817
|
-
: []
|
|
1818
|
-
});
|
|
244
|
+
if (findings.warnings.length > 0) {
|
|
245
|
+
return STATUS.WARN;
|
|
1819
246
|
}
|
|
1820
|
-
return
|
|
1821
|
-
taskGroups: plannedTaskGroups,
|
|
1822
|
-
notes
|
|
1823
|
-
};
|
|
247
|
+
return STATUS.PASS;
|
|
1824
248
|
}
|
|
1825
249
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Convert an overlay result into the stable workflow-status result schema.
|
|
252
|
+
*
|
|
253
|
+
* @param {{
|
|
254
|
+
* projectRoot: string,
|
|
255
|
+
* changeId: string | null,
|
|
256
|
+
* overlay: {
|
|
257
|
+
* stageId: string,
|
|
258
|
+
* findings: { blockers: string[], warnings: string[], notes: string[] },
|
|
259
|
+
* gates: Object.<string, string>,
|
|
260
|
+
* taskGroups: Array<object>,
|
|
261
|
+
* executionProfile?: object,
|
|
262
|
+
* worktreePreflight?: object | null,
|
|
263
|
+
* blockingGate?: object | null,
|
|
264
|
+
* needsRerunSurfaces?: string[],
|
|
265
|
+
* stalePlanningSignals?: Object.<string, object>
|
|
266
|
+
* },
|
|
267
|
+
* checkpoints?: Object.<string, string>,
|
|
268
|
+
* audits?: { integrity: object | null, completion: object | null },
|
|
269
|
+
* routeContext?: object,
|
|
270
|
+
* source?: string,
|
|
271
|
+
* discipline?: object | null,
|
|
272
|
+
* verificationFreshness?: object | null
|
|
273
|
+
* }} params
|
|
274
|
+
* @returns {object}
|
|
275
|
+
*/
|
|
276
|
+
function buildWorkflowResultFromOverlay(params) {
|
|
277
|
+
return buildWorkflowResult({
|
|
278
|
+
projectRoot: params.projectRoot,
|
|
279
|
+
changeId: params.changeId,
|
|
280
|
+
stageId: params.overlay.stageId,
|
|
281
|
+
findings: params.overlay.findings,
|
|
282
|
+
checkpoints: params.checkpoints || {},
|
|
283
|
+
gates: params.overlay.gates,
|
|
284
|
+
audits: params.audits || {
|
|
285
|
+
integrity: null,
|
|
286
|
+
completion: null
|
|
287
|
+
},
|
|
288
|
+
routeContext: params.routeContext,
|
|
289
|
+
source: params.source || "derived",
|
|
290
|
+
taskGroups: params.overlay.taskGroups,
|
|
291
|
+
discipline: params.discipline || null,
|
|
292
|
+
executionProfile: params.overlay.executionProfile || null,
|
|
293
|
+
worktreePreflight: params.overlay.worktreePreflight || null,
|
|
294
|
+
verificationFreshness: params.verificationFreshness || null,
|
|
295
|
+
blockingGate: params.overlay.blockingGate || null,
|
|
296
|
+
needsRerunSurfaces: params.overlay.needsRerunSurfaces,
|
|
297
|
+
stalePlanningSignals: params.overlay.stalePlanningSignals
|
|
298
|
+
});
|
|
1839
299
|
}
|
|
1840
300
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Shared base-view pipeline:
|
|
303
|
+
* 1. derive task-group runtime state from the chosen base seed,
|
|
304
|
+
* 2. apply workflow overlays,
|
|
305
|
+
* 3. normalize the final workflow result shape.
|
|
306
|
+
*
|
|
307
|
+
* @param {{
|
|
308
|
+
* projectRoot: string,
|
|
309
|
+
* changeId: string | null,
|
|
310
|
+
* baseView: {
|
|
311
|
+
* source: string,
|
|
312
|
+
* stageId: string,
|
|
313
|
+
* findings: { blockers: string[], warnings: string[], notes: string[] },
|
|
314
|
+
* baseGates: Object.<string, string>,
|
|
315
|
+
* completionAudit: object | null,
|
|
316
|
+
* taskGroupSeed: Array<object>
|
|
317
|
+
* },
|
|
318
|
+
* plannedTaskGroups: Array<object>,
|
|
319
|
+
* changeSignals: Array<object>,
|
|
320
|
+
* signalSummary: Object.<string, object>,
|
|
321
|
+
* planningSignalFreshness: object,
|
|
322
|
+
* integrityAudit: object | null,
|
|
323
|
+
* disciplineState: object | null,
|
|
324
|
+
* verificationFreshness: object | null,
|
|
325
|
+
* hasTasksArtifact: boolean,
|
|
326
|
+
* decisionTraceRecords: Array<object> | null,
|
|
327
|
+
* checkpoints: Object.<string, string>,
|
|
328
|
+
* routeContext: object
|
|
329
|
+
* }} [options]
|
|
330
|
+
* @returns {object}
|
|
331
|
+
*/
|
|
332
|
+
function buildWorkflowResultForBaseView(options = {}) {
|
|
1869
333
|
const taskGroups = deriveTaskGroupRuntimeState(
|
|
1870
334
|
options.plannedTaskGroups,
|
|
1871
335
|
options.changeSignals,
|
|
1872
|
-
options.taskGroupSeed,
|
|
1873
|
-
decisionTraceRecords
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
|
|
1877
|
-
stageId = applyPlanningSignalFreshnessFindings(
|
|
1878
|
-
stageId,
|
|
1879
|
-
findings,
|
|
1880
|
-
planningSignalFreshness,
|
|
1881
|
-
decisionTraceRecords
|
|
1882
|
-
);
|
|
1883
|
-
stageId = applyExecutionSignalFindings(stageId, findings, planningSignalFreshness.effectiveSignalSummary || {});
|
|
1884
|
-
applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
|
|
1885
|
-
|
|
1886
|
-
if (disciplineState) {
|
|
1887
|
-
findings.blockers.push(...disciplineState.blockers);
|
|
1888
|
-
findings.warnings.push(...disciplineState.warnings);
|
|
1889
|
-
findings.notes.push(...disciplineState.notes);
|
|
1890
|
-
if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
|
|
1891
|
-
stageId = options.hasTasksArtifact ? "tasks" : "design";
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
|
|
1896
|
-
const stageBeforeFreshness = stageId;
|
|
1897
|
-
findings.blockers.push(
|
|
1898
|
-
"Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
|
|
1899
|
-
);
|
|
1900
|
-
stageId = "verify";
|
|
1901
|
-
if (stageBeforeFreshness === "complete") {
|
|
1902
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
1903
|
-
decisionFamily: "verification_freshness_downgrade",
|
|
1904
|
-
decisionKey: "verification_freshness_stale",
|
|
1905
|
-
outcome: "downgraded",
|
|
1906
|
-
reasonSummary: "Completion-facing routing stays in verify because verification evidence is stale.",
|
|
1907
|
-
context: {
|
|
1908
|
-
fromStage: "complete",
|
|
1909
|
-
toStage: "verify",
|
|
1910
|
-
baselineIso: verificationFreshness.baselineIso || null,
|
|
1911
|
-
staleReasonCount: Array.isArray(verificationFreshness.staleReasons)
|
|
1912
|
-
? verificationFreshness.staleReasons.length
|
|
1913
|
-
: 0,
|
|
1914
|
-
requiredSurfaces: Array.isArray(verificationFreshness.requiredSurfaces)
|
|
1915
|
-
? verificationFreshness.requiredSurfaces
|
|
1916
|
-
: []
|
|
1917
|
-
},
|
|
1918
|
-
evidenceRefs: collectVerificationFreshnessEvidenceRefs(verificationFreshness)
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
const gates = buildGatesWithLiveOverlays(
|
|
1924
|
-
options.baseGates,
|
|
1925
|
-
completionAudit,
|
|
1926
|
-
disciplineState,
|
|
1927
|
-
verificationFreshness
|
|
336
|
+
options.baseView.taskGroupSeed,
|
|
337
|
+
options.decisionTraceRecords,
|
|
338
|
+
recordWorkflowDecision
|
|
1928
339
|
);
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
|
|
340
|
+
const overlay = applyWorkflowOverlays({
|
|
341
|
+
projectRoot: options.projectRoot,
|
|
342
|
+
changeId: options.changeId,
|
|
343
|
+
stageId: options.baseView.stageId,
|
|
344
|
+
findings: options.baseView.findings,
|
|
345
|
+
baseGates: options.baseView.baseGates,
|
|
346
|
+
taskGroups,
|
|
347
|
+
changeSignals: options.changeSignals,
|
|
348
|
+
signalSummary: options.signalSummary,
|
|
349
|
+
planningSignalFreshness: options.planningSignalFreshness,
|
|
350
|
+
integrityAudit: options.integrityAudit,
|
|
351
|
+
completionAudit: options.baseView.completionAudit,
|
|
352
|
+
disciplineState: options.disciplineState,
|
|
353
|
+
verificationFreshness: options.verificationFreshness,
|
|
354
|
+
hasTasksArtifact: options.hasTasksArtifact,
|
|
355
|
+
decisionTraceRecords: options.decisionTraceRecords,
|
|
356
|
+
recordWorkflowDecision
|
|
1932
357
|
});
|
|
1933
|
-
let worktreePreflight = null;
|
|
1934
|
-
if (options.changeId && (stageId === "build" || stageId === "verify")) {
|
|
1935
|
-
worktreePreflight = runWorktreePreflight(options.projectRoot, {
|
|
1936
|
-
parallelPreferred: executionProfile.mode === "bounded_parallel"
|
|
1937
|
-
});
|
|
1938
|
-
if (
|
|
1939
|
-
executionProfile.mode === "bounded_parallel" &&
|
|
1940
|
-
worktreePreflight.summary &&
|
|
1941
|
-
worktreePreflight.summary.recommendedIsolation
|
|
1942
|
-
) {
|
|
1943
|
-
executionProfile.effectiveMode = "serial";
|
|
1944
|
-
executionProfile.rationale = dedupeMessages([
|
|
1945
|
-
...(executionProfile.rationale || []),
|
|
1946
|
-
"worktree preflight recommends isolation; effective mode downgraded to serial"
|
|
1947
|
-
]);
|
|
1948
|
-
findings.warnings.push(
|
|
1949
|
-
"Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
|
|
1950
|
-
);
|
|
1951
|
-
recordWorkflowDecision(decisionTraceRecords, {
|
|
1952
|
-
decisionFamily: "worktree_isolation_downgrade",
|
|
1953
|
-
decisionKey: "effective_serial_after_preflight",
|
|
1954
|
-
outcome: "downgraded",
|
|
1955
|
-
reasonSummary: "Worktree preflight downgraded advisory bounded parallel execution to effective serial mode.",
|
|
1956
|
-
context: {
|
|
1957
|
-
advisoryMode: executionProfile.mode,
|
|
1958
|
-
effectiveMode: executionProfile.effectiveMode || "serial",
|
|
1959
|
-
preflightStatus: worktreePreflight.status || null,
|
|
1960
|
-
recommendedIsolation: Boolean(
|
|
1961
|
-
worktreePreflight.summary && worktreePreflight.summary.recommendedIsolation
|
|
1962
|
-
),
|
|
1963
|
-
dirtyEntries:
|
|
1964
|
-
worktreePreflight.summary &&
|
|
1965
|
-
Number.isFinite(Number(worktreePreflight.summary.dirtyEntries))
|
|
1966
|
-
? Number(worktreePreflight.summary.dirtyEntries)
|
|
1967
|
-
: 0
|
|
1968
|
-
},
|
|
1969
|
-
evidenceRefs: ["surface:worktree-preflight"]
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
dedupeFindings(findings);
|
|
1975
|
-
const blockingGate = selectBlockingGateIdentity(findings);
|
|
1976
358
|
|
|
1977
|
-
return
|
|
359
|
+
return buildWorkflowResultFromOverlay({
|
|
1978
360
|
projectRoot: options.projectRoot,
|
|
1979
361
|
changeId: options.changeId,
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
362
|
+
checkpoints: options.checkpoints,
|
|
363
|
+
routeContext: options.routeContext,
|
|
364
|
+
source: options.baseView.source,
|
|
365
|
+
overlay,
|
|
1984
366
|
audits: {
|
|
1985
|
-
integrity: integrityAudit,
|
|
1986
|
-
completion: completionAudit
|
|
367
|
+
integrity: options.integrityAudit,
|
|
368
|
+
completion: options.baseView.completionAudit
|
|
1987
369
|
},
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
taskGroups,
|
|
1991
|
-
discipline: disciplineState,
|
|
1992
|
-
executionProfile,
|
|
1993
|
-
worktreePreflight,
|
|
1994
|
-
verificationFreshness,
|
|
1995
|
-
blockingGate,
|
|
1996
|
-
needsRerunSurfaces: planningSignalFreshness.needsRerunSurfaces,
|
|
1997
|
-
stalePlanningSignals: planningSignalFreshness.stalePlanningSignals
|
|
370
|
+
discipline: options.disciplineState,
|
|
371
|
+
verificationFreshness: options.verificationFreshness
|
|
1998
372
|
});
|
|
1999
373
|
}
|
|
2000
374
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
? left.resumeCursor.groupIndex
|
|
2021
|
-
: Number.MAX_SAFE_INTEGER;
|
|
2022
|
-
const rightIndex =
|
|
2023
|
-
right && right.resumeCursor && Number.isInteger(right.resumeCursor.groupIndex)
|
|
2024
|
-
? right.resumeCursor.groupIndex
|
|
2025
|
-
: Number.MAX_SAFE_INTEGER;
|
|
2026
|
-
return leftIndex - rightIndex;
|
|
2027
|
-
})[0] || null;
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
function statusFromFindings(findings) {
|
|
2031
|
-
if (findings.blockers.length > 0) {
|
|
2032
|
-
return STATUS.BLOCK;
|
|
2033
|
-
}
|
|
2034
|
-
if (findings.warnings.length > 0) {
|
|
2035
|
-
return STATUS.WARN;
|
|
2036
|
-
}
|
|
2037
|
-
return STATUS.PASS;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Main workflow-status orchestrator.
|
|
377
|
+
*
|
|
378
|
+
* Responsibilities:
|
|
379
|
+
* 1. resolve the active project/change context,
|
|
380
|
+
* 2. gather artifact, signal, audit, and freshness inputs,
|
|
381
|
+
* 3. choose a trusted persisted or artifact-derived base view,
|
|
382
|
+
* 4. apply runtime overlays,
|
|
383
|
+
* 5. persist refreshed derived snapshots when appropriate.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} projectPathInput
|
|
386
|
+
* @param {{
|
|
387
|
+
* changeId?: string,
|
|
388
|
+
* staleWindowMs?: number,
|
|
389
|
+
* env?: object,
|
|
390
|
+
* traceSurface?: string
|
|
391
|
+
* }} [options]
|
|
392
|
+
* @returns {object}
|
|
393
|
+
*/
|
|
2040
394
|
function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
2041
395
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
2042
396
|
const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
|
|
@@ -2153,15 +507,6 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
2153
507
|
}
|
|
2154
508
|
: null;
|
|
2155
509
|
const integrityAudit = collectIntegrityAudit(projectRoot, workflowRoot, routingSignalSummary);
|
|
2156
|
-
const designCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.DESIGN]);
|
|
2157
|
-
const designSourceCheckpointStatus = normalizeCheckpointStatus(
|
|
2158
|
-
checkpointStatuses[CHECKPOINT_LABELS.DESIGN_SOURCE]
|
|
2159
|
-
);
|
|
2160
|
-
const runtimeGateCheckpointStatus = normalizeCheckpointStatus(
|
|
2161
|
-
checkpointStatuses[CHECKPOINT_LABELS.RUNTIME_GATE]
|
|
2162
|
-
);
|
|
2163
|
-
const mappingCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.MAPPING]);
|
|
2164
|
-
const taskCheckpointStatus = normalizeCheckpointStatus(checkpointStatuses[CHECKPOINT_LABELS.TASK]);
|
|
2165
510
|
const verificationFreshness = activeChangeId
|
|
2166
511
|
? collectVerificationFreshness(projectRoot, {
|
|
2167
512
|
changeId: activeChangeId,
|
|
@@ -2175,6 +520,8 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
2175
520
|
ambiguousChangeSelection
|
|
2176
521
|
};
|
|
2177
522
|
|
|
523
|
+
// Persisted snapshots can be reused only when the fingerprint still matches
|
|
524
|
+
// current artifact/signal truth; otherwise we fall back to the derived path.
|
|
2178
525
|
if (activeChangeId) {
|
|
2179
526
|
const persistedSelection = selectPersistedStateForChange(projectRoot, activeChangeId, {
|
|
2180
527
|
staleWindowMs: options.staleWindowMs
|
|
@@ -2201,51 +548,49 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
2201
548
|
evidenceRefs: [`state:${formatPathRef(projectRoot, persistedSelection.statePath)}`]
|
|
2202
549
|
});
|
|
2203
550
|
const persistedRecord = persistedSelection.changeRecord;
|
|
2204
|
-
const stageRecord = getStageById(persistedRecord.stage) || getStageById("bootstrap");
|
|
2205
|
-
const completionAudit =
|
|
2206
|
-
stageRecord.id === "verify" || stageRecord.id === "complete"
|
|
2207
|
-
? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
|
|
2208
|
-
: null;
|
|
2209
551
|
const persistedSeed = resolvePersistedTaskGroupSeed(
|
|
2210
552
|
projectRoot,
|
|
2211
553
|
activeChangeId,
|
|
2212
554
|
persistedRecord,
|
|
2213
555
|
plannedTaskGroups,
|
|
2214
|
-
decisionTraceRecords
|
|
556
|
+
decisionTraceRecords,
|
|
557
|
+
recordWorkflowDecision
|
|
2215
558
|
);
|
|
559
|
+
const persistedBaseView = buildPersistedWorkflowBaseView({
|
|
560
|
+
persistedRecord,
|
|
561
|
+
findings: {
|
|
562
|
+
blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
|
|
563
|
+
warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
|
|
564
|
+
notes: [
|
|
565
|
+
...sanitizePersistedNotes(persistedRecord.notes),
|
|
566
|
+
...(Array.isArray(persistedSelection.advisoryNotes) ? persistedSelection.advisoryNotes : []),
|
|
567
|
+
...persistedSeed.notes,
|
|
568
|
+
"workflow-status is using trusted persisted workflow state."
|
|
569
|
+
]
|
|
570
|
+
},
|
|
571
|
+
taskGroupSeed: persistedSeed.taskGroups,
|
|
572
|
+
resolveCompletionAudit(stageId) {
|
|
573
|
+
return stageId === "verify" || stageId === "complete"
|
|
574
|
+
? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
|
|
575
|
+
: null;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
2216
578
|
return finalizeResultWithWorkflowDecisionTracing(
|
|
2217
|
-
|
|
579
|
+
buildWorkflowResultForBaseView({
|
|
2218
580
|
projectRoot,
|
|
2219
581
|
changeId: activeChangeId,
|
|
2220
|
-
|
|
2221
|
-
findings: {
|
|
2222
|
-
blockers: Array.isArray(persistedRecord.failures) ? persistedRecord.failures.slice() : [],
|
|
2223
|
-
warnings: Array.isArray(persistedRecord.warnings) ? persistedRecord.warnings.slice() : [],
|
|
2224
|
-
notes: [
|
|
2225
|
-
...sanitizePersistedNotes(persistedRecord.notes),
|
|
2226
|
-
...(Array.isArray(persistedSelection.advisoryNotes) ? persistedSelection.advisoryNotes : []),
|
|
2227
|
-
...persistedSeed.notes,
|
|
2228
|
-
"workflow-status is using trusted persisted workflow state."
|
|
2229
|
-
]
|
|
2230
|
-
},
|
|
2231
|
-
baseGates:
|
|
2232
|
-
persistedRecord && persistedRecord.gates && typeof persistedRecord.gates === "object"
|
|
2233
|
-
? { ...persistedRecord.gates }
|
|
2234
|
-
: {},
|
|
2235
|
-
checkpoints: checkpointStatuses,
|
|
2236
|
-
routeContext,
|
|
2237
|
-
source: "persisted",
|
|
2238
|
-
taskGroupSeed: persistedSeed.taskGroups,
|
|
582
|
+
baseView: persistedBaseView,
|
|
2239
583
|
plannedTaskGroups,
|
|
2240
584
|
changeSignals,
|
|
2241
585
|
signalSummary,
|
|
2242
586
|
planningSignalFreshness,
|
|
2243
587
|
integrityAudit,
|
|
2244
|
-
completionAudit,
|
|
2245
588
|
disciplineState,
|
|
2246
589
|
verificationFreshness,
|
|
2247
590
|
hasTasksArtifact: artifactState.tasks,
|
|
2248
|
-
decisionTraceRecords
|
|
591
|
+
decisionTraceRecords,
|
|
592
|
+
checkpoints: checkpointStatuses,
|
|
593
|
+
routeContext
|
|
2249
594
|
}),
|
|
2250
595
|
{
|
|
2251
596
|
env: options.env,
|
|
@@ -2291,56 +636,36 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
2291
636
|
warnings: findings.warnings.slice(),
|
|
2292
637
|
notes: findings.notes.slice()
|
|
2293
638
|
};
|
|
2294
|
-
const stageId = deriveStageFromArtifacts(artifactState, checkpointStatuses, baseFindings);
|
|
2295
|
-
const completionAudit =
|
|
2296
|
-
stageId === "verify" || stageId === "complete"
|
|
2297
|
-
? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
|
|
2298
|
-
: null;
|
|
2299
|
-
const baseGates = {
|
|
2300
|
-
[HANDOFF_GATES.BREAKDOWN_TO_DESIGN]:
|
|
2301
|
-
artifactState.proposal && artifactState.specFiles.length > 0 ? STATUS.PASS : STATUS.BLOCK,
|
|
2302
|
-
[HANDOFF_GATES.DESIGN_TO_TASKS]: mergeStatuses([
|
|
2303
|
-
artifactState.design && artifactState.pencilDesign && artifactState.pencilBindings
|
|
2304
|
-
? STATUS.PASS
|
|
2305
|
-
: STATUS.BLOCK,
|
|
2306
|
-
designCheckpointStatus,
|
|
2307
|
-
designSourceCheckpointStatus,
|
|
2308
|
-
runtimeGateCheckpointStatus,
|
|
2309
|
-
mappingCheckpointStatus
|
|
2310
|
-
]),
|
|
2311
|
-
[HANDOFF_GATES.TASKS_TO_BUILD]: mergeStatuses([
|
|
2312
|
-
artifactState.tasks ? STATUS.PASS : STATUS.BLOCK,
|
|
2313
|
-
designCheckpointStatus,
|
|
2314
|
-
designSourceCheckpointStatus,
|
|
2315
|
-
runtimeGateCheckpointStatus,
|
|
2316
|
-
mappingCheckpointStatus,
|
|
2317
|
-
taskCheckpointStatus
|
|
2318
|
-
]),
|
|
2319
|
-
[HANDOFF_GATES.BUILD_TO_VERIFY]: artifactState.verification ? STATUS.PASS : STATUS.BLOCK,
|
|
2320
|
-
[HANDOFF_GATES.VERIFY_TO_COMPLETE]:
|
|
2321
|
-
completionAudit && completionAudit.status === "PASS" ? STATUS.PASS : STATUS.WARN
|
|
2322
|
-
};
|
|
2323
639
|
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
640
|
+
// The derived path rebuilds base routing from artifacts, then re-applies
|
|
641
|
+
// live overlays before refreshing canonical task-group metadata and the
|
|
642
|
+
// persisted workflow snapshot.
|
|
643
|
+
const derivedBaseView = buildDerivedWorkflowBaseView({
|
|
644
|
+
artifactState,
|
|
645
|
+
checkpointStatuses,
|
|
2328
646
|
findings: baseFindings,
|
|
2329
|
-
baseGates,
|
|
2330
|
-
checkpoints: checkpointStatuses,
|
|
2331
|
-
routeContext,
|
|
2332
|
-
source: "derived",
|
|
2333
647
|
taskGroupSeed: plannedTaskGroups,
|
|
648
|
+
resolveCompletionAudit(stageId) {
|
|
649
|
+
return stageId === "verify" || stageId === "complete"
|
|
650
|
+
? collectCompletionAudit(projectRoot, workflowRoot, activeChangeId, routingSignalSummary)
|
|
651
|
+
: null;
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
const derivedResult = buildWorkflowResultForBaseView({
|
|
655
|
+
projectRoot,
|
|
656
|
+
changeId: activeChangeId,
|
|
657
|
+
baseView: derivedBaseView,
|
|
2334
658
|
plannedTaskGroups,
|
|
2335
659
|
changeSignals,
|
|
2336
660
|
signalSummary,
|
|
2337
661
|
planningSignalFreshness,
|
|
2338
662
|
integrityAudit,
|
|
2339
|
-
completionAudit,
|
|
2340
663
|
disciplineState,
|
|
2341
664
|
verificationFreshness,
|
|
2342
665
|
hasTasksArtifact: artifactState.tasks,
|
|
2343
|
-
decisionTraceRecords
|
|
666
|
+
decisionTraceRecords,
|
|
667
|
+
checkpoints: checkpointStatuses,
|
|
668
|
+
routeContext
|
|
2344
669
|
});
|
|
2345
670
|
|
|
2346
671
|
if (activeChangeId && !ambiguousChangeSelection) {
|
|
@@ -2417,6 +742,30 @@ function deriveWorkflowStatus(projectPathInput, options = {}) {
|
|
|
2417
742
|
});
|
|
2418
743
|
}
|
|
2419
744
|
|
|
745
|
+
/**
|
|
746
|
+
* Build the public workflow-status payload.
|
|
747
|
+
*
|
|
748
|
+
* @param {{
|
|
749
|
+
* projectRoot: string,
|
|
750
|
+
* changeId: string | null,
|
|
751
|
+
* stageId: string,
|
|
752
|
+
* findings: { blockers: string[], warnings: string[], notes: string[] },
|
|
753
|
+
* checkpoints: Object.<string, string>,
|
|
754
|
+
* gates: Object.<string, string>,
|
|
755
|
+
* audits: { integrity: object | null, completion: object | null },
|
|
756
|
+
* routeContext: object,
|
|
757
|
+
* source?: string,
|
|
758
|
+
* taskGroups?: Array<object>,
|
|
759
|
+
* discipline?: object | null,
|
|
760
|
+
* executionProfile?: object | null,
|
|
761
|
+
* worktreePreflight?: object | null,
|
|
762
|
+
* verificationFreshness?: object | null,
|
|
763
|
+
* blockingGate?: object | null,
|
|
764
|
+
* needsRerunSurfaces?: string[],
|
|
765
|
+
* stalePlanningSignals?: Object.<string, object>
|
|
766
|
+
* }} params
|
|
767
|
+
* @returns {object}
|
|
768
|
+
*/
|
|
2420
769
|
function buildWorkflowResult(params) {
|
|
2421
770
|
const stage = getStageById(params.stageId) || getStageById("bootstrap");
|
|
2422
771
|
const nextStep = buildRouteRecommendation(params.stageId, params.routeContext);
|
|
@@ -2452,6 +801,12 @@ function buildWorkflowResult(params) {
|
|
|
2452
801
|
};
|
|
2453
802
|
}
|
|
2454
803
|
|
|
804
|
+
/**
|
|
805
|
+
* Render the human-readable `workflow-status` report.
|
|
806
|
+
*
|
|
807
|
+
* @param {object} result
|
|
808
|
+
* @returns {string}
|
|
809
|
+
*/
|
|
2455
810
|
function formatWorkflowStatusReport(result) {
|
|
2456
811
|
const lines = [
|
|
2457
812
|
"Da Vinci workflow-status",
|
|
@@ -2537,6 +892,12 @@ function formatWorkflowStatusReport(result) {
|
|
|
2537
892
|
return lines.join("\n");
|
|
2538
893
|
}
|
|
2539
894
|
|
|
895
|
+
/**
|
|
896
|
+
* Render the human-readable `next-step` report.
|
|
897
|
+
*
|
|
898
|
+
* @param {object} result
|
|
899
|
+
* @returns {string}
|
|
900
|
+
*/
|
|
2540
901
|
function formatNextStepReport(result) {
|
|
2541
902
|
const lines = [
|
|
2542
903
|
"Da Vinci next-step",
|