@xenonbyte/da-vinci-workflow 0.1.14 → 0.1.16

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 (46) hide show
  1. package/CHANGELOG.md +20 -2
  2. package/README.md +41 -1
  3. package/README.zh-CN.md +42 -1
  4. package/SKILL.md +22 -0
  5. package/commands/claude/dv/design.md +8 -0
  6. package/commands/claude/dv/verify.md +2 -0
  7. package/commands/codex/prompts/dv-design.md +8 -0
  8. package/commands/codex/prompts/dv-verify.md +1 -0
  9. package/commands/gemini/dv/design.toml +8 -0
  10. package/commands/gemini/dv/verify.toml +1 -0
  11. package/docs/mcp-aware-gate-implementation.md +291 -0
  12. package/docs/mcp-aware-gate-tests.md +244 -0
  13. package/docs/mcp-aware-gate.md +246 -0
  14. package/docs/mode-use-cases.md +7 -1
  15. package/docs/prompt-presets/README.md +3 -0
  16. package/docs/prompt-presets/desktop-app.md +19 -1
  17. package/docs/prompt-presets/mobile-app.md +19 -1
  18. package/docs/prompt-presets/tablet-app.md +19 -1
  19. package/docs/prompt-presets/web-app.md +19 -1
  20. package/docs/visual-assist-presets/README.md +5 -0
  21. package/docs/workflow-examples.md +24 -5
  22. package/docs/zh-CN/mcp-aware-gate-implementation.md +290 -0
  23. package/docs/zh-CN/mcp-aware-gate-tests.md +244 -0
  24. package/docs/zh-CN/mcp-aware-gate.md +249 -0
  25. package/docs/zh-CN/mode-use-cases.md +15 -4
  26. package/docs/zh-CN/prompt-presets/README.md +3 -0
  27. package/docs/zh-CN/prompt-presets/desktop-app.md +19 -1
  28. package/docs/zh-CN/prompt-presets/mobile-app.md +19 -1
  29. package/docs/zh-CN/prompt-presets/tablet-app.md +19 -1
  30. package/docs/zh-CN/prompt-presets/web-app.md +19 -1
  31. package/docs/zh-CN/visual-assist-presets/README.md +5 -0
  32. package/docs/zh-CN/workflow-examples.md +24 -5
  33. package/lib/audit.js +348 -0
  34. package/lib/cli.js +142 -1
  35. package/lib/mcp-runtime-gate.js +342 -0
  36. package/lib/pen-persistence.js +326 -0
  37. package/lib/pencil-preflight.js +438 -0
  38. package/package.json +5 -2
  39. package/references/artifact-templates.md +28 -1
  40. package/references/checkpoints.md +75 -1
  41. package/references/design-inputs.md +2 -1
  42. package/references/pencil-design-to-code.md +16 -0
  43. package/scripts/fixtures/complex-sample.pen +295 -0
  44. package/scripts/test-mcp-runtime-gate.js +199 -0
  45. package/scripts/test-pen-persistence.js +110 -0
  46. package/scripts/test-pencil-preflight.js +153 -0
