@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
@@ -46,6 +46,13 @@ Visual adapter used for this forward test:
46
46
  - runtime declaration: explicit before first anchor pass
47
47
  - fallback: `native-da-vinci` if no local adapter were available
48
48
 
49
+ Icon library workflow used for this forward test:
50
+
51
+ - sync command: `da-vinci icon-sync`
52
+ - alias template: `cp references/icon-aliases.example.json ~/.da-vinci/icon-aliases.json`
53
+ - lookup command example: `da-vinci icon-search --query "settings publish workflow" --family material --top 6`
54
+ - gate policy: icon lookup remains advisory by default; use `--strict` only for explicit CI hard gates
55
+
49
56
  Important note:
50
57
 
51
58
  - the `.pen` source was authored in a live Pencil session and the example records `.da-vinci/designs/project-baseline.pen` as the intended project-local persisted path
@@ -41,6 +41,11 @@
41
41
  - keep the structural emphasis on three core capabilities on page one
42
42
  - keep the import, annotate, publish pipeline readable on page two
43
43
 
44
+ ## Icon Notes
45
+ - functional icons are expected to use `icon_font` instead of placeholder geometry
46
+ - preferred family for this pass: `Material Symbols Rounded`
47
+ - when icon intent is ambiguous, resolve candidates via `da-vinci icon-search` before writing `batch_design`
48
+
44
49
  ## Validation Notes
45
50
  - both screens were reviewed in Pencil during authoring
46
51
  - no obvious clipping, overlap, or alignment failures were observed in the design pass
package/lib/cli.js CHANGED
@@ -29,6 +29,19 @@ const {
29
29
  endPencilSession,
30
30
  getPencilSessionStatus
31
31
  } = require("./pencil-session");
32
+ const {
33
+ searchIconLibrary,
34
+ formatIconSearchReport
35
+ } = require("./icon-search");
36
+ const {
37
+ syncIconCatalog,
38
+ loadIconCatalog,
39
+ formatIconSyncReport
40
+ } = require("./icon-sync");
41
+ const {
42
+ loadIconAliases,
43
+ expandQueryWithAliases
44
+ } = require("./icon-aliases");
32
45
 
