@xenonbyte/da-vinci-workflow 0.1.26 → 0.2.1

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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +17 -65
  3. package/README.zh-CN.md +17 -65
  4. package/commands/claude/dv/continue.md +5 -0
  5. package/commands/codex/prompts/dv-continue.md +6 -1
  6. package/commands/gemini/dv/continue.toml +5 -0
  7. package/commands/templates/dv-continue.shared.md +33 -0
  8. package/docs/dv-command-reference.md +31 -0
  9. package/docs/execution-chain-migration.md +46 -0
  10. package/docs/execution-chain-plan.md +125 -0
  11. package/docs/prompt-entrypoints.md +6 -0
  12. package/docs/workflow-examples.md +10 -0
  13. package/docs/workflow-overview.md +25 -0
  14. package/docs/zh-CN/dv-command-reference.md +31 -0
  15. package/docs/zh-CN/execution-chain-migration.md +46 -0
  16. package/docs/zh-CN/prompt-entrypoints.md +6 -0
  17. package/docs/zh-CN/workflow-examples.md +10 -0
  18. package/docs/zh-CN/workflow-overview.md +25 -0
  19. package/lib/artifact-parsers.js +120 -0
  20. package/lib/audit.js +61 -0
  21. package/lib/cli.js +328 -13
  22. package/lib/diff-spec.js +242 -0
  23. package/lib/execution-signals.js +136 -0
  24. package/lib/lint-bindings.js +143 -0
  25. package/lib/lint-spec.js +408 -0
  26. package/lib/lint-tasks.js +176 -0
  27. package/lib/planning-parsers.js +567 -0
  28. package/lib/scaffold.js +193 -0
  29. package/lib/scope-check.js +603 -0
  30. package/lib/sidecars.js +369 -0
  31. package/lib/supervisor-review.js +28 -3
  32. package/lib/utils.js +10 -2
  33. package/lib/verify.js +652 -0
  34. package/lib/workflow-contract.js +107 -0
  35. package/lib/workflow-persisted-state.js +297 -0
  36. package/lib/workflow-state.js +785 -0
  37. package/package.json +10 -2
  38. package/references/artifact-templates.md +26 -0
  39. package/references/checkpoints.md +14 -0
  40. package/references/modes.md +10 -0
package/lib/cli.js CHANGED
@@ -54,8 +54,30 @@ const {
54
54
  bootstrapProjectArtifacts,
55
55
  formatBootstrapProjectReport
56
56
  } = require("./workflow-bootstrap");
57
+ const {
58
+ deriveWorkflowStatus,
59
+ formatWorkflowStatusReport,
60
+ formatNextStepReport
61
+ } = require("./workflow-state");
62
+ const { lintRuntimeSpecs, formatLintSpecReport } = require("./lint-spec");
63
+ const { runScopeCheck, formatScopeCheckReport } = require("./scope-check");
64
+ const { lintTasks, formatLintTasksReport } = require("./lint-tasks");
65
+ const { lintBindings, formatLintBindingsReport } = require("./lint-bindings");
66
+ const { generatePlanningSidecars, formatGenerateSidecarsReport } = require("./sidecars");
67
+ const {
68
+ verifyBindings,
69
+ verifyImplementation,
70
+ verifyStructure,
71
+ verifyCoverage,
72
+ formatVerifyReport
73
+ } = require("./verify");
74
+ const { diffSpec, formatDiffSpecReport } = require("./diff-spec");
75
+ const { scaffoldFromBindings, formatScaffoldReport } = require("./scaffold");
76
+ const { writeExecutionSignal } = require("./execution-signals");
57
77
 
58
78
  const DEFAULT_MAX_PREFLIGHT_STDIN_BYTES = 1024 * 1024;
