@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.
- package/CHANGELOG.md +14 -0
- package/README.md +17 -65
- package/README.zh-CN.md +17 -65
- package/commands/claude/dv/continue.md +5 -0
- package/commands/codex/prompts/dv-continue.md +6 -1
- package/commands/gemini/dv/continue.toml +5 -0
- package/commands/templates/dv-continue.shared.md +33 -0
- package/docs/dv-command-reference.md +31 -0
- package/docs/execution-chain-migration.md +46 -0
- package/docs/execution-chain-plan.md +125 -0
- package/docs/prompt-entrypoints.md +6 -0
- package/docs/workflow-examples.md +10 -0
- package/docs/workflow-overview.md +25 -0
- package/docs/zh-CN/dv-command-reference.md +31 -0
- package/docs/zh-CN/execution-chain-migration.md +46 -0
- package/docs/zh-CN/prompt-entrypoints.md +6 -0
- package/docs/zh-CN/workflow-examples.md +10 -0
- package/docs/zh-CN/workflow-overview.md +25 -0
- package/lib/artifact-parsers.js +120 -0
- package/lib/audit.js +61 -0
- package/lib/cli.js +328 -13
- package/lib/diff-spec.js +242 -0
- package/lib/execution-signals.js +136 -0
- package/lib/lint-bindings.js +143 -0
- package/lib/lint-spec.js +408 -0
- package/lib/lint-tasks.js +176 -0
- package/lib/planning-parsers.js +567 -0
- package/lib/scaffold.js +193 -0
- package/lib/scope-check.js +603 -0
- package/lib/sidecars.js +369 -0
- package/lib/supervisor-review.js +28 -3
- package/lib/utils.js +10 -2
- package/lib/verify.js +652 -0
- package/lib/workflow-contract.js +107 -0
- package/lib/workflow-persisted-state.js +297 -0
- package/lib/workflow-state.js +785 -0
- package/package.json +10 -2
- package/references/artifact-templates.md +26 -0
- package/references/checkpoints.md +14 -0
- 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
|
-
{
|
|
163
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
854
|
-
|
|
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
|
|
907
|
-
|
|
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 (
|
|
1032
|
-
|
|
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));
|
package/lib/diff-spec.js
ADDED
|
@@ -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
|
+
};
|