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