@xenonbyte/da-vinci-workflow 0.2.5 → 0.2.7

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/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
- status: STATUS.PASS,
16
- failures: [],
17
- warnings: [],
18
- notes: [],
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
- const hasFindings = result.failures.length > 0 || result.warnings.length > 0;
34
- if (!hasFindings) {
35
- result.status = STATUS.PASS;
36
- return result;
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
- result.status = result.strict ? STATUS.BLOCK : STATUS.WARN;
39
- return result;
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 resolved = resolveChangeDir(projectRoot, requestedChangeId);
66
- result.failures.push(...resolved.failures);
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 = resolved.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, resolved.changeId);
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(resolved.changeDir, projectRoot);
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."