@xenonbyte/da-vinci-workflow 0.1.20 → 0.1.22

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 +29 -0
  2. package/README.md +40 -54
  3. package/README.zh-CN.md +40 -54
  4. package/commands/claude/dv/build.md +6 -0
  5. package/commands/claude/dv/continue.md +1 -0
  6. package/commands/codex/prompts/dv-build.md +4 -0
  7. package/commands/codex/prompts/dv-continue.md +1 -0
  8. package/commands/gemini/dv/build.toml +4 -0
  9. package/commands/gemini/dv/continue.toml +1 -0
  10. package/docs/constraint-files.md +109 -0
  11. package/docs/dv-command-reference.md +24 -0
  12. package/docs/visual-adapters.md +24 -80
  13. package/docs/visual-assist-presets/desktop-app.md +20 -68
  14. package/docs/visual-assist-presets/mobile-app.md +20 -68
  15. package/docs/visual-assist-presets/tablet-app.md +20 -68
  16. package/docs/visual-assist-presets/web-app.md +20 -68
  17. package/docs/workflow-examples.md +29 -13
  18. package/docs/workflow-overview.md +2 -0
  19. package/docs/zh-CN/constraint-files.md +111 -0
  20. package/docs/zh-CN/dv-command-reference.md +24 -0
  21. package/docs/zh-CN/visual-adapters.md +24 -80
  22. package/docs/zh-CN/visual-assist-presets/desktop-app.md +20 -68
  23. package/docs/zh-CN/visual-assist-presets/mobile-app.md +20 -68
  24. package/docs/zh-CN/visual-assist-presets/tablet-app.md +20 -68
  25. package/docs/zh-CN/visual-assist-presets/web-app.md +20 -68
  26. package/docs/zh-CN/workflow-examples.md +29 -13
  27. package/docs/zh-CN/workflow-overview.md +2 -0
  28. package/examples/greenfield-spec-markupflow/DA-VINCI.md +13 -13
  29. package/examples/greenfield-spec-markupflow/README.md +7 -0
  30. package/examples/greenfield-spec-markupflow/pencil-design.md +5 -0
  31. package/lib/cli.js +194 -2
  32. package/lib/icon-aliases.js +165 -0
  33. package/lib/icon-search.js +370 -0
  34. package/lib/icon-sync.js +361 -0
  35. package/lib/pencil-session.js +6 -0
  36. package/package.json +5 -2
  37. package/references/artifact-templates.md +24 -0
  38. package/references/icon-aliases.example.json +12 -0
  39. package/scripts/fixtures/mock-pencil.js +49 -0
  40. package/scripts/test-icon-aliases.js +87 -0
  41. package/scripts/test-icon-search.js +72 -0
  42. package/scripts/test-icon-sync.js +178 -0
  43. package/scripts/test-mode-consistency.js +50 -0
  44. package/scripts/test-pen-persistence.js +7 -3
  45. package/scripts/test-pencil-session.js +40 -0
  46. package/scripts/test-persistence-flows.js +31 -1
