@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
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
parseDisciplineMarkers,
|
|
5
|
+
DISCIPLINE_MARKER_NAMES
|
|
6
|
+
} = require("./audit-parsers");
|
|
7
|
+
const { readTextIfExists, dedupeMessages } = require("./utils");
|
|
8
|
+
const { listSignalsBySurfacePrefix } = require("./execution-signals");
|
|
9
|
+
const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness");
|
|
10
|
+
const { deriveExecutionProfile } = require("./execution-profile");
|
|
11
|
+
const { runWorktreePreflight } = require("./worktree-preflight");
|
|
12
|
+
const { STATUS, HANDOFF_GATES, getStageById } = require("./workflow-contract");
|
|
13
|
+
|
|
14
|
+
const MAX_REPORTED_MESSAGES = 3;
|
|
15
|
+
const BLOCKING_GATE_PRIORITY = Object.freeze([
|
|
16
|
+
"clarify",
|
|
17
|
+
"scenarioQuality",
|
|
18
|
+
"analyze",
|
|
19
|
+
"taskCheckpoint",
|
|
20
|
+
"stalePlanningSignal",
|
|
21
|
+
"principleInheritance",
|
|
22
|
+
"lint-tasks",
|
|
23
|
+
"lint-spec",
|
|
24
|
+
"scope-check"
|
|
25
|
+
]);
|
|
26
|
+
const PLANNING_SIGNAL_PROMOTION_FALLBACKS = Object.freeze({
|
|
27
|
+
"lint-spec": "breakdown",
|
|
28
|
+
"scope-check": "tasks",
|
|
29
|
+
"lint-tasks": "tasks"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} WorkflowFindings
|
|
34
|
+
* @property {string[]} blockers
|
|
35
|
+
* @property {string[]} warnings
|
|
36
|
+
* @property {string[]} notes
|
|
37
|
+
* @property {Array<{ id: string, surface: string, reason: string, evidence: string[] }>} [blockingGates]
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} PlanningSignalFreshnessState
|
|
42
|
+
* @property {Object.<string, object>} effectiveSignalSummary
|
|
43
|
+
* @property {Object.<string, object>} stalePlanningSignals
|
|
44
|
+
* @property {string[]} needsRerunSurfaces
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} WorkflowDisciplineState
|
|
49
|
+
* @property {boolean} strictMode
|
|
50
|
+
* @property {boolean} hasAnyMarker
|
|
51
|
+
* @property {{ state: string, stale: boolean, marker: object | null }} designApproval
|
|
52
|
+
* @property {{ marker: object | null }} planSelfReview
|
|
53
|
+
* @property {{ marker: object | null }} operatorReviewAck
|
|
54
|
+
* @property {Array<object>} malformed
|
|
55
|
+
* @property {string[]} blockers
|
|
56
|
+
* @property {string[]} warnings
|
|
57
|
+
* @property {string[]} notes
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} WorkflowOverlayResult
|
|
62
|
+
* @property {string} stageId
|
|
63
|
+
* @property {WorkflowFindings} findings
|
|
64
|
+
* @property {Object.<string, string>} gates
|
|
65
|
+
* @property {Array<object>} taskGroups
|
|
66
|
+
* @property {object} executionProfile
|
|
67
|
+
* @property {object | null} worktreePreflight
|
|
68
|
+
* @property {object | null} blockingGate
|
|
69
|
+
* @property {string[]} needsRerunSurfaces
|
|
70
|
+
* @property {Object.<string, object>} stalePlanningSignals
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
function recordDecision(callback, records, record) {
|
|
74
|
+
if (typeof callback === "function") {
|
|
75
|
+
callback(records, record);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function dedupeFindings(findings) {
|
|
80
|
+
findings.blockers = dedupeMessages(findings.blockers);
|
|
81
|
+
findings.warnings = dedupeMessages(findings.warnings);
|
|
82
|
+
findings.notes = dedupeMessages(findings.notes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveDisciplineGateMode() {
|
|
86
|
+
const strictEnv = String(process.env.DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL || "").trim();
|
|
87
|
+
const strict = strictEnv === "1" || /^true$/i.test(strictEnv);
|
|
88
|
+
return {
|
|
89
|
+
strict
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isStrictPromotionEnabled() {
|
|
94
|
+
const raw = String(process.env.DA_VINCI_DISCIPLINE_STRICT_PROMOTION || "").trim();
|
|
95
|
+
return raw === "1" || /^true$/i.test(raw);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function fallbackStageIfBeyond(currentStageId, targetStageId) {
|
|
99
|
+
const current = getStageById(currentStageId);
|
|
100
|
+
const target = getStageById(targetStageId);
|
|
101
|
+
if (!current || !target) {
|
|
102
|
+
return currentStageId;
|
|
103
|
+
}
|
|
104
|
+
if (current.order > target.order) {
|
|
105
|
+
return target.id;
|
|
106
|
+
}
|
|
107
|
+
return current.id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureGateTracking(findings) {
|
|
111
|
+
if (!Array.isArray(findings.blockingGates)) {
|
|
112
|
+
findings.blockingGates = [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectGateEvidenceRefs(gate) {
|
|
117
|
+
return Array.isArray(gate && gate.evidence) ? gate.evidence.slice(0, MAX_REPORTED_MESSAGES) : [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addBlockingGateRecord(findings, gateId, surface, gate, reason) {
|
|
121
|
+
ensureGateTracking(findings);
|
|
122
|
+
findings.blockingGates.push({
|
|
123
|
+
id: gateId,
|
|
124
|
+
surface,
|
|
125
|
+
reason: String(reason || "").trim(),
|
|
126
|
+
evidence: collectGateEvidenceRefs(gate)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeSignalStatus(status) {
|
|
131
|
+
const normalized = String(status || "").trim().toUpperCase();
|
|
132
|
+
if (normalized === STATUS.BLOCK || normalized === STATUS.WARN || normalized === STATUS.PASS) {
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function statusSeverity(status) {
|
|
139
|
+
if (status === STATUS.BLOCK) {
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
if (status === STATUS.WARN) {
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
if (status === STATUS.PASS) {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
return -1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getSignalGate(signal, gateKey) {
|
|
152
|
+
if (!signal || !signal.details || !signal.details.gates) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const gates = signal.details.gates;
|
|
156
|
+
if (!gates || typeof gates !== "object" || !gateKey) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return gates[gateKey] && typeof gates[gateKey] === "object" ? gates[gateKey] : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveEffectiveGateStatus(gate, signal) {
|
|
163
|
+
const gateStatus = normalizeSignalStatus(gate && gate.status);
|
|
164
|
+
if (gateStatus) {
|
|
165
|
+
return gateStatus;
|
|
166
|
+
}
|
|
167
|
+
const signalStatus = normalizeSignalStatus(signal && signal.status);
|
|
168
|
+
return signalStatus || STATUS.PASS;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function statusTokenMatches(status, acceptedTokens) {
|
|
172
|
+
const normalized = String(status || "").trim().toUpperCase();
|
|
173
|
+
return acceptedTokens.includes(normalized);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collectVerificationFreshnessEvidenceRefs(verificationFreshness) {
|
|
177
|
+
const surfaces =
|
|
178
|
+
verificationFreshness && verificationFreshness.surfaces && typeof verificationFreshness.surfaces === "object"
|
|
179
|
+
? verificationFreshness.surfaces
|
|
180
|
+
: {};
|
|
181
|
+
return Object.keys(surfaces)
|
|
182
|
+
.filter((surface) => surfaces[surface] && surfaces[surface].stale && surfaces[surface].present)
|
|
183
|
+
.sort()
|
|
184
|
+
.map((surface) => `signal:${surface}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Inspect planning-signal freshness and remove stale planning signals from the effective routing summary.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} projectRoot
|
|
191
|
+
* @param {string | null} changeId
|
|
192
|
+
* @param {Object.<string, object>} signalSummary
|
|
193
|
+
* @returns {PlanningSignalFreshnessState}
|
|
194
|
+
*/
|
|
195
|
+
function collectPlanningSignalFreshnessState(projectRoot, changeId, signalSummary) {
|
|
196
|
+
const effectiveSignalSummary =
|
|
197
|
+
signalSummary && typeof signalSummary === "object"
|
|
198
|
+
? { ...signalSummary }
|
|
199
|
+
: {};
|
|
200
|
+
const stalePlanningSignals = {};
|
|
201
|
+
|
|
202
|
+
if (!changeId) {
|
|
203
|
+
return {
|
|
204
|
+
effectiveSignalSummary,
|
|
205
|
+
stalePlanningSignals,
|
|
206
|
+
needsRerunSurfaces: []
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const surface of Object.keys(PLANNING_SIGNAL_PROMOTION_FALLBACKS)) {
|
|
211
|
+
const signal = effectiveSignalSummary[surface];
|
|
212
|
+
if (!signal) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const freshness = evaluatePlanningSignalFreshness(projectRoot, {
|
|
216
|
+
changeId,
|
|
217
|
+
surface,
|
|
218
|
+
signal
|
|
219
|
+
});
|
|
220
|
+
if (!freshness.applicable || freshness.fresh) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
stalePlanningSignals[surface] = {
|
|
224
|
+
surface,
|
|
225
|
+
reasons: Array.isArray(freshness.reasons) ? freshness.reasons.slice() : [],
|
|
226
|
+
signalStatus: normalizeSignalStatus(signal.status),
|
|
227
|
+
signalTimestamp: signal && signal.timestamp ? String(signal.timestamp) : null,
|
|
228
|
+
signalTimestampMs: freshness.signalTimestampMs,
|
|
229
|
+
latestArtifactMtimeMs: freshness.latestArtifactMtimeMs,
|
|
230
|
+
latestArtifactTimestamp:
|
|
231
|
+
freshness.latestArtifactMtimeMs > 0 ? new Date(freshness.latestArtifactMtimeMs).toISOString() : null,
|
|
232
|
+
staleByMs: freshness.staleByMs
|
|
233
|
+
};
|
|
234
|
+
delete effectiveSignalSummary[surface];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
effectiveSignalSummary,
|
|
239
|
+
stalePlanningSignals,
|
|
240
|
+
needsRerunSurfaces: Object.keys(stalePlanningSignals).sort()
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function applyPlanningSignalFreshnessFindings(
|
|
245
|
+
stageId,
|
|
246
|
+
findings,
|
|
247
|
+
planningSignalFreshness,
|
|
248
|
+
decisionTraceRecords,
|
|
249
|
+
recordWorkflowDecision
|
|
250
|
+
) {
|
|
251
|
+
let nextStageId = stageId;
|
|
252
|
+
const strictPromotion = isStrictPromotionEnabled();
|
|
253
|
+
const stalePlanningSignals =
|
|
254
|
+
planningSignalFreshness && planningSignalFreshness.stalePlanningSignals
|
|
255
|
+
? planningSignalFreshness.stalePlanningSignals
|
|
256
|
+
: {};
|
|
257
|
+
|
|
258
|
+
for (const surface of Object.keys(stalePlanningSignals).sort()) {
|
|
259
|
+
const freshness = stalePlanningSignals[surface];
|
|
260
|
+
const reasonText =
|
|
261
|
+
Array.isArray(freshness.reasons) && freshness.reasons.length > 0
|
|
262
|
+
? freshness.reasons.join(", ")
|
|
263
|
+
: "unknown_staleness_reason";
|
|
264
|
+
findings.warnings.push(
|
|
265
|
+
`Stale ${surface} planning signal requires rerun before routing can rely on it (${reasonText}).`
|
|
266
|
+
);
|
|
267
|
+
if (!strictPromotion) {
|
|
268
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
269
|
+
decisionFamily: "planning_signal_freshness",
|
|
270
|
+
decisionKey: "stale_signal_rerun_required",
|
|
271
|
+
outcome: "rerun_required",
|
|
272
|
+
reasonSummary: `Stale ${surface} planning signal requires rerun before routing can rely on it.`,
|
|
273
|
+
context: {
|
|
274
|
+
planningSurface: surface,
|
|
275
|
+
strictPromotion: false,
|
|
276
|
+
signalStatus: freshness.signalStatus || null,
|
|
277
|
+
staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
|
|
278
|
+
reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
|
|
279
|
+
},
|
|
280
|
+
evidenceRefs: [`signal:${surface}`]
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
findings.blockers.push(
|
|
285
|
+
`DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; stale ${surface} planning signal blocks promotion until ${surface} is rerun.`
|
|
286
|
+
);
|
|
287
|
+
addBlockingGateRecord(
|
|
288
|
+
findings,
|
|
289
|
+
"stalePlanningSignal",
|
|
290
|
+
surface,
|
|
291
|
+
null,
|
|
292
|
+
`strict promotion requires rerun for stale ${surface} planning signal`
|
|
293
|
+
);
|
|
294
|
+
const fallbackStageId = PLANNING_SIGNAL_PROMOTION_FALLBACKS[surface] || nextStageId;
|
|
295
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
296
|
+
decisionFamily: "planning_signal_freshness",
|
|
297
|
+
decisionKey: "stale_signal_strict_fallback",
|
|
298
|
+
outcome: "downgraded",
|
|
299
|
+
reasonSummary: `Strict promotion forces routing fallback because ${surface} planning signal is stale.`,
|
|
300
|
+
context: {
|
|
301
|
+
planningSurface: surface,
|
|
302
|
+
strictPromotion: true,
|
|
303
|
+
signalStatus: freshness.signalStatus || null,
|
|
304
|
+
fallbackStage: fallbackStageId,
|
|
305
|
+
staleByMs: Number.isFinite(freshness.staleByMs) ? freshness.staleByMs : null,
|
|
306
|
+
reasons: Array.isArray(freshness.reasons) ? freshness.reasons : []
|
|
307
|
+
},
|
|
308
|
+
evidenceRefs: [`signal:${surface}`]
|
|
309
|
+
});
|
|
310
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, fallbackStageId);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return nextStageId;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function selectBlockingGateIdentity(findings) {
|
|
317
|
+
const candidates = Array.isArray(findings && findings.blockingGates) ? findings.blockingGates : [];
|
|
318
|
+
if (candidates.length === 0) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const sorted = candidates
|
|
322
|
+
.slice()
|
|
323
|
+
.sort((left, right) => {
|
|
324
|
+
const leftPriority = BLOCKING_GATE_PRIORITY.indexOf(left.id);
|
|
325
|
+
const rightPriority = BLOCKING_GATE_PRIORITY.indexOf(right.id);
|
|
326
|
+
const normalizedLeft = leftPriority === -1 ? Number.MAX_SAFE_INTEGER : leftPriority;
|
|
327
|
+
const normalizedRight = rightPriority === -1 ? Number.MAX_SAFE_INTEGER : rightPriority;
|
|
328
|
+
if (normalizedLeft !== normalizedRight) {
|
|
329
|
+
return normalizedLeft - normalizedRight;
|
|
330
|
+
}
|
|
331
|
+
return String(left.surface || "").localeCompare(String(right.surface || ""));
|
|
332
|
+
});
|
|
333
|
+
return sorted[0];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Inspect discipline markers and staleness on design/tasks artifacts.
|
|
338
|
+
*
|
|
339
|
+
* @param {string} changeDir
|
|
340
|
+
* @returns {WorkflowDisciplineState}
|
|
341
|
+
*/
|
|
342
|
+
function inspectDisciplineState(changeDir) {
|
|
343
|
+
const designPath = path.join(changeDir, "design.md");
|
|
344
|
+
const pencilDesignPath = path.join(changeDir, "pencil-design.md");
|
|
345
|
+
const pencilBindingsPath = path.join(changeDir, "pencil-bindings.md");
|
|
346
|
+
const tasksPath = path.join(changeDir, "tasks.md");
|
|
347
|
+
const designText = readTextIfExists(designPath);
|
|
348
|
+
const tasksText = readTextIfExists(tasksPath);
|
|
349
|
+
const designMarkers = parseDisciplineMarkers(designText);
|
|
350
|
+
const taskMarkers = parseDisciplineMarkers(tasksText);
|
|
351
|
+
const gateMode = resolveDisciplineGateMode();
|
|
352
|
+
const hasAnyMarker =
|
|
353
|
+
designMarkers.ordered.length > 0 ||
|
|
354
|
+
taskMarkers.ordered.length > 0 ||
|
|
355
|
+
designMarkers.malformed.length > 0 ||
|
|
356
|
+
taskMarkers.malformed.length > 0;
|
|
357
|
+
|
|
358
|
+
const designApproval =
|
|
359
|
+
designMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
|
|
360
|
+
taskMarkers.markers[DISCIPLINE_MARKER_NAMES.designApproval] ||
|
|
361
|
+
null;
|
|
362
|
+
const planSelfReview = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.planSelfReview] || null;
|
|
363
|
+
const operatorReviewAck = taskMarkers.markers[DISCIPLINE_MARKER_NAMES.operatorReviewAck] || null;
|
|
364
|
+
|
|
365
|
+
const blockers = [];
|
|
366
|
+
const warnings = [];
|
|
367
|
+
const notes = [];
|
|
368
|
+
const designApprovalArtifacts = [
|
|
369
|
+
{ label: "design.md", path: designPath },
|
|
370
|
+
{ label: "pencil-design.md", path: pencilDesignPath },
|
|
371
|
+
{ label: "pencil-bindings.md", path: pencilBindingsPath }
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
for (const malformed of [...designMarkers.malformed, ...taskMarkers.malformed]) {
|
|
375
|
+
warnings.push(`Malformed discipline marker at line ${malformed.line}: ${malformed.reason}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let designApprovalState = "missing";
|
|
379
|
+
let designApprovalStale = false;
|
|
380
|
+
if (designApproval) {
|
|
381
|
+
if (!statusTokenMatches(designApproval.status, ["APPROVED", "PASS", "ACCEPTED"])) {
|
|
382
|
+
designApprovalState = "rejected";
|
|
383
|
+
blockers.push(
|
|
384
|
+
`Design approval marker is not approved (${designApproval.status}); keep workflow in tasks/design.`
|
|
385
|
+
);
|
|
386
|
+
} else {
|
|
387
|
+
designApprovalState = "approved";
|
|
388
|
+
if (designApproval.time) {
|
|
389
|
+
const approvalMs = Date.parse(designApproval.time);
|
|
390
|
+
const staleArtifacts = [];
|
|
391
|
+
if (Number.isFinite(approvalMs)) {
|
|
392
|
+
for (const artifact of designApprovalArtifacts) {
|
|
393
|
+
let artifactMtimeMs = 0;
|
|
394
|
+
try {
|
|
395
|
+
artifactMtimeMs = fs.statSync(artifact.path).mtimeMs;
|
|
396
|
+
} catch (_error) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (artifactMtimeMs > approvalMs) {
|
|
400
|
+
staleArtifacts.push(artifact.label);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (staleArtifacts.length > 0) {
|
|
405
|
+
designApprovalState = "stale";
|
|
406
|
+
designApprovalStale = true;
|
|
407
|
+
blockers.push(
|
|
408
|
+
`Design approval marker is stale because design artifacts changed after approval timestamp: ${staleArtifacts.join(", ")}.`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
warnings.push("Design approval marker is missing timestamp; staleness checks are limited.");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} else if (hasAnyMarker || gateMode.strict) {
|
|
416
|
+
blockers.push(
|
|
417
|
+
"Missing required design approval marker (`- design approval: APPROVED @ <ISO8601>`) for disciplined handoff."
|
|
418
|
+
);
|
|
419
|
+
} else {
|
|
420
|
+
warnings.push(
|
|
421
|
+
"Design approval marker is missing; legacy compatibility mode keeps this advisory. Set DA_VINCI_DISCIPLINE_REQUIRE_APPROVAL=1 to enforce."
|
|
422
|
+
);
|
|
423
|
+
notes.push("Legacy compatibility mode: missing discipline markers are advisory.");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!planSelfReview) {
|
|
427
|
+
warnings.push("Missing plan self-review marker in tasks.md (`- plan self review: PASS @ <ISO8601>`).");
|
|
428
|
+
} else if (!statusTokenMatches(planSelfReview.status, ["PASS", "APPROVED", "DONE"])) {
|
|
429
|
+
warnings.push(`Plan self-review marker is not PASS (${planSelfReview.status}).`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!operatorReviewAck) {
|
|
433
|
+
warnings.push("Missing operator review acknowledgment marker in tasks.md (`- operator review ack: ACKNOWLEDGED @ <ISO8601>`).");
|
|
434
|
+
} else if (!statusTokenMatches(operatorReviewAck.status, ["ACKNOWLEDGED", "ACK", "CONFIRMED", "APPROVED"])) {
|
|
435
|
+
warnings.push(`Operator review acknowledgment marker is not acknowledged (${operatorReviewAck.status}).`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
strictMode: gateMode.strict,
|
|
440
|
+
hasAnyMarker,
|
|
441
|
+
designApproval: {
|
|
442
|
+
state: designApprovalState,
|
|
443
|
+
stale: designApprovalStale,
|
|
444
|
+
marker: designApproval
|
|
445
|
+
},
|
|
446
|
+
planSelfReview: {
|
|
447
|
+
marker: planSelfReview
|
|
448
|
+
},
|
|
449
|
+
operatorReviewAck: {
|
|
450
|
+
marker: operatorReviewAck
|
|
451
|
+
},
|
|
452
|
+
malformed: [...designMarkers.malformed, ...taskMarkers.malformed],
|
|
453
|
+
blockers,
|
|
454
|
+
warnings,
|
|
455
|
+
notes
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function mergeSignalBySurface(signals) {
|
|
460
|
+
const summary = {};
|
|
461
|
+
for (const signal of signals || []) {
|
|
462
|
+
const key = String(signal.surface || "").trim();
|
|
463
|
+
if (!key || summary[key]) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
summary[key] = signal;
|
|
467
|
+
}
|
|
468
|
+
return summary;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function applyTaskExecutionAndReviewFindings(findings, signals) {
|
|
472
|
+
const taskExecutionSignals = listSignalsBySurfacePrefix(signals, "task-execution.");
|
|
473
|
+
const taskReviewSignals = listSignalsBySurfacePrefix(signals, "task-review.");
|
|
474
|
+
const latestTaskExecution = mergeSignalBySurface(taskExecutionSignals);
|
|
475
|
+
const latestTaskReview = mergeSignalBySurface(taskReviewSignals);
|
|
476
|
+
|
|
477
|
+
for (const signal of Object.values(latestTaskExecution)) {
|
|
478
|
+
const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
479
|
+
const outOfScopeWrites =
|
|
480
|
+
signal.details && Array.isArray(signal.details.outOfScopeWrites)
|
|
481
|
+
? dedupeMessages(signal.details.outOfScopeWrites.map((item) => String(item || "").trim()).filter(Boolean))
|
|
482
|
+
: [];
|
|
483
|
+
const taskGroupId =
|
|
484
|
+
(envelope && envelope.taskGroupId) ||
|
|
485
|
+
String(signal.surface || "").replace(/^task-execution\./, "") ||
|
|
486
|
+
"unknown";
|
|
487
|
+
if (signal.status === STATUS.BLOCK) {
|
|
488
|
+
findings.blockers.push(`Task group ${taskGroupId} is BLOCKED in implementer status envelope.`);
|
|
489
|
+
} else if (signal.status === STATUS.WARN) {
|
|
490
|
+
findings.warnings.push(`Task group ${taskGroupId} has unresolved implementer concerns/context needs.`);
|
|
491
|
+
}
|
|
492
|
+
if (outOfScopeWrites.length > 0) {
|
|
493
|
+
findings.blockers.push(
|
|
494
|
+
`Task group ${taskGroupId} reported out-of-scope writes: ${outOfScopeWrites.join(", ")}.`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (envelope && envelope.summary) {
|
|
498
|
+
findings.notes.push(`Implementer summary ${taskGroupId}: ${envelope.summary}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const reviewStateByGroup = {};
|
|
503
|
+
for (const signal of Object.values(latestTaskReview)) {
|
|
504
|
+
const envelope = signal.details && signal.details.envelope ? signal.details.envelope : null;
|
|
505
|
+
const taskGroupId =
|
|
506
|
+
(envelope && envelope.taskGroupId) ||
|
|
507
|
+
String(signal.surface || "").replace(/^task-review\./, "").split(".")[0] ||
|
|
508
|
+
"unknown";
|
|
509
|
+
const stage = envelope && envelope.stage ? envelope.stage : String(signal.surface || "").split(".").pop();
|
|
510
|
+
if (!reviewStateByGroup[taskGroupId]) {
|
|
511
|
+
reviewStateByGroup[taskGroupId] = {};
|
|
512
|
+
}
|
|
513
|
+
reviewStateByGroup[taskGroupId][stage] = signal.status;
|
|
514
|
+
if (signal.status === STATUS.BLOCK) {
|
|
515
|
+
findings.blockers.push(`Task review ${taskGroupId}/${stage} is BLOCK.`);
|
|
516
|
+
} else if (signal.status === STATUS.WARN) {
|
|
517
|
+
findings.warnings.push(`Task review ${taskGroupId}/${stage} is WARN and requires follow-up.`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
for (const [taskGroupId, state] of Object.entries(reviewStateByGroup)) {
|
|
522
|
+
if (state.quality && !state.spec) {
|
|
523
|
+
findings.blockers.push(
|
|
524
|
+
`Task review ordering violation for ${taskGroupId}: quality review exists without a prior spec review result.`
|
|
525
|
+
);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (state.quality && state.spec === STATUS.WARN) {
|
|
529
|
+
findings.blockers.push(
|
|
530
|
+
`Task review ordering violation for ${taskGroupId}: quality review was recorded before spec review reached PASS.`
|
|
531
|
+
);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (state.quality && state.spec === STATUS.BLOCK) {
|
|
535
|
+
findings.blockers.push(
|
|
536
|
+
`Task review ordering violation for ${taskGroupId}: quality review was recorded while spec review is BLOCK.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function applyAuditFindings(stageId, findings, integrityAudit, completionAudit) {
|
|
543
|
+
let nextStageId = stageId;
|
|
544
|
+
|
|
545
|
+
if (integrityAudit && integrityAudit.status === "FAIL") {
|
|
546
|
+
findings.warnings.push("Integrity audit currently reports FAIL.");
|
|
547
|
+
} else if (integrityAudit && integrityAudit.status === "WARN") {
|
|
548
|
+
findings.warnings.push("Integrity audit currently reports WARN.");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if ((nextStageId === "verify" || nextStageId === "complete") && completionAudit) {
|
|
552
|
+
if (completionAudit.status === "PASS") {
|
|
553
|
+
if (nextStageId === "verify") {
|
|
554
|
+
nextStageId = "complete";
|
|
555
|
+
}
|
|
556
|
+
findings.notes.push("Completion audit reports PASS for the active change.");
|
|
557
|
+
} else if (completionAudit.status === "WARN") {
|
|
558
|
+
findings.warnings.push("Completion audit currently reports WARN.");
|
|
559
|
+
if (nextStageId === "complete") {
|
|
560
|
+
nextStageId = "verify";
|
|
561
|
+
}
|
|
562
|
+
} else if (completionAudit.status === "FAIL") {
|
|
563
|
+
findings.blockers.push("Completion audit currently reports FAIL.");
|
|
564
|
+
if (nextStageId === "complete") {
|
|
565
|
+
nextStageId = "verify";
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return nextStageId;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function applyExecutionSignalFindings(stageId, findings, signalSummary) {
|
|
574
|
+
let nextStageId = stageId;
|
|
575
|
+
const strictPromotion = isStrictPromotionEnabled();
|
|
576
|
+
const signalParseIssue = signalSummary["signal-file-parse"];
|
|
577
|
+
if (signalParseIssue) {
|
|
578
|
+
const warningText =
|
|
579
|
+
Array.isArray(signalParseIssue.warnings) && signalParseIssue.warnings[0]
|
|
580
|
+
? String(signalParseIssue.warnings[0])
|
|
581
|
+
: "Malformed execution signal files were detected.";
|
|
582
|
+
findings.warnings.push(warningText);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const diffSignal = signalSummary["diff-spec"];
|
|
586
|
+
if (diffSignal && diffSignal.status && diffSignal.status !== STATUS.PASS) {
|
|
587
|
+
findings.warnings.push(`Planning diff signal diff-spec reports ${diffSignal.status}.`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const lintSpecSignal = signalSummary["lint-spec"];
|
|
591
|
+
const lintSpecGateConfigs = [
|
|
592
|
+
{ id: "principleInheritance", label: "principleInheritance", fallbackStage: "breakdown" },
|
|
593
|
+
{ id: "clarify", label: "clarify", fallbackStage: "breakdown" },
|
|
594
|
+
{ id: "scenarioQuality", label: "scenarioQuality", fallbackStage: "breakdown" }
|
|
595
|
+
];
|
|
596
|
+
let lintSpecGateObserved = false;
|
|
597
|
+
let strongestLintSpecGateStatus = "";
|
|
598
|
+
for (const config of lintSpecGateConfigs) {
|
|
599
|
+
const gate = getSignalGate(lintSpecSignal, config.id);
|
|
600
|
+
if (!gate) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
lintSpecGateObserved = true;
|
|
604
|
+
const gateStatus = resolveEffectiveGateStatus(gate, lintSpecSignal);
|
|
605
|
+
if (statusSeverity(gateStatus) > statusSeverity(strongestLintSpecGateStatus)) {
|
|
606
|
+
strongestLintSpecGateStatus = gateStatus;
|
|
607
|
+
}
|
|
608
|
+
const evidenceRefs = collectGateEvidenceRefs(gate);
|
|
609
|
+
const evidenceSuffix =
|
|
610
|
+
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
611
|
+
if (gateStatus === STATUS.BLOCK) {
|
|
612
|
+
findings.blockers.push(
|
|
613
|
+
`lint-spec gate ${config.label} is BLOCK and prevents planning promotion.${evidenceSuffix}`
|
|
614
|
+
);
|
|
615
|
+
addBlockingGateRecord(
|
|
616
|
+
findings,
|
|
617
|
+
config.id,
|
|
618
|
+
"lint-spec",
|
|
619
|
+
gate,
|
|
620
|
+
`lint-spec gate ${config.label} is BLOCK`
|
|
621
|
+
);
|
|
622
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
|
|
623
|
+
} else if (gateStatus === STATUS.WARN) {
|
|
624
|
+
findings.warnings.push(`lint-spec gate ${config.label} is WARN.${evidenceSuffix}`);
|
|
625
|
+
if (strictPromotion) {
|
|
626
|
+
findings.blockers.push(
|
|
627
|
+
`DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec gate ${config.label} WARN blocks promotion.`
|
|
628
|
+
);
|
|
629
|
+
addBlockingGateRecord(
|
|
630
|
+
findings,
|
|
631
|
+
config.id,
|
|
632
|
+
"lint-spec",
|
|
633
|
+
gate,
|
|
634
|
+
`strict promotion escalated lint-spec gate ${config.label} WARN`
|
|
635
|
+
);
|
|
636
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, config.fallbackStage);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const message of Array.isArray(gate.compatibility) ? gate.compatibility : []) {
|
|
640
|
+
findings.notes.push(`lint-spec gate ${config.label} compatibility: ${message}`);
|
|
641
|
+
}
|
|
642
|
+
if (config.id === "clarify") {
|
|
643
|
+
for (const bounded of Array.isArray(gate.bounded) ? gate.bounded : []) {
|
|
644
|
+
findings.notes.push(`lint-spec gate clarify bounded ambiguity: ${bounded}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const lintSpecSignalStatus = normalizeSignalStatus(lintSpecSignal && lintSpecSignal.status);
|
|
649
|
+
if (
|
|
650
|
+
lintSpecSignal &&
|
|
651
|
+
(!lintSpecGateObserved || statusSeverity(lintSpecSignalStatus) > statusSeverity(strongestLintSpecGateStatus))
|
|
652
|
+
) {
|
|
653
|
+
if (lintSpecSignalStatus === STATUS.BLOCK) {
|
|
654
|
+
findings.blockers.push("lint-spec signal is BLOCK.");
|
|
655
|
+
addBlockingGateRecord(
|
|
656
|
+
findings,
|
|
657
|
+
"lint-spec",
|
|
658
|
+
"lint-spec",
|
|
659
|
+
lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
|
|
660
|
+
"lint-spec signal is BLOCK"
|
|
661
|
+
);
|
|
662
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
|
|
663
|
+
} else if (lintSpecSignalStatus === STATUS.WARN) {
|
|
664
|
+
findings.warnings.push("lint-spec signal is WARN.");
|
|
665
|
+
if (strictPromotion) {
|
|
666
|
+
findings.blockers.push(
|
|
667
|
+
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-spec WARN blocks promotion."
|
|
668
|
+
);
|
|
669
|
+
addBlockingGateRecord(
|
|
670
|
+
findings,
|
|
671
|
+
"lint-spec",
|
|
672
|
+
"lint-spec",
|
|
673
|
+
lintSpecSignal.details && lintSpecSignal.details.gates ? lintSpecSignal.details.gates : null,
|
|
674
|
+
"strict promotion escalated lint-spec WARN"
|
|
675
|
+
);
|
|
676
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "breakdown");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const analyzeSignal = signalSummary["scope-check"];
|
|
682
|
+
const analyzeGate = getSignalGate(analyzeSignal, "analyze");
|
|
683
|
+
let analyzeGateStatus = "";
|
|
684
|
+
if (analyzeGate) {
|
|
685
|
+
const evidenceRefs = collectGateEvidenceRefs(analyzeGate);
|
|
686
|
+
const evidenceSuffix =
|
|
687
|
+
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
688
|
+
analyzeGateStatus = resolveEffectiveGateStatus(analyzeGate, analyzeSignal);
|
|
689
|
+
if (analyzeGateStatus === STATUS.BLOCK) {
|
|
690
|
+
findings.blockers.push(`scope-check gate analyze is BLOCK.${evidenceSuffix}`);
|
|
691
|
+
addBlockingGateRecord(
|
|
692
|
+
findings,
|
|
693
|
+
"analyze",
|
|
694
|
+
"scope-check",
|
|
695
|
+
analyzeGate,
|
|
696
|
+
"scope-check gate analyze is BLOCK"
|
|
697
|
+
);
|
|
698
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
699
|
+
} else if (analyzeGateStatus === STATUS.WARN) {
|
|
700
|
+
findings.warnings.push(`scope-check gate analyze is WARN.${evidenceSuffix}`);
|
|
701
|
+
if (strictPromotion) {
|
|
702
|
+
findings.blockers.push(
|
|
703
|
+
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check gate analyze WARN blocks promotion."
|
|
704
|
+
);
|
|
705
|
+
addBlockingGateRecord(
|
|
706
|
+
findings,
|
|
707
|
+
"analyze",
|
|
708
|
+
"scope-check",
|
|
709
|
+
analyzeGate,
|
|
710
|
+
"strict promotion escalated scope-check gate analyze WARN"
|
|
711
|
+
);
|
|
712
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
for (const message of Array.isArray(analyzeGate.compatibility) ? analyzeGate.compatibility : []) {
|
|
716
|
+
findings.notes.push(`scope-check gate analyze compatibility: ${message}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const analyzeSignalStatus = normalizeSignalStatus(analyzeSignal && analyzeSignal.status);
|
|
720
|
+
if (
|
|
721
|
+
analyzeSignal &&
|
|
722
|
+
(!analyzeGate || statusSeverity(analyzeSignalStatus) > statusSeverity(analyzeGateStatus))
|
|
723
|
+
) {
|
|
724
|
+
if (analyzeSignalStatus === STATUS.BLOCK) {
|
|
725
|
+
findings.blockers.push("scope-check signal is BLOCK.");
|
|
726
|
+
addBlockingGateRecord(
|
|
727
|
+
findings,
|
|
728
|
+
"scope-check",
|
|
729
|
+
"scope-check",
|
|
730
|
+
analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
|
|
731
|
+
"scope-check signal is BLOCK"
|
|
732
|
+
);
|
|
733
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
734
|
+
} else if (analyzeSignalStatus === STATUS.WARN) {
|
|
735
|
+
findings.warnings.push("scope-check signal is WARN.");
|
|
736
|
+
if (strictPromotion) {
|
|
737
|
+
findings.blockers.push(
|
|
738
|
+
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; scope-check WARN blocks promotion."
|
|
739
|
+
);
|
|
740
|
+
addBlockingGateRecord(
|
|
741
|
+
findings,
|
|
742
|
+
"scope-check",
|
|
743
|
+
"scope-check",
|
|
744
|
+
analyzeSignal.details && analyzeSignal.details.gates ? analyzeSignal.details.gates : null,
|
|
745
|
+
"strict promotion escalated scope-check WARN"
|
|
746
|
+
);
|
|
747
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const lintTasksSignal = signalSummary["lint-tasks"];
|
|
753
|
+
const taskCheckpointGate = getSignalGate(lintTasksSignal, "taskCheckpoint");
|
|
754
|
+
const taskCheckpointGateStatus = taskCheckpointGate
|
|
755
|
+
? resolveEffectiveGateStatus(taskCheckpointGate, lintTasksSignal)
|
|
756
|
+
: "";
|
|
757
|
+
const lintTasksSignalStatus = normalizeSignalStatus(lintTasksSignal && lintTasksSignal.status);
|
|
758
|
+
if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.BLOCK) {
|
|
759
|
+
const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
|
|
760
|
+
const evidenceSuffix =
|
|
761
|
+
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
762
|
+
findings.blockers.push(`lint-tasks task-checkpoint is BLOCK and prevents promotion into build.${evidenceSuffix}`);
|
|
763
|
+
addBlockingGateRecord(
|
|
764
|
+
findings,
|
|
765
|
+
"taskCheckpoint",
|
|
766
|
+
"lint-tasks",
|
|
767
|
+
taskCheckpointGate,
|
|
768
|
+
"lint-tasks task-checkpoint is BLOCK"
|
|
769
|
+
);
|
|
770
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
771
|
+
} else if (taskCheckpointGate && taskCheckpointGateStatus === STATUS.WARN) {
|
|
772
|
+
const evidenceRefs = collectGateEvidenceRefs(taskCheckpointGate);
|
|
773
|
+
const evidenceSuffix =
|
|
774
|
+
evidenceRefs.length > 0 ? ` Evidence: ${evidenceRefs.join(", ")}` : "";
|
|
775
|
+
findings.warnings.push(`lint-tasks task-checkpoint is WARN.${evidenceSuffix}`);
|
|
776
|
+
if (strictPromotion) {
|
|
777
|
+
findings.blockers.push(
|
|
778
|
+
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
|
|
779
|
+
);
|
|
780
|
+
addBlockingGateRecord(
|
|
781
|
+
findings,
|
|
782
|
+
"taskCheckpoint",
|
|
783
|
+
"lint-tasks",
|
|
784
|
+
taskCheckpointGate,
|
|
785
|
+
"strict promotion escalated lint-tasks task-checkpoint WARN"
|
|
786
|
+
);
|
|
787
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (
|
|
791
|
+
lintTasksSignal &&
|
|
792
|
+
(!taskCheckpointGate || statusSeverity(lintTasksSignalStatus) > statusSeverity(taskCheckpointGateStatus))
|
|
793
|
+
) {
|
|
794
|
+
if (lintTasksSignalStatus === STATUS.BLOCK) {
|
|
795
|
+
findings.blockers.push("lint-tasks signal is BLOCK.");
|
|
796
|
+
addBlockingGateRecord(
|
|
797
|
+
findings,
|
|
798
|
+
"lint-tasks",
|
|
799
|
+
"lint-tasks",
|
|
800
|
+
lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
|
|
801
|
+
"lint-tasks signal is BLOCK"
|
|
802
|
+
);
|
|
803
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
804
|
+
} else if (lintTasksSignalStatus === STATUS.WARN) {
|
|
805
|
+
findings.warnings.push("lint-tasks signal is WARN.");
|
|
806
|
+
if (strictPromotion) {
|
|
807
|
+
findings.blockers.push(
|
|
808
|
+
"DA_VINCI_DISCIPLINE_STRICT_PROMOTION is enabled; lint-tasks WARN blocks promotion into build."
|
|
809
|
+
);
|
|
810
|
+
addBlockingGateRecord(
|
|
811
|
+
findings,
|
|
812
|
+
"lint-tasks",
|
|
813
|
+
"lint-tasks",
|
|
814
|
+
lintTasksSignal.details && lintTasksSignal.details.gates ? lintTasksSignal.details.gates : null,
|
|
815
|
+
"strict promotion escalated lint-tasks WARN"
|
|
816
|
+
);
|
|
817
|
+
nextStageId = fallbackStageIfBeyond(nextStageId, "tasks");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (taskCheckpointGate) {
|
|
822
|
+
for (const message of Array.isArray(taskCheckpointGate.compatibility)
|
|
823
|
+
? taskCheckpointGate.compatibility
|
|
824
|
+
: []) {
|
|
825
|
+
findings.notes.push(`lint-tasks gate taskCheckpoint compatibility: ${message}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const verificationSignal = signalSummary["verify-coverage"];
|
|
830
|
+
if (verificationSignal && verificationSignal.status === STATUS.BLOCK) {
|
|
831
|
+
findings.blockers.push("verify-coverage signal is BLOCK.");
|
|
832
|
+
if (nextStageId === "complete") {
|
|
833
|
+
nextStageId = "verify";
|
|
834
|
+
}
|
|
835
|
+
} else if (verificationSignal && verificationSignal.status === STATUS.WARN) {
|
|
836
|
+
findings.warnings.push("verify-coverage signal is WARN.");
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return nextStageId;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function buildGatesWithLiveOverlays(baseGates, completionAudit, disciplineState, verificationFreshness) {
|
|
843
|
+
const gates = baseGates && typeof baseGates === "object" ? { ...baseGates } : {};
|
|
844
|
+
if (completionAudit) {
|
|
845
|
+
gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] =
|
|
846
|
+
completionAudit.status === STATUS.PASS ? STATUS.PASS : STATUS.WARN;
|
|
847
|
+
}
|
|
848
|
+
if (disciplineState && disciplineState.blockers.length > 0) {
|
|
849
|
+
gates[HANDOFF_GATES.TASKS_TO_BUILD] = STATUS.BLOCK;
|
|
850
|
+
}
|
|
851
|
+
if (verificationFreshness && !verificationFreshness.fresh) {
|
|
852
|
+
gates[HANDOFF_GATES.VERIFY_TO_COMPLETE] = STATUS.BLOCK;
|
|
853
|
+
}
|
|
854
|
+
return gates;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Apply runtime/live overlays on top of a base workflow view.
|
|
859
|
+
*
|
|
860
|
+
* @param {{
|
|
861
|
+
* projectRoot: string,
|
|
862
|
+
* changeId?: string | null,
|
|
863
|
+
* stageId: string,
|
|
864
|
+
* findings: WorkflowFindings,
|
|
865
|
+
* baseGates: Object.<string, string>,
|
|
866
|
+
* taskGroups?: Array<object>,
|
|
867
|
+
* changeSignals?: Array<object>,
|
|
868
|
+
* signalSummary?: Object.<string, object>,
|
|
869
|
+
* planningSignalFreshness?: PlanningSignalFreshnessState,
|
|
870
|
+
* integrityAudit?: object | null,
|
|
871
|
+
* completionAudit?: object | null,
|
|
872
|
+
* disciplineState?: object | null,
|
|
873
|
+
* verificationFreshness?: object | null,
|
|
874
|
+
* hasTasksArtifact?: boolean,
|
|
875
|
+
* decisionTraceRecords?: Array<object> | null,
|
|
876
|
+
* recordWorkflowDecision?: Function
|
|
877
|
+
* }} [options]
|
|
878
|
+
* @returns {WorkflowOverlayResult}
|
|
879
|
+
*/
|
|
880
|
+
function applyWorkflowOverlays(options = {}) {
|
|
881
|
+
const findings = {
|
|
882
|
+
blockers: Array.isArray(options.findings && options.findings.blockers)
|
|
883
|
+
? options.findings.blockers.slice()
|
|
884
|
+
: [],
|
|
885
|
+
warnings: Array.isArray(options.findings && options.findings.warnings)
|
|
886
|
+
? options.findings.warnings.slice()
|
|
887
|
+
: [],
|
|
888
|
+
notes: Array.isArray(options.findings && options.findings.notes)
|
|
889
|
+
? options.findings.notes.slice()
|
|
890
|
+
: []
|
|
891
|
+
};
|
|
892
|
+
let stageId = options.stageId;
|
|
893
|
+
const integrityAudit = options.integrityAudit || null;
|
|
894
|
+
const completionAudit = options.completionAudit || null;
|
|
895
|
+
const disciplineState = options.disciplineState || null;
|
|
896
|
+
const verificationFreshness = options.verificationFreshness || null;
|
|
897
|
+
const planningSignalFreshness =
|
|
898
|
+
options.planningSignalFreshness && typeof options.planningSignalFreshness === "object"
|
|
899
|
+
? options.planningSignalFreshness
|
|
900
|
+
: {
|
|
901
|
+
effectiveSignalSummary: options.signalSummary || {},
|
|
902
|
+
stalePlanningSignals: {},
|
|
903
|
+
needsRerunSurfaces: []
|
|
904
|
+
};
|
|
905
|
+
const taskGroups = Array.isArray(options.taskGroups) ? options.taskGroups : [];
|
|
906
|
+
const decisionTraceRecords = Array.isArray(options.decisionTraceRecords)
|
|
907
|
+
? options.decisionTraceRecords
|
|
908
|
+
: null;
|
|
909
|
+
const recordWorkflowDecision = options.recordWorkflowDecision;
|
|
910
|
+
|
|
911
|
+
stageId = applyAuditFindings(stageId, findings, integrityAudit, completionAudit);
|
|
912
|
+
stageId = applyPlanningSignalFreshnessFindings(
|
|
913
|
+
stageId,
|
|
914
|
+
findings,
|
|
915
|
+
planningSignalFreshness,
|
|
916
|
+
decisionTraceRecords,
|
|
917
|
+
recordWorkflowDecision
|
|
918
|
+
);
|
|
919
|
+
stageId = applyExecutionSignalFindings(
|
|
920
|
+
stageId,
|
|
921
|
+
findings,
|
|
922
|
+
planningSignalFreshness.effectiveSignalSummary || {}
|
|
923
|
+
);
|
|
924
|
+
applyTaskExecutionAndReviewFindings(findings, options.changeSignals || []);
|
|
925
|
+
|
|
926
|
+
if (disciplineState) {
|
|
927
|
+
findings.blockers.push(...disciplineState.blockers);
|
|
928
|
+
findings.warnings.push(...disciplineState.warnings);
|
|
929
|
+
findings.notes.push(...disciplineState.notes);
|
|
930
|
+
if (disciplineState.blockers.length > 0 && ["build", "verify", "complete"].includes(stageId)) {
|
|
931
|
+
stageId = options.hasTasksArtifact ? "tasks" : "design";
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (verificationFreshness && !verificationFreshness.fresh && (stageId === "verify" || stageId === "complete")) {
|
|
936
|
+
const stageBeforeFreshness = stageId;
|
|
937
|
+
findings.blockers.push(
|
|
938
|
+
"Completion-facing routing requires fresh verification evidence; stale evidence keeps the route in verify."
|
|
939
|
+
);
|
|
940
|
+
stageId = "verify";
|
|
941
|
+
if (stageBeforeFreshness === "complete") {
|
|
942
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
943
|
+
decisionFamily: "verification_freshness_downgrade",
|
|
944
|
+
decisionKey: "verification_freshness_stale",
|
|
945
|
+
outcome: "downgraded",
|
|
946
|
+
reasonSummary: "Completion-facing routing stays in verify because verification evidence is stale.",
|
|
947
|
+
context: {
|
|
948
|
+
fromStage: "complete",
|
|
949
|
+
toStage: "verify",
|
|
950
|
+
baselineIso: verificationFreshness.baselineIso || null,
|
|
951
|
+
staleReasonCount: Array.isArray(verificationFreshness.staleReasons)
|
|
952
|
+
? verificationFreshness.staleReasons.length
|
|
953
|
+
: 0,
|
|
954
|
+
requiredSurfaces: Array.isArray(verificationFreshness.requiredSurfaces)
|
|
955
|
+
? verificationFreshness.requiredSurfaces
|
|
956
|
+
: []
|
|
957
|
+
},
|
|
958
|
+
evidenceRefs: collectVerificationFreshnessEvidenceRefs(verificationFreshness)
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const gates = buildGatesWithLiveOverlays(
|
|
964
|
+
options.baseGates,
|
|
965
|
+
completionAudit,
|
|
966
|
+
disciplineState,
|
|
967
|
+
verificationFreshness
|
|
968
|
+
);
|
|
969
|
+
const executionProfile = deriveExecutionProfile({
|
|
970
|
+
stage: stageId,
|
|
971
|
+
taskGroups
|
|
972
|
+
});
|
|
973
|
+
let worktreePreflight = null;
|
|
974
|
+
if (options.changeId && (stageId === "build" || stageId === "verify")) {
|
|
975
|
+
worktreePreflight = runWorktreePreflight(options.projectRoot, {
|
|
976
|
+
parallelPreferred: executionProfile.mode === "bounded_parallel"
|
|
977
|
+
});
|
|
978
|
+
if (
|
|
979
|
+
executionProfile.mode === "bounded_parallel" &&
|
|
980
|
+
worktreePreflight.summary &&
|
|
981
|
+
worktreePreflight.summary.recommendedIsolation
|
|
982
|
+
) {
|
|
983
|
+
executionProfile.effectiveMode = "serial";
|
|
984
|
+
executionProfile.rationale = dedupeMessages([
|
|
985
|
+
...(executionProfile.rationale || []),
|
|
986
|
+
"worktree preflight recommends isolation; effective mode downgraded to serial"
|
|
987
|
+
]);
|
|
988
|
+
findings.warnings.push(
|
|
989
|
+
"Bounded-parallel profile downgraded to serial until worktree isolation is ready or explicitly accepted."
|
|
990
|
+
);
|
|
991
|
+
recordDecision(recordWorkflowDecision, decisionTraceRecords, {
|
|
992
|
+
decisionFamily: "worktree_isolation_downgrade",
|
|
993
|
+
decisionKey: "effective_serial_after_preflight",
|
|
994
|
+
outcome: "downgraded",
|
|
995
|
+
reasonSummary: "Worktree preflight downgraded advisory bounded parallel execution to effective serial mode.",
|
|
996
|
+
context: {
|
|
997
|
+
advisoryMode: executionProfile.mode,
|
|
998
|
+
effectiveMode: executionProfile.effectiveMode || "serial",
|
|
999
|
+
preflightStatus: worktreePreflight.status || null,
|
|
1000
|
+
recommendedIsolation: Boolean(
|
|
1001
|
+
worktreePreflight.summary && worktreePreflight.summary.recommendedIsolation
|
|
1002
|
+
),
|
|
1003
|
+
dirtyEntries:
|
|
1004
|
+
worktreePreflight.summary &&
|
|
1005
|
+
Number.isFinite(Number(worktreePreflight.summary.dirtyEntries))
|
|
1006
|
+
? Number(worktreePreflight.summary.dirtyEntries)
|
|
1007
|
+
: 0
|
|
1008
|
+
},
|
|
1009
|
+
evidenceRefs: ["surface:worktree-preflight"]
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
dedupeFindings(findings);
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
stageId,
|
|
1018
|
+
findings,
|
|
1019
|
+
gates,
|
|
1020
|
+
taskGroups,
|
|
1021
|
+
executionProfile,
|
|
1022
|
+
worktreePreflight,
|
|
1023
|
+
blockingGate: selectBlockingGateIdentity(findings),
|
|
1024
|
+
needsRerunSurfaces: planningSignalFreshness.needsRerunSurfaces,
|
|
1025
|
+
stalePlanningSignals: planningSignalFreshness.stalePlanningSignals
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
module.exports = {
|
|
1030
|
+
collectPlanningSignalFreshnessState,
|
|
1031
|
+
inspectDisciplineState,
|
|
1032
|
+
applyWorkflowOverlays
|
|
1033
|
+
};
|