@xenonbyte/da-vinci-workflow 0.2.4 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -9
  3. package/README.zh-CN.md +16 -9
  4. package/SKILL.md +45 -704
  5. package/docs/dv-command-reference.md +33 -5
  6. package/docs/execution-chain-migration.md +14 -3
  7. package/docs/maintainer-bootstrap.md +102 -0
  8. package/docs/pencil-rendering-workflow.md +1 -1
  9. package/docs/prompt-entrypoints.md +1 -0
  10. package/docs/skill-contract-maintenance.md +14 -0
  11. package/docs/skill-usage.md +31 -0
  12. package/docs/workflow-overview.md +40 -5
  13. package/docs/zh-CN/dv-command-reference.md +31 -5
  14. package/docs/zh-CN/maintainer-bootstrap.md +101 -0
  15. package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
  16. package/docs/zh-CN/prompt-entrypoints.md +1 -0
  17. package/docs/zh-CN/skill-usage.md +30 -0
  18. package/docs/zh-CN/workflow-overview.md +38 -5
  19. package/lib/audit.js +19 -0
  20. package/lib/cli/helpers.js +104 -0
  21. package/lib/cli/lint-family.js +56 -0
  22. package/lib/cli/verify-family.js +79 -0
  23. package/lib/cli.js +143 -172
  24. package/lib/gate-utils.js +56 -0
  25. package/lib/install.js +134 -6
  26. package/lib/lint-bindings.js +41 -28
  27. package/lib/lint-spec.js +403 -109
  28. package/lib/lint-tasks.js +571 -21
  29. package/lib/maintainer-readiness.js +317 -0
  30. package/lib/planning-parsers.js +198 -2
  31. package/lib/planning-quality-utils.js +81 -0
  32. package/lib/planning-signal-freshness.js +205 -0
  33. package/lib/scaffold.js +454 -23
  34. package/lib/scope-check.js +751 -82
  35. package/lib/sidecars.js +396 -1
  36. package/lib/task-review.js +2 -1
  37. package/lib/utils.js +34 -0
  38. package/lib/verify.js +1160 -88
  39. package/lib/workflow-persisted-state.js +52 -32
  40. package/lib/workflow-state.js +1187 -249
  41. package/package.json +1 -1
  42. package/references/skill-workflow-detail.md +66 -0
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
  };
@@ -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
- if (!specSignal || String(specSignal.status || "").toUpperCase() !== "PASS") {
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
@@ -28,10 +28,41 @@ function readTextIfExists(targetPath, options = {}) {
28
28
  return fs.readFileSync(targetPath, encoding);
29
29
  }
30
30
 
31
+ function normalizeRelativePath(relativePath) {
32
+ return String(relativePath || "")
33
+ .split(path.sep)
34
+ .filter(Boolean)
35
+ .join("/");
36
+ }
37
+
38
+ function pathWithinRoot(projectRoot, candidatePath) {
39
+ const root = path.resolve(projectRoot);
40
+ const candidate = path.resolve(candidatePath);
41
+ if (candidate === root) {
42
+ return true;
43
+ }
44
+ const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
45
+ return candidate.startsWith(prefix);
46
+ }
47
+
31
48
  function uniqueValues(values) {
32
49
  return Array.from(new Set((values || []).filter(Boolean)));
33
50
  }
34
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
+
35
66
  function parseJsonText(raw, context = "JSON payload") {
36
67
  try {
37
68
  return JSON.parse(String(raw));
@@ -120,7 +151,10 @@ module.exports = {
120
151
  escapeRegExp,
121
152
  pathExists,
122
153
  readTextIfExists,
154
+ normalizeRelativePath,
155
+ pathWithinRoot,
123
156
  uniqueValues,
157
+ dedupeMessages,
124
158
  parseJsonText,
125
159
  readJsonFile,
126
160
  sleepSync,