@@ -0,0 +1,178 @@
1
+ const assert = require("assert/strict");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ MATERIAL_METADATA_URL,
7
+ LUCIDE_TREE_URL,
8
+ FEATHER_TREE_URL,
9
+ PHOSPHOR_TREE_URL,
10
+ getDefaultCatalogPath,
11
+ resolveCatalogPath,
12
+ loadIconCatalog,
13
+ syncIconCatalog,
14
+ formatIconSyncReport,
15
+ parseGoogleMaterialMetadata,
16
+ parseGitHubTreeIcons,
17
+ dedupeIconRecords,
18
+ summarizeSourceResults
19
+ } = require("../lib/icon-sync");
20
+
21
+ async function runTest(name, fn) {
22
+ try {
23
+ await fn();
24
+ console.log(`PASS ${name}`);
25
+ } catch (error) {
26
+ console.error(`FAIL ${name}`);
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ function createMockFetch({ failLucide = false } = {}) {
32
+ return async (url) => {
33
+ if (url === MATERIAL_METADATA_URL) {
34
+ return `)]}'\n{"icons":[{"name":"settings","unsupported_families":[]}]}`;
35
+ }
36
+ if (url === LUCIDE_TREE_URL) {
37
+ if (failLucide) {
38
+ throw new Error("mock lucide outage");
39
+ }
40
+ return JSON.stringify({
41
+ tree: [{ path: "icons/settings.json" }]
42
+ });
43
+ }
44
+ if (url === FEATHER_TREE_URL) {
45
+ return JSON.stringify({
46
+ tree: [{ path: "icons/lock.svg" }]
47
+ });
48
+ }
49
+ if (url === PHOSPHOR_TREE_URL) {
50
+ return JSON.stringify({
51
+ tree: [{ path: "assets/regular/vault.svg" }]
52
+ });
53
+ }
54
+ throw new Error(`Unexpected URL: ${url}`);
55
+ };
56
+ }
57
+
58
+ (async () => {
59
+ await runTest("google metadata parser extracts material variants", () => {
60
+ const raw = `)]}'\n{"icons":[{"name":"settings","unsupported_families":[]},{"name":"lock_open","unsupported_families":["Material Icons Sharp"]}]}`;
61
+ const records = parseGoogleMaterialMetadata(raw);
62
+ const keys = new Set(records.map((record) => `${record.family}/${record.name}`));
63
+ assert.ok(keys.has("Material Symbols Rounded/settings"));
64
+ assert.ok(keys.has("Material Symbols Outlined/settings"));
65
+ assert.ok(keys.has("Material Symbols Sharp/settings"));
66
+ assert.ok(keys.has("Material Symbols Rounded/lock_open"));
67
+ assert.equal(keys.has("Material Symbols Sharp/lock_open"), false);
68
+ });
69
+
70
+ await runTest("github tree parser extracts icon names from paths", () => {
71
+ const raw = JSON.stringify({
72
+ tree: [
73
+ { path: "icons/settings.json" },
74
+ { path: "icons/lock-open.json" },
75
+ { path: "icons/nested/skip.json" },
76
+ { path: "readme.md" }
77
+ ]
78
+ });
79
+ const records = parseGitHubTreeIcons(raw, {
80
+ family: "lucide",
81
+ prefix: "icons/",
82
+ suffix: ".json"
83
+ });
84
+ const names = records.map((record) => record.name);
85
+ assert.deepEqual(names, ["settings", "lock-open"]);
86
+ });
87
+
88
+ await runTest("dedupe keeps first unique family/name pair", () => {
89
+ const deduped = dedupeIconRecords([
90
+ { family: "lucide", name: "settings" },
91
+ { family: "lucide", name: "settings" },
92
+ { family: "feather", name: "settings" }
93
+ ]);
94
+ assert.equal(deduped.length, 2);
95
+ });
96
+
97
+ await runTest("catalog path resolves to home .da-vinci directory by default", () => {
98
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-home-"));
99
+ const resolved = getDefaultCatalogPath(tempHome);
100
+ assert.match(resolved, /\.da-vinci[\/\\]icon-catalog\.json$/);
101
+ });
102
+
103
+ await runTest("load icon catalog reads valid schema", () => {
104
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-catalog-"));
105
+ const catalogPath = path.join(tempDir, "icon-catalog.json");
106
+ fs.writeFileSync(
107
+ catalogPath,
108
+ JSON.stringify(
109
+ {
110
+ schema: 1,
111
+ generatedAt: "2026-03-29T00:00:00.000Z",
112
+ iconCount: 1,
113
+ icons: [{ family: "lucide", name: "settings", semantic: "settings", tags: [] }]
114
+ },
115
+ null,
116
+ 2
117
+ )
118
+ );
119
+
120
+ const loaded = loadIconCatalog({
121
+ catalogPath
122
+ });
123
+ assert.ok(loaded.catalog);
124
+ assert.equal(loaded.catalog.icons.length, 1);
125
+ assert.equal(resolveCatalogPath({ catalogPath }), path.resolve(catalogPath));
126
+ });
127
+
128
+ await runTest("source summary marks degraded when at least one source errors", () => {
129
+ const summary = summarizeSourceResults({
130
+ material: { status: "ok" },
131
+ lucide: { status: "error" },
132
+ feather: { status: "ok" }
133
+ });
134
+ assert.deepEqual(summary, {
135
+ total: 3,
136
+ okCount: 2,
137
+ errorCount: 1,
138
+ degraded: true
139
+ });
140
+ });
141
+
142
+ await runTest("icon sync keeps flow non-blocking by default on partial source failure", async () => {
143
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-sync-"));
144
+ const outputPath = path.join(tempDir, "catalog.json");
145
+ const result = await syncIconCatalog({
146
+ outputPath,
147
+ fetchText: createMockFetch({ failLucide: true })
148
+ });
149
+
150
+ assert.equal(result.catalog.syncStatus, "degraded");
151
+ assert.equal(result.catalog.sourceResults.lucide.status, "error");
152
+ assert.ok(result.catalog.iconCount > 0);
153
+ assert.equal(fs.existsSync(outputPath), true);
154
+ assert.match(formatIconSyncReport(result), /Status: DEGRADED/i);
155
+ });
156
+
157
+ await runTest("icon sync strict mode fails on partial source failure and does not write partial catalog", async () => {
158
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-sync-strict-"));
159
+ const outputPath = path.join(tempDir, "catalog.json");
160
+
161
+ await assert.rejects(
162
+ () =>
163
+ syncIconCatalog({
164
+ outputPath,
165
+ strict: true,
166
+ fetchText: createMockFetch({ failLucide: true })
167
+ }),
168
+ /strict mode failed/i
169
+ );
170
+
171
+ assert.equal(fs.existsSync(outputPath), false);
172
+ });
173
+
174
+ console.log("All icon-sync tests passed.");
175
+ })().catch((error) => {
176
+ console.error(error.stack || error.message);
177
+ process.exit(1);
178
+ });
@@ -286,4 +286,54 @@ runTest("design-supervisor review stays distinct from preferred adapters and is
286
286
  }
287
287
  });