79
+ const DEFAULT_MAX_STDIN_TRANSIENT_RETRIES = 2000;
80
+ const DEFAULT_MAX_STDIN_TRANSIENT_BACKOFF_MS = 25;
59
81
  const OPTION_FLAGS_WITH_VALUES = new Set([
60
82
  "--home",
61
83
  "--platform",
@@ -159,10 +181,20 @@ const HELP_OPTION_SPECS = [
159
181
  { flag: "--revision-outcome <text>", description: "supervisor-review revision result summary" },
160
182
  { flag: "--top <value>", description: "icon-search result count (1-50, default 8)" },
161
183
  { flag: "--timeout-ms <value>", description: "network timeout for icon-sync requests" },
162
- { flag: "--strict", description: "fail icon-sync when any upstream source request fails" },
163
- { flag: "--json", description: "print structured JSON for icon-search" },
184
+ {
185
+ flag: "--strict",
186
+ description: "enable strict failure mode for commands that support advisory defaults (for example icon-sync, lint-spec)"
187
+ },
188
+ {
189
+ flag: "--continue-on-error",
190
+ description: "print BLOCK/FAIL command results without throwing process errors"
191
+ },
192
+ { flag: "--json", description: "print structured JSON output when supported by the command" },
164
193
  { flag: "--pen <path>", description: "registered .pen path for sync checks" },
165
- { flag: "--from <path>", description: "source .pen path for sync-pen-source" },
194
+ {
195
+ flag: "--from <path>",
196
+ description: "source path for sync-pen-source, or baseline sidecars directory for diff-spec"
197
+ },
166
198
  { flag: "--to <path>", description: "destination .pen path for sync-pen-source" },
167
199
  { flag: "--ops-file <path>", description: "Pencil batch operations file for preflight" },
168
200
  { flag: "--input <path>", description: "input .pen file for snapshot-pen" },
@@ -178,11 +210,19 @@ const HELP_OPTION_SPECS = [
178
210
  { flag: "--force", description: "overwrite bootstrap placeholders or force commands that explicitly support it" }
179
211
  ];
180
212
 
181
- function readLimitedStdin(maxBytes = DEFAULT_MAX_PREFLIGHT_STDIN_BYTES) {
213
+ function readLimitedStdin(maxBytes = DEFAULT_MAX_PREFLIGHT_STDIN_BYTES, options = {}) {
182
214
  const limit =
183
215
  Number.isFinite(Number(maxBytes)) && Number(maxBytes) > 0
184
216
  ? Number(maxBytes)
185
217
  : DEFAULT_MAX_PREFLIGHT_STDIN_BYTES;
218
+ const maxTransientRetries =
219
+ Number.isFinite(Number(options.maxTransientRetries)) && Number(options.maxTransientRetries) >= 0
220
+ ? Number(options.maxTransientRetries)
221
+ : DEFAULT_MAX_STDIN_TRANSIENT_RETRIES;
222
+ const maxBackoffMs =
223
+ Number.isFinite(Number(options.maxBackoffMs)) && Number(options.maxBackoffMs) > 0
224
+ ? Number(options.maxBackoffMs)
225
+ : DEFAULT_MAX_STDIN_TRANSIENT_BACKOFF_MS;
186
226
  const chunks = [];
187
227
  let totalBytes = 0;
188
228
  let transientReadRetries = 0;
@@ -195,7 +235,12 @@ function readLimitedStdin(maxBytes = DEFAULT_MAX_PREFLIGHT_STDIN_BYTES) {
195
235
  } catch (error) {
196
236
  if (error && (error.code === "EAGAIN" || error.code === "EINTR")) {
197
237
  transientReadRetries += 1;
198
- sleepSyncMs(Math.min(5 * transientReadRetries, 25));
238
+ if (transientReadRetries > maxTransientRetries) {
239
+ throw new Error(
240
+ `Unable to read stdin after ${transientReadRetries} transient retry attempts (${error.code}).`
241
+ );
242
+ }
243
+ sleepSyncMs(Math.min(5 * transientReadRetries, maxBackoffMs));
199
244
  continue;
200
245
  }
201
246
  throw error;
@@ -285,6 +330,21 @@ function getOptionValues(args, name) {
285
330
  .map((entry) => entry.value);
286
331
  }
287
332
 
333
+ function shouldContinueOnError(args) {
334
+ return Array.isArray(args) && args.includes("--continue-on-error");
335
+ }
336
+
337
+ function emitOrThrowOnStatus(status, blockedStatuses, output, continueOnError) {
338
+ if (!Array.isArray(blockedStatuses) || !blockedStatuses.includes(status)) {
339
+ return false;
340
+ }
341
+ if (continueOnError) {
342
+ console.log(output);
343
+ return true;
344
+ }
345
+ throw new Error(output);
346
+ }
347
+
288
348
  function getIntegerOption(args, name, options = {}) {
289
349
  const raw = getOption(args, name);
290
350
  if (raw === undefined) {
@@ -372,6 +432,32 @@ function appendStatusIssues(lines, label, missing = [], mismatched = [], unreada
372
432
  }
373
433
  }
374
434
 
435
+ function persistExecutionSignal(projectPath, changeId, surface, result, strict = false) {
436
+ try {
437
+ writeExecutionSignal(projectPath, {
438
+ changeId: changeId || "global",
439
+ surface,
440
+ status: result.status,
441
+ advisory: strict ? false : true,
442
+ strict,
443
+ failures: result.failures || [],
444
+ warnings: result.warnings || [],
445
+ notes: result.notes || []
446
+ });
447
+ } catch (error) {
448
+ // Signals are advisory metadata and should not break command execution.
449
+ const code = error && error.code ? String(error.code).toUpperCase() : "";
450
+ if (code === "EACCES" || code === "ENOSPC") {
451
+ return;
452
+ }
453
+
454
+ const message = error && error.message ? error.message : String(error);
455
+ console.error(
456
+ `Warning: failed to persist execution signal (${surface}) for change ${changeId || "global"}: ${message}`
457
+ );
458
+ }
459
+ }
460
+
375
461
  function printHelp() {
376
462
  const optionLines = HELP_OPTION_SPECS.map((optionSpec) => {
377
463
  const paddedFlag = optionSpec.flag.padEnd(30, " ");
@@ -386,6 +472,19 @@ function printHelp() {
386
472
  " da-vinci install --platform codex,claude,gemini",
387
473
  " da-vinci uninstall --platform codex,claude,gemini",
388
474
  " da-vinci status",
475
+ " da-vinci workflow-status [--project <path>] [--change <id>] [--json]",
476
+ " da-vinci next-step [--project <path>] [--change <id>] [--json]",
477
+ " da-vinci lint-spec [--project <path>] [--change <id>] [--strict] [--json]",
478
+ " da-vinci scope-check [--project <path>] [--change <id>] [--strict] [--json]",
479
+ " da-vinci lint-tasks [--project <path>] [--change <id>] [--strict] [--json]",
480
+ " da-vinci lint-bindings [--project <path>] [--change <id>] [--strict] [--json]",
481
+ " da-vinci generate-sidecars [--project <path>] [--change <id>] [--json]",
482
+ " da-vinci verify-bindings [--project <path>] [--change <id>] [--strict] [--json]",
483
+ " da-vinci verify-implementation [--project <path>] [--change <id>] [--strict] [--json]",
484
+ " da-vinci verify-structure [--project <path>] [--change <id>] [--strict] [--json]",
485
+ " da-vinci verify-coverage [--project <path>] [--change <id>] [--strict] [--json]",
486
+ " da-vinci diff-spec [--project <path>] [--change <id>] [--from <sidecars-dir>] [--json]",
487
+ " da-vinci scaffold [--project <path>] [--change <id>] [--output <path>] [--json]",
389
488
  " da-vinci validate-assets",
390
489
  " da-vinci bootstrap-project --project <path> [--change <id>] [--force]",
391
490
  " da-vinci audit [project-path]",
@@ -803,6 +902,7 @@ async function runCli(argv) {
803
902
  const [command] = argv;
804
903
  const homeDir = getOption(argv, "--home");
805
904
  const positionalArgs = getPositionalArgs(argv.slice(1), OPTION_FLAGS_WITH_VALUES);
905
+ const continueOnError = shouldContinueOnError(argv);
806
906
 
807
907
  if (!command || command === "help" || command === "--help" || command === "-h") {
808
908
  printHelp();
@@ -837,6 +937,216 @@ async function runCli(argv) {
837
937
  return;
838
938
  }
839
939
 
940
+ if (command === "workflow-status") {
941
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
942
+ const changeId = getOption(argv, "--change");
943
+ const result = deriveWorkflowStatus(projectPath, { changeId });
944
+
945
+ if (argv.includes("--json")) {
946
+ console.log(JSON.stringify(result, null, 2));
947
+ return;
948
+ }
949
+
950
+ console.log(formatWorkflowStatusReport(result));
951
+ return;
952
+ }
953
+
954
+ if (command === "next-step") {
955
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
956
+ const changeId = getOption(argv, "--change");
957
+ const result = deriveWorkflowStatus(projectPath, { changeId });
958
+
959
+ if (argv.includes("--json")) {
960
+ console.log(JSON.stringify(result.nextStep || null, null, 2));
961
+ return;
962
+ }
963
+
964
+ console.log(formatNextStepReport(result));
965
+ return;
966
+ }
967
+
968
+ if (command === "lint-spec") {
969
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
970
+ const changeId = getOption(argv, "--change");
971
+ const strict = argv.includes("--strict");
972
+ const result = lintRuntimeSpecs(projectPath, { changeId, strict });
973
+ persistExecutionSignal(projectPath, result.changeId || changeId, "lint-spec", result, strict);
974
+ const useJson = argv.includes("--json");
975
+ const output = useJson ? JSON.stringify(result, null, 2) : formatLintSpecReport(result);
976
+
977
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
978
+ return;
979
+ }
980
+
981
+ console.log(output);
982
+ return;
983
+ }
984
+
985
+ if (command === "scope-check") {
986
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
987
+ const changeId = getOption(argv, "--change");
988
+ const strict = argv.includes("--strict");
989
+ const result = runScopeCheck(projectPath, { changeId, strict });
990
+ persistExecutionSignal(projectPath, result.changeId || changeId, "scope-check", result, strict);
991
+ const useJson = argv.includes("--json");
992
+ const output = useJson ? JSON.stringify(result, null, 2) : formatScopeCheckReport(result);
993
+
994
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
995
+ return;
996
+ }
997
+
998
+ console.log(output);
999
+ return;
1000
+ }
1001
+
1002
+ if (command === "lint-tasks") {
1003
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1004
+ const changeId = getOption(argv, "--change");
1005
+ const strict = argv.includes("--strict");
1006
+ const result = lintTasks(projectPath, { changeId, strict });
1007
+ persistExecutionSignal(projectPath, result.changeId || changeId, "lint-tasks", result, strict);
1008
+ const useJson = argv.includes("--json");
1009
+ const output = useJson ? JSON.stringify(result, null, 2) : formatLintTasksReport(result);
1010
+
1011
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1012
+ return;
1013
+ }
1014
+
1015
+ console.log(output);
1016
+ return;
1017
+ }
1018
+
1019
+ if (command === "lint-bindings") {
1020
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1021
+ const changeId = getOption(argv, "--change");
1022
+ const strict = argv.includes("--strict");
1023
+ const result = lintBindings(projectPath, { changeId, strict });
1024
+ persistExecutionSignal(projectPath, result.changeId || changeId, "lint-bindings", result, strict);
1025
+ const useJson = argv.includes("--json");
1026
+ const output = useJson ? JSON.stringify(result, null, 2) : formatLintBindingsReport(result);
1027
+
1028
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1029
+ return;
1030
+ }
1031
+
1032
+ console.log(output);
1033
+ return;
1034
+ }
1035
+
1036
+ if (command === "generate-sidecars") {
1037
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1038
+ const changeId = getOption(argv, "--change");
1039
+ const result = generatePlanningSidecars(projectPath, { changeId, write: true });
1040
+ persistExecutionSignal(projectPath, result.changeId || changeId, "generate-sidecars", result, false);
1041
+ const useJson = argv.includes("--json");
1042
+ const output = useJson ? JSON.stringify(result, null, 2) : formatGenerateSidecarsReport(result);
1043
+
1044
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1045
+ return;
1046
+ }
1047
+
1048
+ console.log(output);
1049
+ return;
1050
+ }
1051
+
1052
+ if (command === "verify-bindings") {
1053
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1054
+ const changeId = getOption(argv, "--change");
1055
+ const strict = argv.includes("--strict");
1056
+ const result = verifyBindings(projectPath, { changeId, strict });
1057
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-bindings", result, strict);
1058
+ const useJson = argv.includes("--json");
1059
+ const output = useJson
1060
+ ? JSON.stringify(result, null, 2)
1061
+ : formatVerifyReport(result, "Da Vinci verify-bindings");
1062
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1063
+ return;
1064
+ }
1065
+ console.log(output);
1066
+ return;
1067
+ }
1068
+
1069
+ if (command === "verify-implementation") {
1070
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1071
+ const changeId = getOption(argv, "--change");
1072
+ const strict = argv.includes("--strict");
1073
+ const result = verifyImplementation(projectPath, { changeId, strict });
1074
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-implementation", result, strict);
1075
+ const useJson = argv.includes("--json");
1076
+ const output = useJson
1077
+ ? JSON.stringify(result, null, 2)
1078
+ : formatVerifyReport(result, "Da Vinci verify-implementation");
1079
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1080
+ return;
1081
+ }
1082
+ console.log(output);
1083
+ return;
1084
+ }
1085
+
1086
+ if (command === "verify-structure") {
1087
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1088
+ const changeId = getOption(argv, "--change");
1089
+ const strict = argv.includes("--strict");
1090
+ const result = verifyStructure(projectPath, { changeId, strict });
1091
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-structure", result, strict);
1092
+ const useJson = argv.includes("--json");
1093
+ const output = useJson
1094
+ ? JSON.stringify(result, null, 2)
1095
+ : formatVerifyReport(result, "Da Vinci verify-structure");
1096
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1097
+ return;
1098
+ }
1099
+ console.log(output);
1100
+ return;
1101
+ }
1102
+
1103
+ if (command === "verify-coverage") {
1104
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1105
+ const changeId = getOption(argv, "--change");
1106
+ const strict = argv.includes("--strict");
1107
+ const result = verifyCoverage(projectPath, { changeId, strict });
1108
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-coverage", result, strict);
1109
+ const useJson = argv.includes("--json");
1110
+ const output = useJson
1111
+ ? JSON.stringify(result, null, 2)
1112
+ : formatVerifyReport(result, "Da Vinci verify-coverage");
1113
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1114
+ return;
1115
+ }
1116
+ console.log(output);
1117
+ return;
1118
+ }
1119
+
1120
+ if (command === "diff-spec") {
1121
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1122
+ const changeId = getOption(argv, "--change");
1123
+ const fromDir = getOption(argv, "--from");
1124
+ const result = diffSpec(projectPath, { changeId, fromDir });
1125
+ persistExecutionSignal(projectPath, result.changeId || changeId, "diff-spec", result, false);
1126
+ const useJson = argv.includes("--json");
1127
+ const output = useJson ? JSON.stringify(result, null, 2) : formatDiffSpecReport(result);
1128
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1129
+ return;
1130
+ }
1131
+ console.log(output);
1132
+ return;
1133
+ }
1134
+
1135
+ if (command === "scaffold") {
1136
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1137
+ const changeId = getOption(argv, "--change");
1138
+ const outputDir = getOption(argv, "--output");
1139
+ const result = scaffoldFromBindings(projectPath, { changeId, outputDir });
1140
+ persistExecutionSignal(projectPath, result.changeId || changeId, "scaffold", result, false);
1141
+ const useJson = argv.includes("--json");
1142
+ const output = useJson ? JSON.stringify(result, null, 2) : formatScaffoldReport(result);
1143
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1144
+ return;
1145
+ }
1146
+ console.log(output);
1147
+ return;
1148
+ }
1149
+
840
1150
  if (command === "validate-assets") {
841
1151
  const result = validateAssets();
842
1152
  console.log(`Da Vinci v${result.version} assets are complete (${result.requiredAssets} required files).`);
@@ -850,8 +1160,8 @@ async function runCli(argv) {
850
1160
  const result = auditProject(projectPath, { mode, changeId });
851
1161
  const report = formatAuditReport(result);
852
1162
 
853
- if (result.status === "FAIL") {
854
- throw new Error(report);
1163
+ if (emitOrThrowOnStatus(result.status, ["FAIL"], report, continueOnError)) {
1164
+ return;
855
1165
  }
856
1166
 
857
1167
  console.log(report);
@@ -903,8 +1213,8 @@ async function runCli(argv) {
903
1213
  const result = preflightPencilBatch(operations);
904
1214
  const report = formatPencilPreflightReport(result);
905
1215
 
906
- if (result.status === "FAIL") {
907
- throw new Error(report);
1216
+ if (emitOrThrowOnStatus(result.status, ["FAIL"], report, continueOnError)) {
1217
+ return;
908
1218
  }
909
1219
 
910
1220
  console.log(report);
@@ -1028,13 +1338,18 @@ async function runCli(argv) {
1028
1338
  preferredSource
1029
1339
  });
1030
1340
 
1031
- if (result.status === "BLOCK") {
1032
- throw new Error(
1341
+ if (
1342
+ emitOrThrowOnStatus(
1343
+ result.status,
1344
+ ["BLOCK"],
1033
1345
  [
1034
1346
  "Baseline alignment check failed.",
1035
1347
  formatPenBaselineAlignmentReport(result)
1036
- ].join("\n")
1037
- );
1348
+ ].join("\n"),
1349
+ continueOnError
1350
+ )
1351
+ ) {
1352
+ return;
1038
1353
  }
1039
1354
 
1040
1355
  console.log(formatPenBaselineAlignmentReport(result));
@@ -0,0 +1,242 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { STATUS } = require("./workflow-contract");
4
+ const { pathExists } = require("./utils");
5
+ const { generatePlanningSidecars } = require("./sidecars");
6
+ const { digestObject } = require("./planning-parsers");
7
+
8
+ function keyByText(items) {
9
+ const map = new Map();
10
+ for (const item of items || []) {
11
+ const text = String(item && item.text ? item.text : item || "").trim();
12
+ if (!text) {
13
+ continue;
14
+ }
15
+ map.set(text, item);
16
+ }
17
+ return map;
18
+ }
19
+
20
+ function readJsonFileIfExists(filePath) {
21
+ if (!filePath || !pathExists(filePath)) {
22
+ return null;
23
+ }
24
+ try {
25
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
26
+ } catch (_error) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function diffTextCollection(baseItems, headItems) {
32
+ const baseMap = keyByText(baseItems);
33
+ const headMap = keyByText(headItems);
34
+ const added = [];
35
+ const removed = [];
36
+ const modified = [];
37
+
38
+ for (const text of headMap.keys()) {
39
+ if (!baseMap.has(text)) {
40
+ added.push(text);
41
+ }
42
+ }
43
+ for (const text of baseMap.keys()) {
44
+ if (!headMap.has(text)) {
45
+ removed.push(text);
46
+ }
47
+ }
48
+
49
+ for (const text of headMap.keys()) {
50
+ if (!baseMap.has(text)) {
51
+ continue;
52
+ }
53
+ const baseRecord = baseMap.get(text) || {};
54
+ const headRecord = headMap.get(text) || {};
55
+ if (String(baseRecord.specPath || "") !== String(headRecord.specPath || "")) {
56
+ modified.push({
57
+ text,
58
+ from: String(baseRecord.specPath || ""),
59
+ to: String(headRecord.specPath || "")
60
+ });
61
+ }
62
+ }
63
+
64
+ return {
65
+ added: added.sort((a, b) => a.localeCompare(b)),
66
+ removed: removed.sort((a, b) => a.localeCompare(b)),
67
+ modified
68
+ };
69
+ }
70
+
71
+ function hasAnyDiff(diff) {
72
+ return (
73
+ (diff.added && diff.added.length > 0) ||
74
+ (diff.removed && diff.removed.length > 0) ||
75
+ (diff.modified && diff.modified.length > 0)
76
+ );
77
+ }
78
+
79
+ function chooseBaseSidecars(projectRoot, changeId, fromDir) {
80
+ const sidecarDir = fromDir || path.join(projectRoot, ".da-vinci", "changes", changeId, "sidecars");
81
+ return {
82
+ spec: readJsonFileIfExists(path.join(sidecarDir, "spec.index.json")),
83
+ tasks: readJsonFileIfExists(path.join(sidecarDir, "tasks.index.json")),
84
+ pageMap: readJsonFileIfExists(path.join(sidecarDir, "page-map.index.json")),
85
+ bindings: readJsonFileIfExists(path.join(sidecarDir, "bindings.index.json")),
86
+ sidecarDir
87
+ };
88
+ }
89
+
90
+ function diffSpec(projectPathInput, options = {}) {
91
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
92
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
93
+ const sidecarResult = generatePlanningSidecars(projectRoot, {
94
+ changeId: requestedChangeId,
95
+ write: false
96
+ });
97
+ if (sidecarResult.failures.length > 0 || !sidecarResult.changeId) {
98
+ return {
99
+ status: STATUS.BLOCK,
100
+ failures: sidecarResult.failures,
101
+ warnings: sidecarResult.warnings,
102
+ notes: sidecarResult.notes,
103
+ projectRoot,
104
+ changeId: sidecarResult.changeId || null,
105
+ diff: null
106
+ };
107
+ }
108
+
109
+ const changeId = sidecarResult.changeId;
110
+ const baseSidecars = chooseBaseSidecars(projectRoot, changeId, options.fromDir);
111
+ const headSidecars = sidecarResult.sidecars;
112
+
113
+ if (!baseSidecars.spec) {
114
+ return {
115
+ status: STATUS.WARN,
116
+ failures: [],
117
+ warnings: [
118
+ `No baseline sidecars found in ${baseSidecars.sidecarDir}. Run \`da-vinci generate-sidecars\` before diff-spec.`
119
+ ],
120
+ notes: [
121
+ "diff-spec currently stays under the `diff-spec` surface and includes broader planning summaries for page-map/bindings/tasks."
122
+ ],
123
+ projectRoot,
124
+ changeId,
125
+ diff: {
126
+ spec: null,
127
+ tasks: null,
128
+ pageMap: null,
129
+ bindings: null
130
+ }
131
+ };
132
+ }
133
+
134
+ const baseSpecCollections = baseSidecars.spec.collections || {};
135
+ const headSpecCollections = headSidecars["spec.index.json"].collections || {};
136
+ const baseSpecDigest = digestObject(baseSidecars.spec);
137
+ const headSpecDigest = digestObject(headSidecars["spec.index.json"]);
138
+ const specDiff = {
139
+ behavior: diffTextCollection(baseSpecCollections.behavior || [], headSpecCollections.behavior || []),
140
+ states: diffTextCollection(baseSpecCollections.states || [], headSpecCollections.states || []),
141
+ inputs: diffTextCollection(baseSpecCollections.inputs || [], headSpecCollections.inputs || []),
142
+ outputs: diffTextCollection(baseSpecCollections.outputs || [], headSpecCollections.outputs || []),
143
+ acceptance: diffTextCollection(baseSpecCollections.acceptance || [], headSpecCollections.acceptance || [])
144
+ };
145
+
146
+ const tasksDiff = {
147
+ taskGroups: diffTextCollection(
148
+ (baseSidecars.tasks && baseSidecars.tasks.taskGroups
149
+ ? baseSidecars.tasks.taskGroups.map((item) => ({ text: `${item.id}: ${item.title}` }))
150
+ : []),
151
+ headSidecars["tasks.index.json"].taskGroups.map((item) => ({ text: `${item.id}: ${item.title}` }))
152
+ )
153
+ };
154
+
155
+ const pageMapDiff = {
156
+ pages: diffTextCollection(
157
+ (baseSidecars.pageMap && baseSidecars.pageMap.pages
158
+ ? baseSidecars.pageMap.pages.map((page) => ({ text: page }))
159
+ : []),
160
+ headSidecars["page-map.index.json"].pages.map((page) => ({ text: page }))
161
+ )
162
+ };
163
+
164
+ const bindingsDiff = {
165
+ mappings: diffTextCollection(
166
+ (baseSidecars.bindings && baseSidecars.bindings.mappings
167
+ ? baseSidecars.bindings.mappings.map((item) => ({
168
+ text: `${item.implementation} -> ${item.designPage}`
169
+ }))
170
+ : []),
171
+ headSidecars["bindings.index.json"].mappings.map((item) => ({
172
+ text: `${item.implementation} -> ${item.designPage}`
173
+ }))
174
+ )
175
+ };
176
+
177
+ const hasChanges =
178
+ Object.values(specDiff).some((entry) => hasAnyDiff(entry)) ||
179
+ baseSpecDigest !== headSpecDigest ||
180
+ hasAnyDiff(tasksDiff.taskGroups) ||
181
+ hasAnyDiff(pageMapDiff.pages) ||
182
+ hasAnyDiff(bindingsDiff.mappings);
183
+
184
+ return {
185
+ status: hasChanges ? STATUS.WARN : STATUS.PASS,
186
+ failures: [],
187
+ warnings: hasChanges ? ["Planning sidecar differences detected."] : [],
188
+ notes: [
189
+ "diff-spec includes normalized spec deltas and broader planning summaries (tasks/page-map/bindings)."
190
+ ],
191
+ projectRoot,
192
+ changeId,
193
+ diff: {
194
+ spec: specDiff,
195
+ specDigestChanged: baseSpecDigest !== headSpecDigest,
196
+ tasks: tasksDiff,
197
+ pageMap: pageMapDiff,
198
+ bindings: bindingsDiff
199
+ }
200
+ };
201
+ }
202
+
203
+ function formatDiffSpecReport(result) {
204
+ const lines = [
205
+ "Da Vinci diff-spec",
206
+ `Project: ${result.projectRoot}`,
207
+ `Change: ${result.changeId || "(not selected)"}`,
208
+ `Status: ${result.status}`
209
+ ];
210
+ if (result.failures && result.failures.length > 0) {
211
+ lines.push("", "Failures:");
212
+ for (const failure of result.failures) {
213
+ lines.push(`- ${failure}`);
214
+ }
215
+ }
216
+ if (result.warnings && result.warnings.length > 0) {
217
+ lines.push("", "Warnings:");
218
+ for (const warning of result.warnings) {
219
+ lines.push(`- ${warning}`);
220
+ }
221
+ }
222
+ if (result.diff && result.diff.spec) {
223
+ lines.push("", "Spec deltas:");
224
+ for (const [section, entry] of Object.entries(result.diff.spec)) {
225
+ lines.push(
226
+ `- ${section}: +${entry.added.length} / -${entry.removed.length} / ~${entry.modified.length}`
227
+ );
228
+ }
229
+ }
230
+ if (result.notes && result.notes.length > 0) {
231
+ lines.push("", "Notes:");
232
+ for (const note of result.notes) {
233
+ lines.push(`- ${note}`);
234
+ }
235
+ }
236
+ return lines.join("\n");
237
+ }
238
+
239
+ module.exports = {
240
+ diffSpec,
241
+ formatDiffSpecReport
242
+ };