@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/sidecars.js
CHANGED
|
@@ -17,6 +17,87 @@ const {
|
|
|
17
17
|
|
|
18
18
|
const SCHEMA_VERSION = "1.0.0";
|
|
19
19
|
|
|
20
|
+
function readJsonIfExists(targetPath) {
|
|
21
|
+
if (!targetPath || !pathExists(targetPath)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
|
26
|
+
} catch (_error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeEvidenceText(value) {
|
|
32
|
+
return String(value || "")
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[`*_~]/g, "")
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveProjectRootFromChangeDir(changeDir) {
|
|
40
|
+
return path.resolve(changeDir || process.cwd(), "..", "..", "..");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveSourceRefAbsolutePath(projectRoot, sourceRefPath) {
|
|
44
|
+
const sourcePath = String(sourceRefPath || "").trim();
|
|
45
|
+
if (!sourcePath) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (path.isAbsolute(sourcePath)) {
|
|
49
|
+
return sourcePath;
|
|
50
|
+
}
|
|
51
|
+
return path.join(projectRoot, sourcePath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function evaluateSourceRefsFreshness(changeDir, payload) {
|
|
55
|
+
const sourceRefs = Array.isArray(payload && payload.sourceRefs) ? payload.sourceRefs : [];
|
|
56
|
+
if (sourceRefs.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
fresh: false,
|
|
59
|
+
checked: 0,
|
|
60
|
+
stale: ["missing_source_refs"]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const projectRoot = resolveProjectRootFromChangeDir(changeDir);
|
|
64
|
+
const stale = [];
|
|
65
|
+
for (const ref of sourceRefs) {
|
|
66
|
+
const absolutePath = resolveSourceRefAbsolutePath(projectRoot, ref && ref.path);
|
|
67
|
+
if (!absolutePath || !pathExists(absolutePath)) {
|
|
68
|
+
stale.push(`missing:${String((ref && ref.path) || "").trim() || "(unknown)"}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const expected = String((ref && ref.digest) || "").trim();
|
|
72
|
+
const actual = digestFile(absolutePath);
|
|
73
|
+
if (expected && actual && expected !== actual) {
|
|
74
|
+
stale.push(`digest_mismatch:${String(ref.path || absolutePath)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
fresh: stale.length === 0,
|
|
80
|
+
checked: sourceRefs.length,
|
|
81
|
+
stale
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildCollectionTextIdMap(items, idField = "id", textField = "text") {
|
|
86
|
+
const map = new Map();
|
|
87
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
88
|
+
const id = String(item && item[idField] ? item[idField] : "").trim();
|
|
89
|
+
const text = normalizeEvidenceText(item && item[textField]);
|
|
90
|
+
if (!id || !text) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!map.has(text)) {
|
|
94
|
+
map.set(text, []);
|
|
95
|
+
}
|
|
96
|
+
map.get(text).push(id);
|
|
97
|
+
}
|
|
98
|
+
return map;
|
|
99
|
+
}
|
|
100
|
+
|
|
20
101
|
function buildEnvelope(projectRoot) {
|
|
21
102
|
return {
|
|
22
103
|
status: STATUS.PASS,
|
|
@@ -241,6 +322,316 @@ function writeSidecarFile(sidecarsDir, fileName, payload) {
|
|
|
241
322
|
return absolutePath;
|
|
242
323
|
}
|
|
243
324
|
|
|
325
|
+
function loadPlanningAnchorIndex(changeDir) {
|
|
326
|
+
const sidecarsDir = path.join(changeDir, "sidecars");
|
|
327
|
+
const spec = readJsonIfExists(path.join(sidecarsDir, "spec.index.json"));
|
|
328
|
+
const bindings = readJsonIfExists(path.join(sidecarsDir, "bindings.index.json"));
|
|
329
|
+
const notes = [];
|
|
330
|
+
const warnings = [];
|
|
331
|
+
|
|
332
|
+
if (!pathExists(sidecarsDir)) {
|
|
333
|
+
notes.push("Planning sidecars are missing; anchor resolution will use artifact fallback.");
|
|
334
|
+
return {
|
|
335
|
+
sidecarsDir,
|
|
336
|
+
available: false,
|
|
337
|
+
index: {
|
|
338
|
+
behavior: new Set(),
|
|
339
|
+
acceptance: new Set(),
|
|
340
|
+
state: new Set(),
|
|
341
|
+
mapping: new Set()
|
|
342
|
+
},
|
|
343
|
+
notes,
|
|
344
|
+
warnings
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!spec) {
|
|
349
|
+
warnings.push("Missing or unreadable `spec.index.json` in sidecars.");
|
|
350
|
+
}
|
|
351
|
+
if (!bindings) {
|
|
352
|
+
warnings.push("Missing or unreadable `bindings.index.json` in sidecars.");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const specFreshness = spec ? evaluateSourceRefsFreshness(changeDir, spec) : null;
|
|
356
|
+
const bindingsFreshness = bindings ? evaluateSourceRefsFreshness(changeDir, bindings) : null;
|
|
357
|
+
if (specFreshness && !specFreshness.fresh) {
|
|
358
|
+
warnings.push(
|
|
359
|
+
`spec.index.json appears stale (${specFreshness.stale.join(", ")}); anchor resolution will degrade.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (bindingsFreshness && !bindingsFreshness.fresh) {
|
|
363
|
+
warnings.push(
|
|
364
|
+
`bindings.index.json appears stale (${bindingsFreshness.stale.join(", ")}); anchor resolution will degrade.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const behavior = new Set(
|
|
369
|
+
((spec && spec.collections && spec.collections.behavior) || []).map((item) => item.id)
|
|
370
|
+
);
|
|
371
|
+
const acceptance = new Set(
|
|
372
|
+
((spec && spec.collections && spec.collections.acceptance) || []).map((item) => item.id)
|
|
373
|
+
);
|
|
374
|
+
const state = new Set(
|
|
375
|
+
((spec && spec.collections && spec.collections.states) || []).map((item) => item.id)
|
|
376
|
+
);
|
|
377
|
+
const mapping = new Set(((bindings && bindings.mappings) || []).map((item) => item.id));
|
|
378
|
+
|
|
379
|
+
const sourceFresh =
|
|
380
|
+
(!specFreshness || specFreshness.fresh) && (!bindingsFreshness || bindingsFreshness.fresh);
|
|
381
|
+
const available =
|
|
382
|
+
sourceFresh && (behavior.size > 0 || acceptance.size > 0 || state.size > 0 || mapping.size > 0);
|
|
383
|
+
if (!available) {
|
|
384
|
+
if (!sourceFresh) {
|
|
385
|
+
notes.push("Planning sidecars exist but are stale; anchor resolution falls back to artifact evidence.");
|
|
386
|
+
} else {
|
|
387
|
+
notes.push("Planning sidecars are present but empty for anchor id resolution.");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
sidecarsDir,
|
|
393
|
+
available,
|
|
394
|
+
fresh: sourceFresh,
|
|
395
|
+
freshness: {
|
|
396
|
+
spec: specFreshness,
|
|
397
|
+
bindings: bindingsFreshness
|
|
398
|
+
},
|
|
399
|
+
index: {
|
|
400
|
+
behavior,
|
|
401
|
+
acceptance,
|
|
402
|
+
state,
|
|
403
|
+
mapping
|
|
404
|
+
},
|
|
405
|
+
raw: {
|
|
406
|
+
spec,
|
|
407
|
+
bindings
|
|
408
|
+
},
|
|
409
|
+
notes,
|
|
410
|
+
warnings
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resolvePlanningAnchorRefs(anchorRefs, options = {}) {
|
|
415
|
+
const refs = Array.isArray(anchorRefs) ? anchorRefs : [];
|
|
416
|
+
const sidecarIndex = options.sidecarIndex || {
|
|
417
|
+
available: false,
|
|
418
|
+
index: {
|
|
419
|
+
behavior: new Set(),
|
|
420
|
+
acceptance: new Set(),
|
|
421
|
+
state: new Set(),
|
|
422
|
+
mapping: new Set()
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const artifactResolver = typeof options.resolveArtifactRef === "function" ? options.resolveArtifactRef : null;
|
|
426
|
+
const anchorIdResolver = typeof options.resolveAnchorIdRef === "function" ? options.resolveAnchorIdRef : null;
|
|
427
|
+
|
|
428
|
+
const resolved = [];
|
|
429
|
+
const unresolved = [];
|
|
430
|
+
const notes = [];
|
|
431
|
+
|
|
432
|
+
for (const ref of refs) {
|
|
433
|
+
const normalized = String(ref || "").trim();
|
|
434
|
+
if (!normalized) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const familyMatch = normalized.match(/^(behavior|acceptance|state|mapping)-/i);
|
|
439
|
+
if (familyMatch) {
|
|
440
|
+
const family = String(familyMatch[1] || "").toLowerCase();
|
|
441
|
+
if (sidecarIndex.available && sidecarIndex.index[family] && sidecarIndex.index[family].has(normalized)) {
|
|
442
|
+
resolved.push({
|
|
443
|
+
ref: normalized,
|
|
444
|
+
source: "sidecar",
|
|
445
|
+
family
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
let fallback = null;
|
|
449
|
+
if (anchorIdResolver) {
|
|
450
|
+
fallback = anchorIdResolver({
|
|
451
|
+
ref: normalized,
|
|
452
|
+
family,
|
|
453
|
+
sidecarAvailable: sidecarIndex.available,
|
|
454
|
+
sidecarFresh: sidecarIndex.fresh !== false
|
|
455
|
+
});
|
|
456
|
+
if (fallback && fallback.ok) {
|
|
457
|
+
resolved.push({
|
|
458
|
+
ref: normalized,
|
|
459
|
+
source: fallback.source || "artifact-parse",
|
|
460
|
+
family
|
|
461
|
+
});
|
|
462
|
+
if (fallback.note) {
|
|
463
|
+
notes.push(String(fallback.note));
|
|
464
|
+
}
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const fallbackReason =
|
|
469
|
+
fallback && fallback.reason ? String(fallback.reason).trim() : "";
|
|
470
|
+
const fallbackClassification =
|
|
471
|
+
fallback && fallback.classification ? String(fallback.classification).trim() : "";
|
|
472
|
+
unresolved.push({
|
|
473
|
+
ref: normalized,
|
|
474
|
+
reason:
|
|
475
|
+
fallbackReason ||
|
|
476
|
+
(sidecarIndex.available ? "unknown_sidecar_id" : "sidecar_unavailable"),
|
|
477
|
+
family,
|
|
478
|
+
classification: fallbackClassification ||
|
|
479
|
+
(sidecarIndex.available || sidecarIndex.fresh === false || fallbackReason
|
|
480
|
+
? "invalid_or_stale_sidecar_anchor"
|
|
481
|
+
: "sidecar_not_available")
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const artifactMatch = normalized.match(/^artifact:([^#\s]+)(?:#(.+))?$/i);
|
|
488
|
+
if (artifactMatch && artifactResolver) {
|
|
489
|
+
const artifactPath = String(artifactMatch[1] || "").trim();
|
|
490
|
+
const artifactToken = String(artifactMatch[2] || "").trim();
|
|
491
|
+
const artifactResult = artifactResolver({
|
|
492
|
+
artifactPath,
|
|
493
|
+
artifactToken
|
|
494
|
+
});
|
|
495
|
+
if (artifactResult && artifactResult.ok) {
|
|
496
|
+
resolved.push({
|
|
497
|
+
ref: normalized,
|
|
498
|
+
source: "artifact",
|
|
499
|
+
artifactPath,
|
|
500
|
+
artifactToken
|
|
501
|
+
});
|
|
502
|
+
} else {
|
|
503
|
+
unresolved.push({
|
|
504
|
+
ref: normalized,
|
|
505
|
+
reason:
|
|
506
|
+
artifactResult && artifactResult.reason ? artifactResult.reason : "artifact_ref_unresolved"
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
unresolved.push({
|
|
513
|
+
ref: normalized,
|
|
514
|
+
reason: "unsupported_anchor_format",
|
|
515
|
+
classification: "malformed_anchor"
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!sidecarIndex.available) {
|
|
520
|
+
notes.push("Anchor id resolution degraded: sidecars unavailable; artifact refs are preferred.");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
resolved,
|
|
525
|
+
unresolved,
|
|
526
|
+
notes
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function loadAnalyzeEvidenceIndex(changeDir) {
|
|
531
|
+
const sidecarIndex = loadPlanningAnchorIndex(changeDir);
|
|
532
|
+
const sidecarsDir = sidecarIndex.sidecarsDir;
|
|
533
|
+
const spec = sidecarIndex.raw && sidecarIndex.raw.spec ? sidecarIndex.raw.spec : null;
|
|
534
|
+
const bindings = sidecarIndex.raw && sidecarIndex.raw.bindings ? sidecarIndex.raw.bindings : null;
|
|
535
|
+
|
|
536
|
+
const behaviorMap = buildCollectionTextIdMap(spec && spec.collections && spec.collections.behavior);
|
|
537
|
+
const acceptanceMap = buildCollectionTextIdMap(spec && spec.collections && spec.collections.acceptance);
|
|
538
|
+
const stateMap = buildCollectionTextIdMap(spec && spec.collections && spec.collections.states);
|
|
539
|
+
const mappingByImplementation = new Map();
|
|
540
|
+
for (const mapping of (bindings && bindings.mappings) || []) {
|
|
541
|
+
const implementation = String(mapping && mapping.implementation ? mapping.implementation : "").trim();
|
|
542
|
+
if (!implementation) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const normalized = implementation.replace(/\\/g, "/");
|
|
546
|
+
if (!mappingByImplementation.has(normalized)) {
|
|
547
|
+
mappingByImplementation.set(normalized, []);
|
|
548
|
+
}
|
|
549
|
+
mappingByImplementation.get(normalized).push(String(mapping.id || "").trim());
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
sidecarsDir,
|
|
554
|
+
available: sidecarIndex.available,
|
|
555
|
+
fresh: sidecarIndex.fresh === true,
|
|
556
|
+
notes: sidecarIndex.notes.slice(),
|
|
557
|
+
warnings: sidecarIndex.warnings.slice(),
|
|
558
|
+
maps: {
|
|
559
|
+
behavior: behaviorMap,
|
|
560
|
+
acceptance: acceptanceMap,
|
|
561
|
+
state: stateMap,
|
|
562
|
+
mappingByImplementation
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function resolveAnalyzeEvidenceRefs(evidenceItems, options = {}) {
|
|
568
|
+
const items = Array.isArray(evidenceItems) ? evidenceItems : [];
|
|
569
|
+
const index = options.index || {
|
|
570
|
+
available: false,
|
|
571
|
+
maps: {
|
|
572
|
+
behavior: new Map(),
|
|
573
|
+
acceptance: new Map(),
|
|
574
|
+
state: new Map(),
|
|
575
|
+
mappingByImplementation: new Map()
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const refs = [];
|
|
579
|
+
const notes = [];
|
|
580
|
+
|
|
581
|
+
for (const item of items) {
|
|
582
|
+
const artifactPath = String(item && item.artifactPath ? item.artifactPath : "").trim();
|
|
583
|
+
const itemText = String(item && item.itemText ? item.itemText : "").trim();
|
|
584
|
+
const kind = String(item && item.kind ? item.kind : "").trim().toLowerCase();
|
|
585
|
+
const implementationPath = String(item && item.implementationPath ? item.implementationPath : "").trim();
|
|
586
|
+
const normalizedText = normalizeEvidenceText(itemText);
|
|
587
|
+
|
|
588
|
+
if (index.available && normalizedText) {
|
|
589
|
+
const candidateIds = [];
|
|
590
|
+
if (kind === "behavior" && index.maps.behavior.has(normalizedText)) {
|
|
591
|
+
candidateIds.push(...index.maps.behavior.get(normalizedText));
|
|
592
|
+
}
|
|
593
|
+
if (kind === "acceptance" && index.maps.acceptance.has(normalizedText)) {
|
|
594
|
+
candidateIds.push(...index.maps.acceptance.get(normalizedText));
|
|
595
|
+
}
|
|
596
|
+
if (kind === "state" && index.maps.state.has(normalizedText)) {
|
|
597
|
+
candidateIds.push(...index.maps.state.get(normalizedText));
|
|
598
|
+
}
|
|
599
|
+
for (const id of candidateIds) {
|
|
600
|
+
refs.push(`sidecar:${id}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (index.available && implementationPath) {
|
|
605
|
+
const normalizedImplementation = implementationPath.replace(/\\/g, "/");
|
|
606
|
+
const mappingIds = index.maps.mappingByImplementation.get(normalizedImplementation) || [];
|
|
607
|
+
for (const id of mappingIds) {
|
|
608
|
+
refs.push(`sidecar:${id}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (artifactPath) {
|
|
613
|
+
const fallbackToken = itemText
|
|
614
|
+
? itemText
|
|
615
|
+
.split(/\s+/)
|
|
616
|
+
.slice(0, 8)
|
|
617
|
+
.join(" ")
|
|
618
|
+
: "";
|
|
619
|
+
refs.push(
|
|
620
|
+
fallbackToken ? `artifact:${artifactPath}#${fallbackToken}` : `artifact:${artifactPath}`
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!index.available) {
|
|
626
|
+
notes.push("Analyze evidence mapping degraded to artifact refs because sidecars are unavailable or stale.");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
refs: unique(refs),
|
|
631
|
+
notes
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
244
635
|
function generatePlanningSidecars(projectPathInput, options = {}) {
|
|
245
636
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
246
637
|
const write = options.write !== false;
|
|
@@ -365,5 +756,9 @@ function formatGenerateSidecarsReport(result) {
|
|
|
365
756
|
module.exports = {
|
|
366
757
|
SCHEMA_VERSION,
|
|
367
758
|
generatePlanningSidecars,
|
|
368
|
-
formatGenerateSidecarsReport
|
|
759
|
+
formatGenerateSidecarsReport,
|
|
760
|
+
loadPlanningAnchorIndex,
|
|
761
|
+
resolvePlanningAnchorRefs,
|
|
762
|
+
loadAnalyzeEvidenceIndex,
|
|
763
|
+
resolveAnalyzeEvidenceRefs
|
|
369
764
|
};
|
package/lib/task-review.js
CHANGED
|
@@ -123,7 +123,8 @@ function writeTaskReviewEnvelope(projectPathInput, options = {}) {
|
|
|
123
123
|
|
|
124
124
|
if (envelope.stage === "quality") {
|
|
125
125
|
const specSignal = findLatestTaskReviewSignal(existingSignals, envelope.taskGroupId, "spec");
|
|
126
|
-
|
|
126
|
+
const specStatus = specSignal ? String(specSignal.status || "").toUpperCase() : "";
|
|
127
|
+
if (!specSignal || specStatus !== "PASS") {
|
|
127
128
|
throw new Error(
|
|
128
129
|
`Quality review for task group ${envelope.taskGroupId} requires a prior spec review PASS.`
|
|
129
130
|
);
|
package/lib/utils.js
CHANGED
|
@@ -49,6 +49,20 @@ function uniqueValues(values) {
|
|
|
49
49
|
return Array.from(new Set((values || []).filter(Boolean)));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function dedupeMessages(items, options = {}) {
|
|
53
|
+
const normalize =
|
|
54
|
+
typeof options.normalize === "function"
|
|
55
|
+
? options.normalize
|
|
56
|
+
: (value) => value;
|
|
57
|
+
return Array.from(
|
|
58
|
+
new Set(
|
|
59
|
+
(Array.isArray(items) ? items : [])
|
|
60
|
+
.map((item) => normalize(item))
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
function parseJsonText(raw, context = "JSON payload") {
|
|
53
67
|
try {
|
|
54
68
|
return JSON.parse(String(raw));
|
|
@@ -140,6 +154,7 @@ module.exports = {
|
|
|
140
154
|
normalizeRelativePath,
|
|
141
155
|
pathWithinRoot,
|
|
142
156
|
uniqueValues,
|
|
157
|
+
dedupeMessages,
|
|
143
158
|
parseJsonText,
|
|
144
159
|
readJsonFile,
|
|
145
160
|
sleepSync,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const crypto = require("crypto");
|
|
3
4
|
const { writeFileAtomic, pathExists, readTextIfExists } = require("./utils");
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
// Route snapshot schema version; task-group metadata keeps its own version in workflow-state.js.
|
|
7
|
+
const WORKFLOW_STATE_VERSION = 3;
|
|
8
|
+
const SUPPORTED_WORKFLOW_STATE_VERSIONS = new Set([2, WORKFLOW_STATE_VERSION]);
|
|
6
9
|
const DEFAULT_STALE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
7
10
|
const PERSISTED_NOTE_EXCLUDE_PATTERNS = [
|
|
8
11
|
/^No persisted workflow state found for this change; deriving from artifacts\.$/i,
|
|
@@ -19,16 +22,29 @@ function resolveTaskGroupMetadataPath(projectRoot, changeId) {
|
|
|
19
22
|
return path.join(projectRoot, ".da-vinci", "state", "task-groups", `${changeId}.json`);
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
function hashContent(value) {
|
|
26
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function digestForPath(targetPath) {
|
|
30
|
+
if (!pathExists(targetPath)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return hashContent(fs.readFileSync(targetPath));
|
|
35
|
+
} catch (_error) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
function fingerprintForPath(targetPath) {
|
|
23
41
|
if (!pathExists(targetPath)) {
|
|
24
42
|
return null;
|
|
25
43
|
}
|
|
26
44
|
try {
|
|
27
|
-
const stat = fs.statSync(targetPath);
|
|
28
45
|
return {
|
|
29
46
|
path: targetPath,
|
|
30
|
-
|
|
31
|
-
size: stat.size
|
|
47
|
+
digest: digestForPath(targetPath)
|
|
32
48
|
};
|
|
33
49
|
} catch (_error) {
|
|
34
50
|
return null;
|
|
@@ -46,8 +62,7 @@ function buildWorkflowFingerprint(projectRoot, changeId) {
|
|
|
46
62
|
path.join(changeRoot, "pencil-design.md"),
|
|
47
63
|
path.join(changeRoot, "pencil-bindings.md"),
|
|
48
64
|
path.join(changeRoot, "tasks.md"),
|
|
49
|
-
path.join(changeRoot, "verification.md")
|
|
50
|
-
path.join(changeRoot, "workflow.json")
|
|
65
|
+
path.join(changeRoot, "verification.md")
|
|
51
66
|
];
|
|
52
67
|
|
|
53
68
|
const specRoot = path.join(changeRoot, "specs");
|
|
@@ -96,8 +111,7 @@ function stableFingerprintHash(fingerprint) {
|
|
|
96
111
|
return JSON.stringify(
|
|
97
112
|
(fingerprint || []).map((entry) => ({
|
|
98
113
|
path: entry.path,
|
|
99
|
-
|
|
100
|
-
size: entry.size
|
|
114
|
+
digest: entry.digest
|
|
101
115
|
}))
|
|
102
116
|
);
|
|
103
117
|
}
|
|
@@ -163,7 +177,7 @@ function selectPersistedStateForChange(projectRoot, changeId, options = {}) {
|
|
|
163
177
|
};
|
|
164
178
|
}
|
|
165
179
|
|
|
166
|
-
if (loaded.state.version
|
|
180
|
+
if (!SUPPORTED_WORKFLOW_STATE_VERSIONS.has(Number(loaded.state.version))) {
|
|
167
181
|
return {
|
|
168
182
|
usable: false,
|
|
169
183
|
reason: "version-mismatch",
|
|
@@ -183,25 +197,6 @@ function selectPersistedStateForChange(projectRoot, changeId, options = {}) {
|
|
|
183
197
|
};
|
|
184
198
|
}
|
|
185
199
|
|
|
186
|
-
const persistedAt = Date.parse(String(changeRecord.persistedAt || ""));
|
|
187
|
-
if (!Number.isFinite(persistedAt)) {
|
|
188
|
-
return {
|
|
189
|
-
usable: false,
|
|
190
|
-
reason: "invalid-timestamp",
|
|
191
|
-
statePath: loaded.statePath,
|
|
192
|
-
persisted: loaded.state
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (Date.now() - persistedAt > staleWindowMs) {
|
|
197
|
-
return {
|
|
198
|
-
usable: false,
|
|
199
|
-
reason: "time-stale",
|
|
200
|
-
statePath: loaded.statePath,
|
|
201
|
-
persisted: loaded.state
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
200
|
const currentFingerprint = buildWorkflowFingerprint(projectRoot, changeId);
|
|
206
201
|
const currentHash = stableFingerprintHash(currentFingerprint);
|
|
207
202
|
const persistedHash = String(changeRecord.fingerprintHash || "");
|
|
@@ -219,7 +214,20 @@ function selectPersistedStateForChange(projectRoot, changeId, options = {}) {
|
|
|
219
214
|
reason: null,
|
|
220
215
|
statePath: loaded.statePath,
|
|
221
216
|
persisted: loaded.state,
|
|
222
|
-
changeRecord
|
|
217
|
+
changeRecord,
|
|
218
|
+
advisoryNotes:
|
|
219
|
+
(() => {
|
|
220
|
+
const persistedAt = Date.parse(String(changeRecord.persistedAt || ""));
|
|
221
|
+
if (!Number.isFinite(persistedAt)) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
if (Date.now() - persistedAt > staleWindowMs) {
|
|
225
|
+
return [
|
|
226
|
+
"Persisted workflow state is older than the historical stale-time threshold but remains usable because artifact content digests still match."
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
})()
|
|
223
231
|
};
|
|
224
232
|
}
|
|
225
233
|
|
|
@@ -234,6 +242,14 @@ function persistDerivedWorkflowResult(projectRoot, changeId, workflowResult, opt
|
|
|
234
242
|
}
|
|
235
243
|
|
|
236
244
|
const fingerprint = buildWorkflowFingerprint(projectRoot, changeId);
|
|
245
|
+
const metadataRefsNormalized =
|
|
246
|
+
metadataRefs && typeof metadataRefs === "object"
|
|
247
|
+
? {
|
|
248
|
+
...metadataRefs,
|
|
249
|
+
taskGroupsPath: metadataRefs.taskGroupsPath || null,
|
|
250
|
+
taskGroupsDigest: metadataRefs.taskGroupsDigest || null
|
|
251
|
+
}
|
|
252
|
+
: {};
|
|
237
253
|
changes[changeId] = {
|
|
238
254
|
stage: workflowResult.stage,
|
|
239
255
|
checkpointState: workflowResult.checkpointState,
|
|
@@ -243,8 +259,7 @@ function persistDerivedWorkflowResult(projectRoot, changeId, workflowResult, opt
|
|
|
243
259
|
failures: workflowResult.failures,
|
|
244
260
|
warnings: workflowResult.warnings,
|
|
245
261
|
notes: sanitizePersistedNotes(workflowResult.notes),
|
|
246
|
-
|
|
247
|
-
metadataRefs,
|
|
262
|
+
metadataRefs: metadataRefsNormalized,
|
|
248
263
|
fingerprintHash: stableFingerprintHash(fingerprint),
|
|
249
264
|
persistedAt: new Date().toISOString()
|
|
250
265
|
};
|
|
@@ -264,7 +279,10 @@ function writeTaskGroupMetadata(projectRoot, changeId, metadataPayload) {
|
|
|
264
279
|
const targetPath = resolveTaskGroupMetadataPath(projectRoot, changeId);
|
|
265
280
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
266
281
|
writeFileAtomic(targetPath, `${JSON.stringify(metadataPayload, null, 2)}\n`);
|
|
267
|
-
return
|
|
282
|
+
return {
|
|
283
|
+
path: targetPath,
|
|
284
|
+
digest: digestForPath(targetPath)
|
|
285
|
+
};
|
|
268
286
|
}
|
|
269
287
|
|
|
270
288
|
function readTaskGroupMetadata(projectRoot, changeId) {
|
|
@@ -284,11 +302,13 @@ function readTaskGroupMetadata(projectRoot, changeId) {
|
|
|
284
302
|
|
|
285
303
|
module.exports = {
|
|
286
304
|
WORKFLOW_STATE_VERSION,
|
|
305
|
+
SUPPORTED_WORKFLOW_STATE_VERSIONS,
|
|
287
306
|
DEFAULT_STALE_WINDOW_MS,
|
|
288
307
|
resolveWorkflowStatePath,
|
|
289
308
|
resolveTaskGroupMetadataPath,
|
|
290
309
|
buildWorkflowFingerprint,
|
|
291
310
|
stableFingerprintHash,
|
|
311
|
+
digestForPath,
|
|
292
312
|
sanitizePersistedNotes,
|
|
293
313
|
readPersistedWorkflowState,
|
|
294
314
|
writePersistedWorkflowState,
|