288
288
 
289
+ runTest("build prompts require completion audit and do not treat compile success as workflow completion", () => {
290
+ const buildPrompts = [
291
+ "commands/claude/dv/build.md",
292
+ "commands/codex/prompts/dv-build.md",
293
+ "commands/gemini/dv/build.toml"
294
+ ];
295
+
296
+ for (const file of buildPrompts) {
297
+ const content = read(file);
298
+ assert.match(content, /BUILD SUCCESSFUL/, `${file} should explicitly treat build success as compile-only evidence`);
299
+ assert.match(
300
+ content,
301
+ /da-vinci audit --mode completion --change <change-id> <project-path>/,
302
+ `${file} should require completion audit before terminal completion claims`
303
+ );
304
+ assert.match(
305
+ content,
306
+ /do not report `design complete` or `workflow complete`/i,
307
+ `${file} should prevent terminal completion claims while in-scope tasks remain`
308
+ );
309
+ }
310
+ });
311
+
312
+ runTest("continue prompts keep build routing blocked while design gates are unresolved", () => {
313
+ const continuePrompts = [
314
+ "commands/claude/dv/continue.md",
315
+ "commands/codex/prompts/dv-continue.md",
316
+ "commands/gemini/dv/continue.toml"
317
+ ];
318
+
319
+ for (const file of continuePrompts) {
320
+ const content = read(file);
321
+ assert.match(
322
+ content,
323
+ /missing shell-visible project-local `.pen`/,
324
+ `${file} should block build routing when project-local .pen persistence is unresolved`
325
+ );
326
+ assert.match(
327
+ content,
328
+ /active\/unclosed Pencil session/,
329
+ `${file} should block build routing when Pencil session is still active`
330
+ );
331
+ assert.match(
332
+ content,
333
+ /design-supervisor review still BLOCK\/unaccepted/,
334
+ `${file} should block build routing when required design-supervisor review has not cleared`
335
+ );
336
+ }
337
+ });
338
+
289
339
  console.log("All mode consistency tests passed.");
@@ -17,6 +17,7 @@ const {
17
17
  } = require("../lib/pencil-lock");
18
18
 
19
19
  const fixturePath = path.join(__dirname, "fixtures", "complex-sample.pen");