package/lib/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ const fs = require("fs");
1
2
  const {
2
3
  VERSION,
3
4
  installPlatforms,
@@ -5,6 +6,16 @@ const {
5
6
  getStatus,
6
7
  validateAssets
7
8
  } = require("./install");
9
+ const { auditProject, formatAuditReport } = require("./audit");
10
+ const {
11
+ preflightPencilBatch,
12
+ formatPencilPreflightReport,
13
+ readOperations
14
+ } = require("./pencil-preflight");
15
+ const {
16
+ writePenFromPayloadFiles,
17
+ snapshotPenFile
18
+ } = require("./pen-persistence");
8
19
 
9
20
  function getOption(args, name) {
10
21
  const direct = args.find((arg) => arg.startsWith(`${name}=`));
@@ -20,6 +31,31 @@ function getOption(args, name) {
20
31
  return undefined;
21
32
  }
22
33
 
34
+ function getPositionalArgs(args, optionsWithValues = []) {
35
+ const positional = [];
36
+
37
+ for (let index = 0; index < args.length; index += 1) {
38
+ const arg = args[index];
39
+
40
+ if (optionsWithValues.includes(arg)) {
41
+ index += 1;
42
+ continue;
43
+ }
44
+
45
+ if (optionsWithValues.some((name) => arg.startsWith(`${name}=`))) {
46
+ continue;
47
+ }
48
+
49
+ if (arg.startsWith("-")) {
50
+ continue;
51
+ }
52
+
53
+ positional.push(arg);
54
+ }
55
+
56
+ return positional;
57
+ }
58
+
23
59
  function formatStatus(status) {
24
60
  return [
25
61
  `Da Vinci v${status.version}`,
@@ -40,11 +76,25 @@ function printHelp() {
40
76
  " da-vinci uninstall --platform codex,claude,gemini",
41
77
  " da-vinci status",
42
78
  " da-vinci validate-assets",
79
+ " da-vinci audit [project-path]",
80
+ " da-vinci preflight-pencil --ops-file <path>",
81
+ " da-vinci write-pen --output <path> --nodes-file <path> [--variables-file <path>]",
82
+ " da-vinci snapshot-pen --input <path> --output <path>",
43
83
  " da-vinci --version",
44
84
  "",
45
85
  "Options:",
46
86
  " --platform <value> codex, claude, gemini, or all",
47
- " --home <path> override HOME for installation targets"
87
+ " --home <path> override HOME for installation targets",
88
+ " --project <path> override project path for audit",
89
+ " --ops-file <path> Pencil batch operations file for preflight",
90
+ " --input <path> input .pen file for snapshot-pen",
91
+ " --output <path> output .pen file for write-pen or snapshot-pen",
92
+ " --nodes-file <path> JSON payload from batch_get for write-pen",
93
+ " --variables-file <path> JSON payload from get_variables for write-pen",
94
+ " --verify-open reopen the written .pen with Pencil after writing",
95
+ " --version <value> explicit .pen version when writing from MCP payloads",
96
+ " --mode <value> integrity or completion",
97
+ " --change <id> scope completion audit to one change id"
48
98
  ].join("\n")
49
99
  );
50
100
  }
@@ -52,6 +102,7 @@ function printHelp() {
52
102
  async function runCli(argv) {
53
103
  const [command] = argv;
54
104
  const homeDir = getOption(argv, "--home");
105
+ const positionalArgs = getPositionalArgs(argv.slice(1), ["--home", "--platform", "--project", "--mode", "--change"]);
55
106
 
56
107
  if (!command || command === "help" || command === "--help" || command === "-h") {
57
108
  printHelp();
@@ -92,6 +143,96 @@ async function runCli(argv) {
92
143
  return;
93
144
  }
94
145
 
146
+ if (command === "audit") {
147
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
148
+ const mode = getOption(argv, "--mode");
149
+ const changeId = getOption(argv, "--change");
150
+ const result = auditProject(projectPath, { mode, changeId });
151
+ const report = formatAuditReport(result);
152
+
153
+ if (result.status === "FAIL") {
154
+ throw new Error(report);
155
+ }
156
+
157
+ console.log(report);
158
+ return;
159
+ }
160
+
161
+ if (command === "preflight-pencil") {
162
+ const opsFile = getOption(argv, "--ops-file");
163
+ let operations = "";
164
+
165
+ if (opsFile) {
166
+ operations = readOperations(opsFile);
167
+ } else if (!process.stdin.isTTY) {
168
+ operations = fs.readFileSync(0, "utf8");
169
+ } else {
170
+ throw new Error("`preflight-pencil` requires `--ops-file <path>` or piped stdin input.");
171
+ }
172
+
173
+ const result = preflightPencilBatch(operations);
174
+ const report = formatPencilPreflightReport(result);
175
+
176
+ if (result.status === "FAIL") {
177
+ throw new Error(report);
178
+ }
179
+
180
+ console.log(report);
181
+ return;
182
+ }
183
+
184
+ if (command === "write-pen") {
185
+ const outputPath = getOption(argv, "--output");
186
+ const nodesFile = getOption(argv, "--nodes-file");
187
+ const variablesFile = getOption(argv, "--variables-file");
188
+ const version = getOption(argv, "--version");
189
+ const verifyWithPencil = argv.includes("--verify-open");
190
+
191
+ if (!outputPath || !nodesFile) {
192
+ throw new Error("`write-pen` requires `--output <path>` and `--nodes-file <path>`.");
193
+ }
194
+
195
+ const result = writePenFromPayloadFiles({
196
+ outputPath,
197
+ nodesFile,
198
+ variablesFile,
199
+ version,
200
+ verifyWithPencil
201
+ });
202
+
203
+ console.log(`Wrote .pen file to ${result.outputPath}`);
204
+ console.log(`Top-level nodes: ${result.document.children.length}`);
205
+ if (result.verification) {
206
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
207
+ }
208
+ return;
209
+ }
210
+
211
+ if (command === "snapshot-pen") {
212
+ const inputPath = getOption(argv, "--input");
213
+ const outputPath = getOption(argv, "--output");
214
+ const version = getOption(argv, "--version");
215
+ const verifyWithPencil = argv.includes("--verify-open");
216
+
217
+ if (!inputPath || !outputPath) {
218
+ throw new Error("`snapshot-pen` requires `--input <path>` and `--output <path>`.");
219
+ }
220
+
221
+ const result = snapshotPenFile({
222
+ inputPath,
223
+ outputPath,
224
+ version,
225
+ verifyWithPencil
226
+ });
227
+
228
+ console.log(`Snapshotted ${result.inputPath} to ${result.outputPath}`);
229
+ console.log(`Top-level nodes: ${result.document.children.length}`);
230
+ if (result.verification) {
231
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
232
+ }
233
+ return;
234
+ }
235
+
95
236
  throw new Error(`Unknown command: ${command}`);
96
237
  }
97
238
 
@@ -0,0 +1,342 @@
1
+ const path = require("path");
2
+
3
+ const PASS = "PASS";
4
+ const WARN = "WARN";
5
+ const BLOCK = "BLOCK";
6
+ const SKIP = "SKIP";
7
+
8
+ function normalizeArray(value) {
9
+ return Array.isArray(value) ? value.filter(Boolean) : [];
10
+ }
11
+
12
+ function normalizeLiveScreens(liveScreens) {
13
+ return normalizeArray(liveScreens).map((screen) => ({
14
+ id: screen && screen.id ? String(screen.id) : "",
15
+ name: screen && screen.name ? String(screen.name) : ""
16
+ }));
17
+ }
18
+
19
+ function unique(values) {
20
+ return [...new Set(values)];
21
+ }
22
+
23
+ function getScreenIds(liveScreens) {
24
+ return unique(normalizeLiveScreens(liveScreens).map((screen) => screen.id).filter(Boolean));
25
+ }
26
+
27
+ function getScreenNames(liveScreens) {
28
+ return unique(normalizeLiveScreens(liveScreens).map((screen) => screen.name).filter(Boolean));
29
+ }
30
+
31
+ function normalizeEditorName(activeEditor) {
32
+ return typeof activeEditor === "string" ? activeEditor.trim() : "";
33
+ }
34
+
35
+ function normalizeRegisteredPath(registeredPenPath) {
36
+ return typeof registeredPenPath === "string" ? registeredPenPath.trim() : "";
37
+ }
38
+
39
+ function resolveProjectPath(projectRoot, targetPath) {
40
+ if (!targetPath) {
41
+ return "";
42
+ }
43
+
44
+ if (path.isAbsolute(targetPath)) {
45
+ return path.normalize(targetPath);
46
+ }
47
+
48
+ if (!projectRoot) {
49
+ return "";
50
+ }
51
+
52
+ return path.normalize(path.resolve(projectRoot, targetPath));
53
+ }
54
+
55
+ function isUnnamedEditor(activeEditor) {
56
+ const normalized = normalizeEditorName(activeEditor).toLowerCase();
57
+ return normalized === "" || normalized === "new";
58
+ }
59
+
60
+ function matchesRegisteredPath(activeEditor, registeredPenPath, projectRoot) {
61
+ const editor = normalizeEditorName(activeEditor);
62
+ const registered = normalizeRegisteredPath(registeredPenPath);
63
+
64
+ if (!editor || !registered) {
65
+ return false;
66
+ }
67
+
68
+ const resolvedEditor = resolveProjectPath(projectRoot, editor);
69
+ const resolvedRegistered = resolveProjectPath(projectRoot, registered);
70
+
71
+ if (resolvedEditor && resolvedRegistered) {
72
+ return resolvedEditor === resolvedRegistered;
73
+ }
74
+
75
+ const editorHasPathSeparators = editor.includes("/") || editor.includes("\\");
76
+ if (editorHasPathSeparators) {
77
+ return false;
78
+ }
79
+
80
+ return path.basename(editor) === path.basename(registered);
81
+ }
82
+
83
+ function missingIds(expectedIds, actualIds) {
84
+ const actualSet = new Set(actualIds);
85
+ return unique(expectedIds).filter((id) => !actualSet.has(id));
86
+ }
87
+
88
+ function derivePhase(snapshot) {
89
+ return snapshot.phase || "completion";
90
+ }
91
+
92
+ function evaluateSourceConvergence(snapshot) {
93
+ const notes = [];
94
+ const activeEditor = normalizeEditorName(snapshot.activeEditor);
95
+ const registeredPenPath = normalizeRegisteredPath(snapshot.registeredPenPath);
96
+ const projectRoot = typeof snapshot.projectRoot === "string" ? snapshot.projectRoot.trim() : "";
97
+ const shellVisiblePenExists = Boolean(snapshot.shellVisiblePenExists);
98
+ const documentedReconciliation = Boolean(snapshot.documentedReconciliation);
99
+ const noNewPencilEditsYet = Boolean(snapshot.noNewPencilEditsYet);
100
+
101
+ if (Boolean(snapshot.mcpAvailable) === false) {
102
+ return {
103
+ status: WARN,
104
+ notes: ["Pencil MCP is unavailable, so runtime source convergence could not be fully checked."]
105
+ };
106
+ }
107
+
108
+ if (isUnnamedEditor(activeEditor)) {
109
+ return {
110
+ status: BLOCK,
111
+ notes: ["Active Pencil editor is still an unnamed live document such as `new`."]
112
+ };
113
+ }
114
+
115
+ if (!registeredPenPath) {
116
+ return {
117
+ status: BLOCK,
118
+ notes: ["No registered project-local `.pen` path was provided for runtime comparison."]
119
+ };
120
+ }
121
+
122
+ if (!shellVisiblePenExists) {
123
+ return {
124
+ status: BLOCK,
125
+ notes: ["The registered project-local `.pen` file is not shell-visible on disk."]
126
+ };
127
+ }
128
+
129
+ if (!matchesRegisteredPath(activeEditor, registeredPenPath, projectRoot)) {
130
+ if (documentedReconciliation) {
131
+ return {
132
+ status: WARN,
133
+ notes: [
134
+ "Active Pencil editor does not match the registered project-local `.pen` path, but a documented reconciliation was provided."
135
+ ]
136
+ };
137
+ }
138
+
139
+ return {
140
+ status: BLOCK,
141
+ notes: ["Active Pencil editor does not match the registered project-local `.pen` path."]
142
+ };
143
+ }
144
+
145
+ if (noNewPencilEditsYet) {
146
+ notes.push("No new Pencil edits were recorded yet; source convergence is only provisionally confirmed.");
147
+ return {
148
+ status: WARN,
149
+ notes
150
+ };
151
+ }
152
+
153
+ return {
154
+ status: PASS,
155
+ notes: ["Active Pencil editor matches the registered project-local `.pen` source and the file exists on disk."]
156
+ };
157
+ }
158
+
159
+ function evaluateScreenPresence(snapshot) {
160
+ const phase = derivePhase(snapshot);
161
+ const claimedAnchorIds = normalizeArray(snapshot.claimedAnchorIds);
162
+ const claimedReviewedScreenIds = normalizeArray(snapshot.claimedReviewedScreenIds);
163
+ const reviewTargets = normalizeArray(snapshot.reviewTargets);
164
+ const liveIds = getScreenIds(snapshot.liveScreens);
165
+
166
+ if (Boolean(snapshot.mcpAvailable) === false) {
167
+ return {
168
+ status: WARN,
169
+ notes: ["Pencil MCP is unavailable, so live screen presence could not be fully checked."]
170
+ };
171
+ }
172
+
173
+ if (phase === "first_write" && claimedAnchorIds.length === 0 && claimedReviewedScreenIds.length === 0 && reviewTargets.length === 0) {
174
+ return {
175
+ status: SKIP,
176
+ notes: ["No anchor or review ids were declared yet, so screen-presence checks were skipped at first-write stage."]
177
+ };
178
+ }
179
+
180
+ const missingAnchorIds = missingIds(claimedAnchorIds, liveIds);
181
+ const missingReviewedIds = missingIds(claimedReviewedScreenIds, liveIds);
182
+ const missingReviewTargets = missingIds(reviewTargets, liveIds);
183
+
184
+ const notes = [];
185
+ if (missingAnchorIds.length > 0) {
186
+ notes.push(`Missing claimed anchor ids in the active editor: ${missingAnchorIds.join(", ")}`);
187
+ }
188
+ if (missingReviewedIds.length > 0) {
189
+ notes.push(`Missing claimed reviewed screen ids in the active editor: ${missingReviewedIds.join(", ")}`);
190
+ }
191
+ if (missingReviewTargets.length > 0) {
192
+ notes.push(`Missing screenshot target ids in the active editor: ${missingReviewTargets.join(", ")}`);
193
+ }
194
+
195
+ if (notes.length > 0) {
196
+ return {
197
+ status: BLOCK,
198
+ notes
199
+ };
200
+ }
201
+
202
+ if (claimedAnchorIds.length === 0 && claimedReviewedScreenIds.length === 0 && reviewTargets.length === 0) {
203
+ return {
204
+ status: WARN,
205
+ notes: ["No claimed anchor, reviewed, or screenshot target ids were provided for runtime verification."]
206
+ };
207
+ }
208
+
209
+ return {
210
+ status: PASS,
211
+ notes: ["Claimed anchor surfaces and review targets resolve in the active live editor."]
212
+ };
213
+ }
214
+
215
+ function evaluateReviewExecution(snapshot) {
216
+ const phase = derivePhase(snapshot);
217
+ const claimedReviewedScreenIds = normalizeArray(snapshot.claimedReviewedScreenIds);
218
+ const reviewTargets = normalizeArray(snapshot.reviewTargets);
219
+ const reviewBlockersIgnored = Boolean(snapshot.reviewBlockersIgnored);
220
+ const broadExpansionRequested = Boolean(snapshot.broadExpansionRequested);
221
+
222
+ if (Boolean(snapshot.mcpAvailable) === false) {
223
+ return {
224
+ status: WARN,
225
+ notes: ["Pencil MCP is unavailable, so runtime review execution could not be fully checked."]
226
+ };
227
+ }
228
+
229
+ if (phase === "first_write") {
230
+ return {
231
+ status: SKIP,
232
+ notes: ["Screenshot review is not required at first-write stage."]
233
+ };
234
+ }
235
+
236
+ if (claimedReviewedScreenIds.length === 0 && reviewTargets.length === 0) {
237
+ if (phase === "completion" || broadExpansionRequested) {
238
+ return {
239
+ status: BLOCK,
240
+ notes: ["No reviewed screen ids or screenshot targets were recorded for a stage that requires runtime review evidence."]
241
+ };
242
+ }
243
+
244
+ return {
245
+ status: WARN,
246
+ notes: ["No reviewed screen ids or screenshot targets were recorded yet."]
247
+ };
248
+ }
249
+
250
+ if (reviewBlockersIgnored) {
251
+ return {
252
+ status: BLOCK,
253
+ notes: ["Screenshot review recorded blocker-level issues that were ignored while the surface was still treated as approved."]
254
+ };
255
+ }
256
+
257
+ return {
258
+ status: PASS,
259
+ notes: ["Runtime review evidence exists for the approved surfaces."]
260
+ };
261
+ }
262
+
263
+ function combineStatuses(statuses) {
264
+ if (statuses.includes(BLOCK)) {
265
+ return BLOCK;
266
+ }
267
+ if (statuses.includes(WARN)) {
268
+ return WARN;
269
+ }
270
+ if (statuses.every((status) => status === SKIP)) {
271
+ return WARN;
272
+ }
273
+ return PASS;
274
+ }
275
+
276
+ function evaluateMcpRuntimeGate(snapshot = {}) {
277
+ const sourceConvergence = evaluateSourceConvergence(snapshot);
278
+ const screenPresence = evaluateScreenPresence(snapshot);
279
+ const reviewExecution = evaluateReviewExecution(snapshot);
280
+
281
+ const finalStatus = combineStatuses([
282
+ sourceConvergence.status,
283
+ screenPresence.status === SKIP ? PASS : screenPresence.status,
284
+ reviewExecution.status === SKIP ? PASS : reviewExecution.status
285
+ ]);
286
+
287
+ return {
288
+ phase: derivePhase(snapshot),
289
+ sourceConvergence,
290
+ screenPresence,
291
+ reviewExecution,
292
+ finalStatus,
293
+ activeEditor: normalizeEditorName(snapshot.activeEditor),
294
+ registeredPenPath:
295
+ typeof snapshot.registeredPenPath === "string" ? snapshot.registeredPenPath.trim() : "",
296
+ shellVisiblePenPath:
297
+ typeof snapshot.shellVisiblePenPath === "string" ? snapshot.shellVisiblePenPath.trim() : "",
298
+ shellVisiblePenExists: Boolean(snapshot.shellVisiblePenExists),
299
+ claimedAnchorIds: normalizeArray(snapshot.claimedAnchorIds),
300
+ claimedReviewedScreenIds: normalizeArray(snapshot.claimedReviewedScreenIds),
301
+ reviewTargets: normalizeArray(snapshot.reviewTargets),
302
+ liveScreenIds: getScreenIds(snapshot.liveScreens),
303
+ liveScreenNames: getScreenNames(snapshot.liveScreens)
304
+ };
305
+ }
306
+
307
+ function formatMcpRuntimeGateSection(snapshot, result, options = {}) {
308
+ const timestamp = options.timestamp || new Date().toISOString();
309
+ const notes = [
310
+ ...result.sourceConvergence.notes,
311
+ ...result.screenPresence.notes,
312
+ ...result.reviewExecution.notes
313
+ ];
314
+
315
+ return [
316
+ "## MCP Runtime Gate",
317
+ `- Time: ${timestamp}`,
318
+ `- Phase: ${result.phase}`,
319
+ `- Active editor: ${result.activeEditor || "(unavailable)"}`,
320
+ `- Registered \`.pen\` path: ${result.registeredPenPath || "(missing)"}`,
321
+ `- Shell-visible \`.pen\` path: ${result.shellVisiblePenPath || "(missing)"}`,
322
+ `- Shell-visible \`.pen\` exists: ${result.shellVisiblePenExists ? "yes" : "no"}`,
323
+ `- Claimed anchor ids: ${result.claimedAnchorIds.length > 0 ? result.claimedAnchorIds.join(", ") : "(none)"}`,
324
+ `- Reviewed screen ids: ${result.claimedReviewedScreenIds.length > 0 ? result.claimedReviewedScreenIds.join(", ") : "(none)"}`,
325
+ `- Screenshot target ids: ${result.reviewTargets.length > 0 ? result.reviewTargets.join(", ") : "(none)"}`,
326
+ `- Live screen ids: ${result.liveScreenIds.length > 0 ? result.liveScreenIds.join(", ") : "(none)"}`,
327
+ `- Source convergence: ${result.sourceConvergence.status}`,
328
+ `- Screen presence: ${result.screenPresence.status}`,
329
+ `- Review execution: ${result.reviewExecution.status}`,
330
+ `- Final runtime gate status: ${result.finalStatus}`,
331
+ `- Notes: ${notes.length > 0 ? notes.join(" | ") : "none"}`
332
+ ].join("\n");
333
+ }
334
+
335
+ module.exports = {
336
+ PASS,
337
+ WARN,
338
+ BLOCK,
339
+ SKIP,
340
+ evaluateMcpRuntimeGate,
341
+ formatMcpRuntimeGateSection
342
+ };