33
46
  function getOption(args, name) {
34
47
  const direct = args.find((arg) => arg.startsWith(`${name}=`));
@@ -90,6 +103,8 @@ function printHelp() {
90
103
  " da-vinci status",
91
104
  " da-vinci validate-assets",
92
105
  " da-vinci audit [project-path]",
106
+ " da-vinci icon-sync [--output <path>] [--timeout-ms <value>] [--strict]",
107
+ " da-vinci icon-search --query <text> [--family <value>] [--top <value>] [--catalog <path>] [--aliases <path>] [--json]",
93
108
  " da-vinci preflight-pencil --ops-file <path>",
94
109
  " da-vinci ensure-pen --output <path>",
95
110
  " da-vinci write-pen --output <path> --nodes-file <path> [--variables-file <path>]",
@@ -100,7 +115,7 @@ function printHelp() {
100
115
  " da-vinci pencil-lock status",
101
116
  " da-vinci pencil-session begin --project <path> --pen <path>",
102
117
  " da-vinci pencil-session persist --project <path> --pen <path> --nodes-file <path> [--variables-file <path>]",
103
- " da-vinci pencil-session end --project <path> --pen <path> [--nodes-file <path>]",
118
+ " da-vinci pencil-session end --project <path> --pen <path> --nodes-file <path> [--variables-file <path>]",
104
119
  " da-vinci pencil-session status --project <path>",
105
120
  " da-vinci --version",
106
121
  "",
@@ -108,6 +123,14 @@ function printHelp() {
108
123
  " --platform <value> codex, claude, gemini, or all",
109
124
  " --home <path> override HOME for installation targets",
110
125
  " --project <path> override project path for audit",
126
+ " --catalog <path> icon catalog path for icon-search/icon-sync (default: ~/.da-vinci/icon-catalog.json)",
127
+ " --aliases <path> icon alias mapping file for icon-search (default: ~/.da-vinci/icon-aliases.json)",
128
+ " --query <text> icon-search query text",
129
+ " --family <value> icon-search family filter: all, material, rounded, outlined, sharp, lucide, feather, phosphor",
130
+ " --top <value> icon-search result count (1-50, default 8)",
131
+ " --timeout-ms <value> network timeout for icon-sync requests",
132
+ " --strict fail icon-sync when any upstream source request fails",
133
+ " --json print structured JSON for icon-search",
111
134
  " --pen <path> registered .pen path for sync checks",
112
135
  " --ops-file <path> Pencil batch operations file for preflight",
113
136
  " --input <path> input .pen file for snapshot-pen",
@@ -128,7 +151,28 @@ function printHelp() {
128
151
  async function runCli(argv) {
129
152
  const [command] = argv;
130
153
  const homeDir = getOption(argv, "--home");
131
- const positionalArgs = getPositionalArgs(argv.slice(1), ["--home", "--platform", "--project", "--mode", "--change"]);
154
+ const positionalArgs = getPositionalArgs(argv.slice(1), [
155
+ "--home",
156
+ "--platform",
157
+ "--project",
158
+ "--mode",
159
+ "--change",
160
+ "--query",
161
+ "--family",
162
+ "--top",
163
+ "--catalog",
164
+ "--aliases",
165
+ "--timeout-ms",
166
+ "--ops-file",
167
+ "--input",
168
+ "--output",
169
+ "--pen",
170
+ "--nodes-file",
171
+ "--variables-file",
172
+ "--version",
173
+ "--owner",
174
+ "--wait-ms"
175
+ ]);
132
176
 
133
177
  if (!command || command === "help" || command === "--help" || command === "-h") {
134
178
  printHelp();
@@ -184,6 +228,149 @@ async function runCli(argv) {
184
228
  return;
185
229
  }
186
230
 
231
+ if (command === "icon-sync") {
232
+ const outputPath = getOption(argv, "--output") || getOption(argv, "--catalog");
233
+ const timeoutMs = getOption(argv, "--timeout-ms");
234
+ const strict = argv.includes("--strict");
235
+
236
+ const result = await syncIconCatalog({
237
+ outputPath,
238
+ timeoutMs,
239
+ strict,
240
+ homeDir
241
+ });
242
+
243
+ console.log(formatIconSyncReport(result));
244
+ return;
245
+ }
246
+
247
+ if (command === "icon-search") {
248
+ const family = getOption(argv, "--family") || "all";
249
+ const topRaw = getOption(argv, "--top");
250
+ const queryOption = getOption(argv, "--query");
251
+ const catalogPath = getOption(argv, "--catalog");
252
+ const aliasesPath = getOption(argv, "--aliases");
253
+ const iconPositional = getPositionalArgs(argv.slice(1), [
254
+ "--home",
255
+ "--query",
256
+ "--family",
257
+ "--top",
258
+ "--catalog",
259
+ "--aliases"
260
+ ]);
261
+ const query = queryOption || iconPositional.join(" ").trim();
262
+
263
+ if (!query) {
264
+ throw new Error("`icon-search` requires `--query <text>` or positional query text.");
265
+ }
266
+
267
+ let top;
268
+ if (topRaw !== undefined) {
269
+ top = Number.parseInt(topRaw, 10);
270
+ if (!Number.isFinite(top) || top < 1 || top > 50) {
271
+ throw new Error("`icon-search --top` must be an integer between 1 and 50.");
272
+ }
273
+ }
274
+
275
+ let loadedCatalog = null;
276
+ let loadedCatalogPath = null;
277
+ let catalogLoadError = null;
278
+ let loadedAliases = null;
279
+ let loadedAliasesPath = null;
280
+ let aliasesLoadError = null;
281
+ let aliasExpansion = {
282
+ extraTokens: [],
283
+ matchedAliases: []
284
+ };
285
+
286
+ try {
287
+ const loaded = loadIconCatalog({
288
+ catalogPath,
289
+ homeDir
290
+ });
291
+ loadedCatalog = loaded.catalog;
292
+ loadedCatalogPath = loaded.catalogPath;
293
+ } catch (error) {
294
+ catalogLoadError = error.message || String(error);
295
+ }
296
+
297
+ try {
298
+ const loaded = loadIconAliases({
299
+ aliasPath: aliasesPath,
300
+ homeDir
301
+ });
302
+ loadedAliases = loaded;
303
+ loadedAliasesPath = loaded.aliasPath;
304
+ aliasExpansion = expandQueryWithAliases(query, loaded.aliases);
305
+ } catch (error) {
306
+ aliasesLoadError = error.message || String(error);
307
+ }
308
+
309
+ const result = searchIconLibrary(query, {
310
+ family,
311
+ top,
312
+ catalog: loadedCatalog ? loadedCatalog.icons : [],
313
+ extraQueryTokens: aliasExpansion.extraTokens
314
+ });
315
+ const jsonOutput = argv.includes("--json");
316
+
317
+ const resultWithMeta = {
318
+ ...result,
319
+ catalog: {
320
+ path: loadedCatalogPath || "(unresolved)",
321
+ loaded: Boolean(loadedCatalog),
322
+ iconCount: loadedCatalog ? loadedCatalog.iconCount : 0,
323
+ generatedAt: loadedCatalog ? loadedCatalog.generatedAt : null,
324
+ error: catalogLoadError
325
+ },
326
+ aliases: {
327
+ path: loadedAliasesPath || "(unresolved)",
328
+ loaded: loadedAliases ? Boolean(loadedAliases.loaded) : false,
329
+ available: Boolean(loadedAliases),
330
+ source: loadedAliases ? loadedAliases.source : null,
331
+ matched: aliasExpansion.matchedAliases.length,
332
+ extraTokens: aliasExpansion.extraTokens,
333
+ error: aliasesLoadError
334
+ }
335
+ };
336
+
337
+ if (jsonOutput) {
338
+ console.log(JSON.stringify(resultWithMeta, null, 2));
339
+ return;
340
+ }
341
+
342
+ if (resultWithMeta.catalog.loaded) {
343
+ console.log(
344
+ `Icon catalog: ${resultWithMeta.catalog.path} (${resultWithMeta.catalog.iconCount} icons, ${resultWithMeta.catalog.generatedAt})`
345
+ );
346
+ } else if (resultWithMeta.catalog.error) {
347
+ console.log(
348
+ `Icon catalog: ${resultWithMeta.catalog.path} (load failed: ${resultWithMeta.catalog.error}; using built-in fallback index)`
349
+ );
350
+ } else {
351
+ console.log(
352
+ `Icon catalog: ${resultWithMeta.catalog.path} (not found; using built-in fallback index; run \`da-vinci icon-sync\`)`
353
+ );
354
+ }
355
+
356
+ if (resultWithMeta.aliases.available) {
357
+ console.log(
358
+ `Icon aliases: ${resultWithMeta.aliases.path} (${resultWithMeta.aliases.source}, matched ${resultWithMeta.aliases.matched})`
359
+ );
360
+ } else if (resultWithMeta.aliases.error) {
361
+ console.log(
362
+ `Icon aliases: ${resultWithMeta.aliases.path} (load failed: ${resultWithMeta.aliases.error})`
363
+ );
364
+ } else {
365
+ console.log(
366
+ `Icon aliases: ${resultWithMeta.aliases.path} (not found; using built-in defaults only)`
367
+ );
368
+ }
369
+
370
+ console.log(formatIconSearchReport(result));
371
+ return;
372
+ }
373
+
187
374
  if (command === "preflight-pencil") {
188
375
  const opsFile = getOption(argv, "--ops-file");
189
376
  let operations = "";
@@ -456,6 +643,11 @@ async function runCli(argv) {
456
643
  if (!penPath) {
457
644
  throw new Error("`pencil-session end` requires `--pen <path>`.");
458
645
  }
646
+ if (!nodesFile && !force) {
647
+ throw new Error(
648
+ "`pencil-session end` requires `--nodes-file <path>` (and `--variables-file <path>` when available). Use `--force` only for emergency lock release."
649
+ );
650
+ }
459
651
 
460
652
  const result = endPencilSession({
461
653
  projectPath,
@@ -0,0 +1,165 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+
5
+ const DEFAULT_ALIASES = {
6
+ "保险箱": ["vault", "safe box", "archive", "inventory_2"],
7
+ "金库": ["vault", "safe box", "lock", "inventory_2"],
8
+ "解密": ["unlock", "lock_open", "key", "verified_user"],
9
+ "加密": ["lock", "shield", "key", "fingerprint"],
10
+ "重试": ["refresh", "retry", "sync", "rotate-cw", "arrow-clockwise"],
11
+ "警告": ["warning", "alert-triangle", "triangle-alert"],
12
+ "错误": ["error", "x-circle", "circle-x"],
13
+ "成功": ["check_circle", "check-circle", "circle-check"],
14
+ "设置": ["settings", "tune", "sliders", "sliders-horizontal"],
15
+ "首页": ["home", "house"],
16
+ "用户": ["person", "user", "account", "profile"],
17
+ "账户": ["person", "user", "account", "profile"],
18
+ "通知": ["notifications", "bell"],
19
+ "上传": ["upload", "cloud_upload", "cloud-upload", "upload-cloud"],
20
+ "下载": ["download", "cloud_download", "cloud-download", "download-cloud"],
21
+ "搜索": ["search", "magnifying-glass"],
22
+ "返回": ["chevron_left", "chevron-left", "caret-left", "arrow-left"],
23
+ "关闭": ["close", "x", "x-circle"],
24
+ "删除": ["delete", "trash", "trash-2"],
25
+ "确认": ["check", "done", "check_circle", "check-circle"],
26
+ "客服": ["headset", "support_agent", "message-circle", "chat"],
27
+ "客服消息": ["headset", "support_agent", "message-circle", "chat"]
28
+ };
29
+
30
+ function normalize(text) {
31
+ return String(text || "")
32
+ .normalize("NFKD")
33
+ .replace(/[\u0300-\u036f]/g, "")
34
+ .toLowerCase()
35
+ .replace(/[_/]+/g, " ")
36
+ .replace(/[^\p{L}\p{N}\s-]+/gu, " ")
37
+ .replace(/\s+/g, " ")
38
+ .trim();
39
+ }
40
+
41
+ function tokenize(text) {
42
+ const normalized = normalize(text);
43
+ if (!normalized) {
44
+ return [];
45
+ }
46
+ return normalized
47
+ .split(/[\s-]+/)
48
+ .map((token) => token.trim())
49
+ .filter(Boolean);
50
+ }
51
+
52
+ function unique(values) {
53
+ return Array.from(new Set((values || []).filter(Boolean)));
54
+ }
55
+
56
+ function getDefaultAliasPath(homeDir) {
57
+ const root = homeDir ? path.resolve(homeDir) : os.homedir();
58
+ return path.join(root, ".da-vinci", "icon-aliases.json");
59
+ }
60
+
61
+ function resolveAliasPath(options = {}) {
62
+ if (options.aliasPath) {
63
+ return path.resolve(options.aliasPath);
64
+ }
65
+ return path.resolve(getDefaultAliasPath(options.homeDir));
66
+ }
67
+
68
+ function normalizeAliasMap(rawMap) {
69
+ const normalized = {};
70
+ for (const [key, values] of Object.entries(rawMap || {})) {
71
+ const normalizedKey = normalize(key);
72
+ if (!normalizedKey) {
73
+ continue;
74
+ }
75
+ const normalizedValues = unique(
76
+ (Array.isArray(values) ? values : [values]).flatMap((value) =>
77
+ String(value || "")
78
+ .split(",")
79
+ .map((entry) => entry.trim())
80
+ )
81
+ );
82
+ if (normalizedValues.length === 0) {
83
+ continue;
84
+ }
85
+ normalized[normalizedKey] = normalizedValues;
86
+ }
87
+ return normalized;
88
+ }
89
+
90
+ function loadIconAliases(options = {}) {
91
+ const aliasPath = resolveAliasPath(options);
92
+ const mergedBase = normalizeAliasMap(DEFAULT_ALIASES);
93
+
94
+ if (!fs.existsSync(aliasPath)) {
95
+ return {
96
+ aliasPath,
97
+ aliases: mergedBase,
98
+ loaded: false,
99
+ source: "default"
100
+ };
101
+ }
102
+
103
+ const text = fs.readFileSync(aliasPath, "utf8");
104
+ const parsed = JSON.parse(text);
105
+ const userAliases = normalizeAliasMap(parsed.aliases || parsed);
106
+
107
+ return {
108
+ aliasPath,
109
+ aliases: {
110
+ ...mergedBase,
111
+ ...userAliases
112
+ },
113
+ loaded: true,
114
+ source: "file"
115
+ };
116
+ }
117
+
118
+ function expandQueryWithAliases(query, aliases) {
119
+ const normalizedQuery = normalize(query);
120
+ const queryTokens = tokenize(normalizedQuery);
121
+ const matched = new Map();
122
+
123
+ if (!normalizedQuery) {
124
+ return {
125
+ extraTokens: [],
126
+ matchedAliases: []
127
+ };
128
+ }
129
+
130
+ for (const [aliasKey, aliasValues] of Object.entries(aliases || {})) {
131
+ if (!aliasKey || !Array.isArray(aliasValues) || aliasValues.length === 0) {
132
+ continue;
133
+ }
134
+
135
+ const tokenHit = queryTokens.includes(aliasKey);
136
+ const phraseHit = normalizedQuery === aliasKey || normalizedQuery.includes(aliasKey);
137
+ if (!tokenHit && !phraseHit) {
138
+ continue;
139
+ }
140
+ matched.set(aliasKey, aliasValues);
141
+ }
142
+
143
+ const extraTokens = unique(
144
+ Array.from(matched.values()).flatMap((aliasValues) =>
145
+ aliasValues.flatMap((value) => tokenize(value))
146
+ )
147
+ );
148
+
149
+ return {
150
+ extraTokens,
151
+ matchedAliases: Array.from(matched.entries()).map(([key, values]) => ({
152
+ key,
153
+ values
154
+ }))
155
+ };
156
+ }
157
+
158
+ module.exports = {
159
+ DEFAULT_ALIASES,
160
+ getDefaultAliasPath,
161
+ resolveAliasPath,
162
+ loadIconAliases,
163
+ expandQueryWithAliases,
164
+ normalizeAliasMap
165
+ };