20
+ const mockPencilPath = path.join(__dirname, "fixtures", "mock-pencil.js");
20
21
 
21
22
  function runTest(name, fn) {
22
23
  try {
@@ -65,7 +66,8 @@ runTest("writePenFromPayloadFiles writes and reopens a complex sample", () => {
65
66
  nodesFile,
66
67
  variablesFile,
67
68
  version: fixture.version,
68
- verifyWithPencil: true
69
+ verifyWithPencil: true,
70
+ pencilBin: mockPencilPath
69
71
  });
70
72
 
71
73
  const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
@@ -82,7 +84,8 @@ runTest("snapshotPenFile round-trips a complex sample", () => {
82
84
  const result = snapshotPenFile({
83
85
  inputPath: fixturePath,
84
86
  outputPath,
85
- verifyWithPencil: true
87
+ verifyWithPencil: true,
88
+ pencilBin: mockPencilPath
86
89
  });
87
90
 
88
91
  const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
@@ -97,7 +100,8 @@ runTest("ensurePenFile seeds a missing project-local .pen and state", () => {
97
100
 
98
101
  const result = ensurePenFile({
99
102
  outputPath,
100
- verifyWithPencil: true
103
+ verifyWithPencil: true,
104
+ pencilBin: mockPencilPath
101
105
  });
102
106
 
103
107
  const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
@@ -149,4 +149,44 @@ runTest("pencil-session end fails when live payload is stale", () => {
149
149
  });
150
150
  });
151
151
 
152
+ runTest("pencil-session end requires live payload unless force is used", () => {
153
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
154
+ const tempDir = createTempDir();
155
+ const projectRoot = path.join(tempDir, "project");
156
+ const homeDir = path.join(tempDir, "home");
157
+ const penPath = path.join(projectRoot, ".da-vinci", "designs", "cipher.pen");
158
+ const { nodesFile, variablesFile } = writePayloadFiles(tempDir, fixture);
159
+
160
+ beginPencilSession({
161
+ projectPath: projectRoot,
162
+ penPath,
163
+ homeDir
164
+ });
165
+ persistPencilSession({
166
+ projectPath: projectRoot,
167
+ penPath,
168
+ nodesFile,
169
+ variablesFile,
170
+ version: fixture.version,
171
+ homeDir
172
+ });
173
+
174
+ assert.throws(
175
+ () =>
176
+ endPencilSession({
177
+ projectPath: projectRoot,
178
+ penPath,
179
+ homeDir
180
+ }),
181
+ /without a live MCP snapshot/i
182
+ );
183
+
184
+ endPencilSession({
185
+ projectPath: projectRoot,
186
+ penPath,
187
+ homeDir,
188
+ force: true
189
+ });
190
+ });
191
+
152
192
  console.log("All Pencil session tests passed.");
@@ -241,9 +241,39 @@ runTest("parallel mixed projects are serialized by the global Pencil lock", () =
241
241
  runCli(harness, ["pencil-session", "begin", "--project", existing.root, "--pen", existing.penPath]),
242
242
  /lock is already held/i
243
243
  );
244
+ expectOk(
245
+ "parallel A persist",
246
+ runCli(harness, [
247
+ "pencil-session",
248
+ "persist",
249
+ "--project",
250
+ freshA.root,
251
+ "--pen",
252
+ freshA.penPath,
253
+ "--nodes-file",
254
+ freshA.nodesFile,
255
+ "--variables-file",
256
+ freshA.variablesFile,
257
+ "--version",
258
+ fixture.version
259
+ ])
260
+ );
244
261
  expectOk(
245
262
  "parallel A end",
246
- runCli(harness, ["pencil-session", "end", "--project", freshA.root, "--pen", freshA.penPath])
263
+ runCli(harness, [
264
+ "pencil-session",
265
+ "end",
266
+ "--project",
267
+ freshA.root,
268
+ "--pen",
269
+ freshA.penPath,
270
+ "--nodes-file",
271
+ freshA.nodesFile,
272
+ "--variables-file",
273
+ freshA.variablesFile,
274
+ "--version",
275
+ fixture.version
276
+ ])
247
277
  );
248
278
 
249
279
  expectOk(