@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.
- package/CHANGELOG.md +29 -0
- package/README.md +40 -54
- package/README.zh-CN.md +40 -54
- package/commands/claude/dv/build.md +6 -0
- package/commands/claude/dv/continue.md +1 -0
- package/commands/codex/prompts/dv-build.md +4 -0
- package/commands/codex/prompts/dv-continue.md +1 -0
- package/commands/gemini/dv/build.toml +4 -0
- package/commands/gemini/dv/continue.toml +1 -0
- package/docs/constraint-files.md +109 -0
- package/docs/dv-command-reference.md +24 -0
- package/docs/visual-adapters.md +24 -80
- package/docs/visual-assist-presets/desktop-app.md +20 -68
- package/docs/visual-assist-presets/mobile-app.md +20 -68
- package/docs/visual-assist-presets/tablet-app.md +20 -68
- package/docs/visual-assist-presets/web-app.md +20 -68
- package/docs/workflow-examples.md +29 -13
- package/docs/workflow-overview.md +2 -0
- package/docs/zh-CN/constraint-files.md +111 -0
- package/docs/zh-CN/dv-command-reference.md +24 -0
- package/docs/zh-CN/visual-adapters.md +24 -80
- package/docs/zh-CN/visual-assist-presets/desktop-app.md +20 -68
- package/docs/zh-CN/visual-assist-presets/mobile-app.md +20 -68
- package/docs/zh-CN/visual-assist-presets/tablet-app.md +20 -68
- package/docs/zh-CN/visual-assist-presets/web-app.md +20 -68
- package/docs/zh-CN/workflow-examples.md +29 -13
- package/docs/zh-CN/workflow-overview.md +2 -0
- package/examples/greenfield-spec-markupflow/DA-VINCI.md +13 -13
- package/examples/greenfield-spec-markupflow/README.md +7 -0
- package/examples/greenfield-spec-markupflow/pencil-design.md +5 -0
- package/lib/cli.js +194 -2
- package/lib/icon-aliases.js +165 -0
- package/lib/icon-search.js +370 -0
- package/lib/icon-sync.js +361 -0
- package/lib/pencil-session.js +6 -0
- package/package.json +5 -2
- package/references/artifact-templates.md +24 -0
- package/references/icon-aliases.example.json +12 -0
- package/scripts/fixtures/mock-pencil.js +49 -0
- package/scripts/test-icon-aliases.js +87 -0
- package/scripts/test-icon-search.js +72 -0
- package/scripts/test-icon-sync.js +178 -0
- package/scripts/test-mode-consistency.js +50 -0
- package/scripts/test-pen-persistence.js +7 -3
- package/scripts/test-pencil-session.js +40 -0
- 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, [
|
|
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(
|