@xenonbyte/da-vinci-workflow 0.2.5 → 0.2.6
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 +16 -0
- package/README.md +15 -9
- package/README.zh-CN.md +16 -9
- package/docs/dv-command-reference.md +18 -2
- package/docs/execution-chain-migration.md +14 -3
- package/docs/maintainer-bootstrap.md +102 -0
- package/docs/pencil-rendering-workflow.md +1 -1
- package/docs/skill-usage.md +31 -0
- package/docs/workflow-overview.md +40 -5
- package/docs/zh-CN/dv-command-reference.md +16 -2
- package/docs/zh-CN/maintainer-bootstrap.md +101 -0
- package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
- package/docs/zh-CN/skill-usage.md +30 -0
- package/docs/zh-CN/workflow-overview.md +38 -5
- package/lib/audit.js +19 -0
- package/lib/cli/helpers.js +63 -2
- package/lib/cli.js +98 -0
- package/lib/gate-utils.js +56 -0
- package/lib/install.js +134 -6
- package/lib/lint-bindings.js +41 -28
- package/lib/lint-spec.js +403 -109
- package/lib/lint-tasks.js +571 -21
- package/lib/maintainer-readiness.js +317 -0
- package/lib/planning-parsers.js +190 -1
- package/lib/planning-quality-utils.js +81 -0
- package/lib/planning-signal-freshness.js +205 -0
- package/lib/scope-check.js +751 -82
- package/lib/sidecars.js +396 -1
- package/lib/task-review.js +2 -1
- package/lib/utils.js +15 -0
- package/lib/workflow-persisted-state.js +52 -32
- package/lib/workflow-state.js +1187 -249
- package/package.json +1 -1
package/lib/lint-tasks.js
CHANGED
|
@@ -3,22 +3,34 @@ const { STATUS } = require("./workflow-contract");
|
|
|
3
3
|
const {
|
|
4
4
|
normalizeText,
|
|
5
5
|
unique,
|
|
6
|
-
resolveChangeDir,
|
|
7
6
|
parseTasksArtifact,
|
|
7
|
+
parseBindingsArtifact,
|
|
8
8
|
parseRuntimeSpecs,
|
|
9
9
|
readChangeArtifacts,
|
|
10
10
|
readArtifactTexts
|
|
11
11
|
} = require("./planning-parsers");
|
|
12
|
+
const {
|
|
13
|
+
loadPlanningAnchorIndex,
|
|
14
|
+
resolvePlanningAnchorRefs
|
|
15
|
+
} = require("./sidecars");
|
|
16
|
+
const { buildGateEnvelope, finalizeGateEnvelope } = require("./gate-utils");
|
|
17
|
+
const { pathExists, readTextIfExists } = require("./utils");
|
|
18
|
+
const { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
|
|
19
|
+
const { lintRuntimeSpecs } = require("./lint-spec");
|
|
20
|
+
const { runScopeCheck } = require("./scope-check");
|
|
21
|
+
const { evaluatePlanningSignalFreshness } = require("./planning-signal-freshness");
|
|
22
|
+
const {
|
|
23
|
+
buildBasePlanningResultEnvelope,
|
|
24
|
+
finalizePlanningResult,
|
|
25
|
+
resolveChangeWithFindings
|
|
26
|
+
} = require("./planning-quality-utils");
|
|
12
27
|
|
|
13
28
|
function buildEnvelope(projectRoot, strict) {
|
|
14
29
|
return {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
projectRoot,
|
|
20
|
-
changeId: null,
|
|
21
|
-
strict,
|
|
30
|
+
...buildBasePlanningResultEnvelope(projectRoot, strict),
|
|
31
|
+
gates: {
|
|
32
|
+
taskCheckpoint: null
|
|
33
|
+
},
|
|
22
34
|
summary: {
|
|
23
35
|
groups: 0,
|
|
24
36
|
checklistItems: 0
|
|
@@ -30,13 +42,46 @@ function finalize(result) {
|
|
|
30
42
|
result.failures = unique(result.failures);
|
|
31
43
|
result.warnings = unique(result.warnings);
|
|
32
44
|
result.notes = unique(result.notes);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
return finalizePlanningResult(result);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeKnownStatus(status) {
|
|
49
|
+
const normalized = String(status || "").trim().toUpperCase();
|
|
50
|
+
if (normalized === STATUS.BLOCK || normalized === STATUS.WARN || normalized === STATUS.PASS) {
|
|
51
|
+
return normalized;
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function statusPriority(status) {
|
|
57
|
+
const normalized = normalizeKnownStatus(status);
|
|
58
|
+
if (normalized === STATUS.BLOCK) {
|
|
59
|
+
return 2;
|
|
37
60
|
}
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
if (normalized === STATUS.WARN) {
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
if (normalized === STATUS.PASS) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
return -1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveWorseStatus(left, right) {
|
|
71
|
+
const leftNormalized = normalizeKnownStatus(left);
|
|
72
|
+
const rightNormalized = normalizeKnownStatus(right);
|
|
73
|
+
if (!leftNormalized && !rightNormalized) {
|
|
74
|
+
return STATUS.PASS;
|
|
75
|
+
}
|
|
76
|
+
if (!leftNormalized) {
|
|
77
|
+
return rightNormalized;
|
|
78
|
+
}
|
|
79
|
+
if (!rightNormalized) {
|
|
80
|
+
return leftNormalized;
|
|
81
|
+
}
|
|
82
|
+
return statusPriority(leftNormalized) >= statusPriority(rightNormalized)
|
|
83
|
+
? leftNormalized
|
|
84
|
+
: rightNormalized;
|
|
40
85
|
}
|
|
41
86
|
|
|
42
87
|
function findMissingTaskGroupSequence(taskGroups) {
|
|
@@ -56,22 +101,395 @@ function findMissingTaskGroupSequence(taskGroups) {
|
|
|
56
101
|
return missing;
|
|
57
102
|
}
|
|
58
103
|
|
|
104
|
+
function attachTaskCheckpointFindings(result, gate) {
|
|
105
|
+
for (const message of gate.blocking || []) {
|
|
106
|
+
result.failures.push(`[gate:taskCheckpoint] ${message}`);
|
|
107
|
+
}
|
|
108
|
+
for (const message of gate.advisory || []) {
|
|
109
|
+
result.warnings.push(`[gate:taskCheckpoint] ${message}`);
|
|
110
|
+
}
|
|
111
|
+
for (const message of gate.compatibility || []) {
|
|
112
|
+
result.notes.push(`[gate:taskCheckpoint] ${message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function clampGateStatusBySignal(gateStatus, signalStatus) {
|
|
117
|
+
const gateNormalized = normalizeKnownStatus(gateStatus);
|
|
118
|
+
const signalNormalized = normalizeKnownStatus(signalStatus);
|
|
119
|
+
if (![STATUS.PASS, STATUS.WARN, STATUS.BLOCK].includes(gateNormalized)) {
|
|
120
|
+
return [STATUS.PASS, STATUS.WARN, STATUS.BLOCK].includes(signalNormalized)
|
|
121
|
+
? signalNormalized
|
|
122
|
+
: STATUS.PASS;
|
|
123
|
+
}
|
|
124
|
+
if (![STATUS.PASS, STATUS.WARN, STATUS.BLOCK].includes(signalNormalized)) {
|
|
125
|
+
return gateNormalized;
|
|
126
|
+
}
|
|
127
|
+
return statusPriority(gateNormalized) >= statusPriority(signalNormalized) ? gateNormalized : signalNormalized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function applyUpstreamPlanningGateContext(taskCheckpointGate, signalSummary, strict, derivedGateContext = {}) {
|
|
131
|
+
const lintSpecSignal = signalSummary["lint-spec"];
|
|
132
|
+
const scopeCheckSignal = signalSummary["scope-check"];
|
|
133
|
+
|
|
134
|
+
const clarifyGate =
|
|
135
|
+
lintSpecSignal && lintSpecSignal.details && lintSpecSignal.details.gates
|
|
136
|
+
? lintSpecSignal.details.gates.clarify
|
|
137
|
+
: null;
|
|
138
|
+
const analyzeGate =
|
|
139
|
+
scopeCheckSignal && scopeCheckSignal.details && scopeCheckSignal.details.gates
|
|
140
|
+
? scopeCheckSignal.details.gates.analyze
|
|
141
|
+
: null;
|
|
142
|
+
|
|
143
|
+
const check = [
|
|
144
|
+
{
|
|
145
|
+
id: "clarify",
|
|
146
|
+
signal: lintSpecSignal,
|
|
147
|
+
gate: clarifyGate,
|
|
148
|
+
derived: derivedGateContext.clarify || null,
|
|
149
|
+
message:
|
|
150
|
+
"upstream clarify gate is unresolved; task-checkpoint cannot be treated as healthy until clarify is cleared."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "analyze",
|
|
154
|
+
signal: scopeCheckSignal,
|
|
155
|
+
gate: analyzeGate,
|
|
156
|
+
derived: derivedGateContext.analyze || null,
|
|
157
|
+
message:
|
|
158
|
+
"upstream analyze gate is unresolved; task-checkpoint cannot be treated as healthy until coherence drift is cleared."
|
|
159
|
+
}
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
for (const entry of check) {
|
|
163
|
+
const signalStatus = entry.signal && entry.signal.status ? entry.signal.status : STATUS.PASS;
|
|
164
|
+
const rawGateStatus = entry.gate && entry.gate.status ? entry.gate.status : signalStatus;
|
|
165
|
+
const signalHasBlockingFindings =
|
|
166
|
+
normalizeKnownStatus(signalStatus) === STATUS.BLOCK ||
|
|
167
|
+
(entry.gate && Array.isArray(entry.gate.blocking) && entry.gate.blocking.length > 0);
|
|
168
|
+
const derivedHasBlockingFindings = entry.derived && entry.derived.hasBlocking === true;
|
|
169
|
+
const signalEffectiveStatus = clampGateStatusBySignal(rawGateStatus, signalStatus);
|
|
170
|
+
const derivedStatus = normalizeKnownStatus(entry.derived && entry.derived.status);
|
|
171
|
+
const effectiveStatus =
|
|
172
|
+
signalHasBlockingFindings || derivedHasBlockingFindings
|
|
173
|
+
? STATUS.BLOCK
|
|
174
|
+
: resolveWorseStatus(signalEffectiveStatus, derivedStatus);
|
|
175
|
+
const evidence = unique([
|
|
176
|
+
...(entry.gate && Array.isArray(entry.gate.evidence) ? entry.gate.evidence : []),
|
|
177
|
+
...(entry.derived && Array.isArray(entry.derived.evidence) ? entry.derived.evidence : [])
|
|
178
|
+
]);
|
|
179
|
+
if (entry.id === "clarify") {
|
|
180
|
+
const boundedContext = unique([
|
|
181
|
+
...(entry.gate && Array.isArray(entry.gate.bounded) ? entry.gate.bounded : []),
|
|
182
|
+
...(entry.derived && Array.isArray(entry.derived.bounded) ? entry.derived.bounded : [])
|
|
183
|
+
]);
|
|
184
|
+
for (const message of boundedContext) {
|
|
185
|
+
taskCheckpointGate.compatibility.push(
|
|
186
|
+
`upstream clarify gate has bounded ambiguity context: ${message}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (effectiveStatus === STATUS.BLOCK) {
|
|
191
|
+
if (strict) {
|
|
192
|
+
taskCheckpointGate.blocking.push(entry.message);
|
|
193
|
+
} else {
|
|
194
|
+
taskCheckpointGate.advisory.push(entry.message);
|
|
195
|
+
}
|
|
196
|
+
taskCheckpointGate.evidence.push(...evidence.slice(0, 3));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collectDerivedUpstreamGateContext(projectRoot, changeId, signalSummary = {}) {
|
|
202
|
+
const context = {
|
|
203
|
+
clarify: null,
|
|
204
|
+
analyze: null,
|
|
205
|
+
notes: [],
|
|
206
|
+
ignoredSurfaces: []
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const lintSpecSignal = signalSummary["lint-spec"] || null;
|
|
210
|
+
const lintSpecFreshness = evaluatePlanningSignalFreshness(projectRoot, {
|
|
211
|
+
changeId,
|
|
212
|
+
surface: "lint-spec",
|
|
213
|
+
signal: lintSpecSignal
|
|
214
|
+
});
|
|
215
|
+
const shouldDeriveClarify = !lintSpecSignal || !lintSpecFreshness.fresh;
|
|
216
|
+
if (lintSpecSignal && !lintSpecFreshness.fresh) {
|
|
217
|
+
context.ignoredSurfaces.push("lint-spec");
|
|
218
|
+
context.notes.push(
|
|
219
|
+
`lint-tasks ignored stale lint-spec signal and derived clarify gate from current artifacts (${lintSpecFreshness.reasons.join(", ")}).`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (shouldDeriveClarify) {
|
|
223
|
+
try {
|
|
224
|
+
const lintResult = lintRuntimeSpecs(projectRoot, {
|
|
225
|
+
changeId,
|
|
226
|
+
strict: false
|
|
227
|
+
});
|
|
228
|
+
const clarifyGate =
|
|
229
|
+
lintResult && lintResult.gates && lintResult.gates.clarify
|
|
230
|
+
? lintResult.gates.clarify
|
|
231
|
+
: null;
|
|
232
|
+
if (clarifyGate) {
|
|
233
|
+
context.clarify = {
|
|
234
|
+
status: clarifyGate.status,
|
|
235
|
+
evidence: Array.isArray(clarifyGate.evidence) ? clarifyGate.evidence : [],
|
|
236
|
+
hasBlocking: Array.isArray(clarifyGate.blocking) && clarifyGate.blocking.length > 0,
|
|
237
|
+
bounded: Array.isArray(clarifyGate.bounded) ? clarifyGate.bounded : []
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
context.notes.push(
|
|
242
|
+
`lint-tasks could not derive clarify gate from current artifacts (${error && error.message ? error.message : error}).`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const scopeCheckSignal = signalSummary["scope-check"] || null;
|
|
248
|
+
const scopeCheckFreshness = evaluatePlanningSignalFreshness(projectRoot, {
|
|
249
|
+
changeId,
|
|
250
|
+
surface: "scope-check",
|
|
251
|
+
signal: scopeCheckSignal
|
|
252
|
+
});
|
|
253
|
+
const shouldDeriveAnalyze = !scopeCheckSignal || !scopeCheckFreshness.fresh;
|
|
254
|
+
if (scopeCheckSignal && !scopeCheckFreshness.fresh) {
|
|
255
|
+
context.ignoredSurfaces.push("scope-check");
|
|
256
|
+
context.notes.push(
|
|
257
|
+
`lint-tasks ignored stale scope-check signal and derived analyze gate from current artifacts (${scopeCheckFreshness.reasons.join(", ")}).`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (shouldDeriveAnalyze) {
|
|
261
|
+
try {
|
|
262
|
+
const scopeResult = runScopeCheck(projectRoot, {
|
|
263
|
+
changeId,
|
|
264
|
+
strict: false
|
|
265
|
+
});
|
|
266
|
+
const analyzeGate =
|
|
267
|
+
scopeResult && scopeResult.gates && scopeResult.gates.analyze
|
|
268
|
+
? scopeResult.gates.analyze
|
|
269
|
+
: null;
|
|
270
|
+
if (analyzeGate) {
|
|
271
|
+
context.analyze = {
|
|
272
|
+
status: analyzeGate.status,
|
|
273
|
+
evidence: Array.isArray(analyzeGate.evidence) ? analyzeGate.evidence : [],
|
|
274
|
+
hasBlocking: Array.isArray(analyzeGate.blocking) && analyzeGate.blocking.length > 0
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
context.notes.push(
|
|
279
|
+
`lint-tasks could not derive analyze gate from current artifacts (${error && error.message ? error.message : error}).`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return context;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isImplementationRelevantGroup(group) {
|
|
288
|
+
const hasTargets = Array.isArray(group.targetFiles) && group.targetFiles.length > 0;
|
|
289
|
+
const hasFileRefs = Array.isArray(group.fileReferences) && group.fileReferences.length > 0;
|
|
290
|
+
const hasChecklist = Array.isArray(group.checklistItems) && group.checklistItems.length > 0;
|
|
291
|
+
return (
|
|
292
|
+
group.codeChangeLikely === true ||
|
|
293
|
+
group.testingIntent === true ||
|
|
294
|
+
group.reviewIntent === true ||
|
|
295
|
+
hasTargets ||
|
|
296
|
+
hasFileRefs ||
|
|
297
|
+
hasChecklist
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function groupLikelyNeedsMappingAnchor(group) {
|
|
302
|
+
const combined = normalizeText(
|
|
303
|
+
[
|
|
304
|
+
group.title,
|
|
305
|
+
...(Array.isArray(group.checklistItems)
|
|
306
|
+
? group.checklistItems.map((item) => item.text || "")
|
|
307
|
+
: [])
|
|
308
|
+
].join(" ")
|
|
309
|
+
);
|
|
310
|
+
return /binding|map|mapping|design source|screen id|pencil/i.test(combined);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function resolveArtifactAnchor(changeDir, projectRoot, reference) {
|
|
314
|
+
const ref = reference && typeof reference === "object" ? reference : {};
|
|
315
|
+
const artifactPath = String(ref.artifactPath || "").trim();
|
|
316
|
+
const artifactToken = String(ref.artifactToken || "").trim();
|
|
317
|
+
if (!artifactPath) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
reason: "empty_artifact_path"
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const candidates = [];
|
|
325
|
+
if (path.isAbsolute(artifactPath)) {
|
|
326
|
+
candidates.push(artifactPath);
|
|
327
|
+
} else {
|
|
328
|
+
candidates.push(path.join(changeDir, artifactPath));
|
|
329
|
+
candidates.push(path.join(projectRoot, artifactPath));
|
|
330
|
+
}
|
|
331
|
+
const absolutePath = candidates.find((candidate) => pathExists(candidate));
|
|
332
|
+
if (!absolutePath) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
reason: "artifact_path_missing"
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (!artifactToken) {
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
path: absolutePath
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const content = readTextIfExists(absolutePath);
|
|
345
|
+
if (!content) {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
reason: "artifact_unreadable"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (!String(content).toLowerCase().includes(artifactToken.toLowerCase())) {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
reason: "artifact_token_unmatched"
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
ok: true,
|
|
359
|
+
path: absolutePath
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildArtifactFallbackAnchorIndex(specRecords, bindingsText) {
|
|
364
|
+
const index = {
|
|
365
|
+
behavior: new Set(),
|
|
366
|
+
acceptance: new Set(),
|
|
367
|
+
state: new Set(),
|
|
368
|
+
mapping: new Set()
|
|
369
|
+
};
|
|
370
|
+
const FAMILY_ID_PATTERN = /\b(behavior|acceptance|state|mapping)-(\d{1,6})\b/gi;
|
|
371
|
+
|
|
372
|
+
const registerRef = (family, digits) => {
|
|
373
|
+
const normalizedFamily = String(family || "").toLowerCase();
|
|
374
|
+
const parsed = Number.parseInt(String(digits || "").trim(), 10);
|
|
375
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !index[normalizedFamily]) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
index[normalizedFamily].add(`${normalizedFamily}-${String(parsed).padStart(3, "0")}`);
|
|
379
|
+
index[normalizedFamily].add(`${normalizedFamily}-${String(parsed)}`);
|
|
380
|
+
};
|
|
381
|
+
const registerOrdinalRefs = (family, items) => {
|
|
382
|
+
const normalizedFamily = String(family || "").toLowerCase();
|
|
383
|
+
if (!index[normalizedFamily]) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const list = Array.isArray(items) ? items : [];
|
|
387
|
+
for (let cursor = 0; cursor < list.length; cursor += 1) {
|
|
388
|
+
registerRef(normalizedFamily, String(cursor + 1));
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
for (const record of Array.isArray(specRecords) ? specRecords : []) {
|
|
393
|
+
const text = String(record && record.text ? record.text : "");
|
|
394
|
+
const sections = record && record.parsed && record.parsed.sections ? record.parsed.sections : {};
|
|
395
|
+
registerOrdinalRefs("behavior", sections.behavior && sections.behavior.items);
|
|
396
|
+
registerOrdinalRefs("acceptance", sections.acceptance && sections.acceptance.items);
|
|
397
|
+
registerOrdinalRefs("state", sections.states && sections.states.items);
|
|
398
|
+
FAMILY_ID_PATTERN.lastIndex = 0;
|
|
399
|
+
let match;
|
|
400
|
+
while ((match = FAMILY_ID_PATTERN.exec(text)) !== null) {
|
|
401
|
+
registerRef(match[1], match[2]);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const parsedBindings = parseBindingsArtifact(bindingsText || "");
|
|
406
|
+
const mappings = Array.isArray(parsedBindings.mappings) ? parsedBindings.mappings : [];
|
|
407
|
+
for (let cursor = 0; cursor < mappings.length; cursor += 1) {
|
|
408
|
+
const mapping = mappings[cursor];
|
|
409
|
+
registerRef("mapping", String(cursor + 1));
|
|
410
|
+
const id = String(mapping && mapping.id ? mapping.id : "").trim();
|
|
411
|
+
const raw = String(mapping && mapping.raw ? mapping.raw : "");
|
|
412
|
+
const inlineCandidates = [];
|
|
413
|
+
if (id) {
|
|
414
|
+
inlineCandidates.push(id);
|
|
415
|
+
}
|
|
416
|
+
if (raw) {
|
|
417
|
+
inlineCandidates.push(raw);
|
|
418
|
+
}
|
|
419
|
+
for (const candidate of inlineCandidates) {
|
|
420
|
+
FAMILY_ID_PATTERN.lastIndex = 0;
|
|
421
|
+
let match;
|
|
422
|
+
while ((match = FAMILY_ID_PATTERN.exec(candidate)) !== null) {
|
|
423
|
+
registerRef(match[1], match[2]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return index;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function sectionContainsToken(items, normalizedToken) {
|
|
432
|
+
if (!normalizedToken) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
return (Array.isArray(items) ? items : []).some((item) =>
|
|
436
|
+
normalizeText(String(item || "")).includes(normalizedToken)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function inferFamiliesFromArtifactAnchor(anchor, specRecords) {
|
|
441
|
+
const families = new Set();
|
|
442
|
+
const source = String(anchor && anchor.source ? anchor.source : "").toLowerCase();
|
|
443
|
+
if (source !== "artifact") {
|
|
444
|
+
return families;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const artifactPath = String(anchor && anchor.artifactPath ? anchor.artifactPath : "").toLowerCase();
|
|
448
|
+
const normalizedToken = normalizeText(String(anchor && anchor.artifactToken ? anchor.artifactToken : ""));
|
|
449
|
+
|
|
450
|
+
if (/pencil-bindings\.md$/i.test(artifactPath)) {
|
|
451
|
+
families.add("mapping");
|
|
452
|
+
}
|
|
453
|
+
if (!/spec\.md$/i.test(artifactPath) && !/\/specs\//i.test(artifactPath)) {
|
|
454
|
+
return families;
|
|
455
|
+
}
|
|
456
|
+
if (!normalizedToken) {
|
|
457
|
+
return families;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (const record of Array.isArray(specRecords) ? specRecords : []) {
|
|
461
|
+
const sections = record && record.parsed && record.parsed.sections ? record.parsed.sections : {};
|
|
462
|
+
if (sectionContainsToken(sections.behavior && sections.behavior.items, normalizedToken)) {
|
|
463
|
+
families.add("behavior");
|
|
464
|
+
}
|
|
465
|
+
if (sectionContainsToken(sections.states && sections.states.items, normalizedToken)) {
|
|
466
|
+
families.add("state");
|
|
467
|
+
}
|
|
468
|
+
if (sectionContainsToken(sections.acceptance && sections.acceptance.items, normalizedToken)) {
|
|
469
|
+
families.add("acceptance");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return families;
|
|
474
|
+
}
|
|
475
|
+
|
|
59
476
|
function lintTasks(projectPathInput, options = {}) {
|
|
60
477
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
61
478
|
const strict = options.strict === true;
|
|
62
479
|
const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
|
|
63
480
|
const result = buildEnvelope(projectRoot, strict);
|
|
64
481
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
result.notes.push(...resolved.notes);
|
|
68
|
-
if (!resolved.changeDir) {
|
|
482
|
+
const changeDir = resolveChangeWithFindings(projectRoot, requestedChangeId, result.failures, result.notes);
|
|
483
|
+
if (!changeDir) {
|
|
69
484
|
result.notes.push("lint-tasks defaults to advisory mode; pass `--strict` to block on findings.");
|
|
70
485
|
return finalize(result);
|
|
71
486
|
}
|
|
72
|
-
result.changeId =
|
|
487
|
+
result.changeId = path.basename(changeDir);
|
|
488
|
+
const anchorIndex = loadPlanningAnchorIndex(changeDir);
|
|
489
|
+
result.notes.push(...anchorIndex.notes);
|
|
490
|
+
result.notes.push(...anchorIndex.warnings.map((item) => `[sidecars] ${item}`));
|
|
73
491
|
|
|
74
|
-
const artifactPaths = readChangeArtifacts(projectRoot,
|
|
492
|
+
const artifactPaths = readChangeArtifacts(projectRoot, result.changeId);
|
|
75
493
|
const artifacts = readArtifactTexts(artifactPaths);
|
|
76
494
|
if (!artifacts.tasks) {
|
|
77
495
|
result.failures.push("Missing `tasks.md` for lint-tasks.");
|
|
@@ -80,8 +498,16 @@ function lintTasks(projectPathInput, options = {}) {
|
|
|
80
498
|
}
|
|
81
499
|
|
|
82
500
|
const parsedTasks = parseTasksArtifact(artifacts.tasks);
|
|
83
|
-
const specRecords = parseRuntimeSpecs(
|
|
501
|
+
const specRecords = parseRuntimeSpecs(changeDir, projectRoot);
|
|
502
|
+
const fallbackAnchorIndex = buildArtifactFallbackAnchorIndex(specRecords, artifacts.bindings || "");
|
|
84
503
|
const taskText = normalizeText(artifacts.tasks);
|
|
504
|
+
const taskCheckpointGate = buildGateEnvelope("taskCheckpoint");
|
|
505
|
+
const hasAnyAnchorEvidence = parsedTasks.taskGroups.some(
|
|
506
|
+
(group) =>
|
|
507
|
+
(Array.isArray(group.planningAnchors) && group.planningAnchors.length > 0) ||
|
|
508
|
+
(Array.isArray(group.malformedAnchors) && group.malformedAnchors.length > 0)
|
|
509
|
+
);
|
|
510
|
+
const legacyNoAnchorMode = !hasAnyAnchorEvidence;
|
|
85
511
|
|
|
86
512
|
result.summary.groups = parsedTasks.taskGroups.length;
|
|
87
513
|
result.summary.checklistItems = parsedTasks.checklistItems.length;
|
|
@@ -194,6 +620,109 @@ function lintTasks(projectPathInput, options = {}) {
|
|
|
194
620
|
`Task group ${groupLabel} hints review-required execution but does not declare concrete review intent.`
|
|
195
621
|
);
|
|
196
622
|
}
|
|
623
|
+
|
|
624
|
+
const implementationRelevant = isImplementationRelevantGroup(group);
|
|
625
|
+
const anchorRefs = Array.isArray(group.planningAnchors) ? group.planningAnchors : [];
|
|
626
|
+
const malformedAnchors = Array.isArray(group.malformedAnchors) ? group.malformedAnchors : [];
|
|
627
|
+
for (const malformed of malformedAnchors) {
|
|
628
|
+
taskCheckpointGate.blocking.push(
|
|
629
|
+
`${groupLabel} contains malformed planning anchor (${malformed.reason || "invalid format"}).`
|
|
630
|
+
);
|
|
631
|
+
taskCheckpointGate.evidence.push(`tasks.md:${group.startLine || "?"}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (implementationRelevant && anchorRefs.length === 0) {
|
|
635
|
+
if (legacyNoAnchorMode) {
|
|
636
|
+
taskCheckpointGate.compatibility.push(
|
|
637
|
+
`${groupLabel} has no explicit planning anchors (legacy compatibility mode).`
|
|
638
|
+
);
|
|
639
|
+
} else {
|
|
640
|
+
taskCheckpointGate.blocking.push(
|
|
641
|
+
`${groupLabel} declares implementation-relevant work but has no planning anchors.`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
taskCheckpointGate.evidence.push(`tasks.md:${group.startLine || "?"}`);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (anchorRefs.length > 0) {
|
|
649
|
+
const resolvedAnchors = resolvePlanningAnchorRefs(anchorRefs, {
|
|
650
|
+
sidecarIndex: anchorIndex,
|
|
651
|
+
resolveAnchorIdRef: (reference) => {
|
|
652
|
+
const ref = String(reference && reference.ref ? reference.ref : "").trim();
|
|
653
|
+
const family = String(reference && reference.family ? reference.family : "").trim().toLowerCase();
|
|
654
|
+
if (!ref || !family || !fallbackAnchorIndex[family]) {
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
reason: "anchor_id_unresolved",
|
|
658
|
+
classification: "invalid_or_stale_sidecar_anchor"
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const normalizedRef = ref.toLowerCase();
|
|
662
|
+
return {
|
|
663
|
+
ok: fallbackAnchorIndex[family].has(normalizedRef),
|
|
664
|
+
reason: fallbackAnchorIndex[family].has(normalizedRef) ? "" : "anchor_id_unresolved",
|
|
665
|
+
classification: fallbackAnchorIndex[family].has(normalizedRef)
|
|
666
|
+
? ""
|
|
667
|
+
: "invalid_or_stale_sidecar_anchor"
|
|
668
|
+
};
|
|
669
|
+
},
|
|
670
|
+
resolveArtifactRef: (reference) =>
|
|
671
|
+
resolveArtifactAnchor(changeDir, projectRoot, reference)
|
|
672
|
+
});
|
|
673
|
+
for (const unresolved of resolvedAnchors.unresolved) {
|
|
674
|
+
const reason = String(unresolved.reason || "unresolved").trim();
|
|
675
|
+
if (reason === "sidecar_unavailable") {
|
|
676
|
+
taskCheckpointGate.compatibility.push(
|
|
677
|
+
`${groupLabel} anchor \`${unresolved.ref}\` cannot be validated because sidecars are unavailable/stale.`
|
|
678
|
+
);
|
|
679
|
+
} else {
|
|
680
|
+
taskCheckpointGate.blocking.push(
|
|
681
|
+
`${groupLabel} anchor \`${unresolved.ref}\` is unresolved (${reason}).`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
taskCheckpointGate.evidence.push(`tasks.md:${group.startLine || "?"}`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const families = new Set(
|
|
688
|
+
resolvedAnchors.resolved
|
|
689
|
+
.filter((item) => item.source === "sidecar" || item.source === "artifact-parse")
|
|
690
|
+
.map((item) => String(item.family || "").toLowerCase())
|
|
691
|
+
);
|
|
692
|
+
for (const anchor of resolvedAnchors.resolved) {
|
|
693
|
+
for (const family of inferFamiliesFromArtifactAnchor(anchor, specRecords)) {
|
|
694
|
+
families.add(family);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const needsBehaviorCoverage = group.codeChangeLikely === true;
|
|
698
|
+
const needsAcceptanceCoverage = group.testingIntent === true || group.reviewIntent === true;
|
|
699
|
+
const needsMappingCoverage = groupLikelyNeedsMappingAnchor(group);
|
|
700
|
+
if (needsBehaviorCoverage && !families.has("behavior") && !families.has("state")) {
|
|
701
|
+
taskCheckpointGate.advisory.push(
|
|
702
|
+
`${groupLabel} changes behavior but anchor set is missing behavior/state coverage.`
|
|
703
|
+
);
|
|
704
|
+
taskCheckpointGate.evidence.push(`tasks.md:${group.startLine || "?"}`);
|
|
705
|
+
}
|
|
706
|
+
if (needsAcceptanceCoverage && !families.has("acceptance")) {
|
|
707
|
+
taskCheckpointGate.advisory.push(
|
|
708
|
+
`${groupLabel} has review/testing intent but anchor set is missing acceptance coverage.`
|
|
709
|
+
);
|
|
710
|
+
taskCheckpointGate.evidence.push(`tasks.md:${group.startLine || "?"}`);
|
|
711
|
+
}
|
|
712
|
+
if (needsMappingCoverage && !families.has("mapping")) {
|
|
713
|
+
taskCheckpointGate.advisory.push(
|
|
714
|
+
`${groupLabel} mentions mapping/design-binding intent but anchor set is missing mapping coverage.`
|
|
715
|
+
);
|
|
716
|
+
taskCheckpointGate.evidence.push(`tasks.md:${group.startLine || "?"}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (implementationRelevant && Array.isArray(group.placeholderItems) && group.placeholderItems.length > 0) {
|
|
721
|
+
taskCheckpointGate.advisory.push(
|
|
722
|
+
`${groupLabel} is placeholder-heavy (${group.placeholderItems.length} placeholder item(s)); checkpoint health remains degraded.`
|
|
723
|
+
);
|
|
724
|
+
taskCheckpointGate.evidence.push(`tasks.md:${group.startLine || "?"}`);
|
|
725
|
+
}
|
|
197
726
|
}
|
|
198
727
|
|
|
199
728
|
for (const specRecord of specRecords) {
|
|
@@ -215,6 +744,27 @@ function lintTasks(projectPathInput, options = {}) {
|
|
|
215
744
|
}
|
|
216
745
|
}
|
|
217
746
|
|
|
747
|
+
const signalSummary = summarizeSignalsBySurface(
|
|
748
|
+
readExecutionSignals(projectRoot, {
|
|
749
|
+
changeId: result.changeId
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
const derivedUpstreamGateContext = collectDerivedUpstreamGateContext(projectRoot, result.changeId, signalSummary);
|
|
753
|
+
const effectiveSignalSummary = { ...signalSummary };
|
|
754
|
+
for (const surface of derivedUpstreamGateContext.ignoredSurfaces || []) {
|
|
755
|
+
delete effectiveSignalSummary[surface];
|
|
756
|
+
}
|
|
757
|
+
result.notes.push(...derivedUpstreamGateContext.notes);
|
|
758
|
+
applyUpstreamPlanningGateContext(
|
|
759
|
+
taskCheckpointGate,
|
|
760
|
+
effectiveSignalSummary,
|
|
761
|
+
strict,
|
|
762
|
+
derivedUpstreamGateContext
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
result.gates.taskCheckpoint = finalizeGateEnvelope(taskCheckpointGate, { strict });
|
|
766
|
+
attachTaskCheckpointFindings(result, result.gates.taskCheckpoint);
|
|
767
|
+
|
|
218
768
|
result.notes.push("lint-tasks defaults to advisory mode; pass `--strict` to block on findings.");
|
|
219
769
|
result.notes.push(
|
|
220
770
|
"Strict-promotion guidance: require clean placeholder/file-target/execution-intent/verification-command findings before promoting to build."
|