@xenonbyte/da-vinci-workflow 0.1.13 → 0.1.15

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 (56) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +23 -1
  3. package/README.zh-CN.md +23 -1
  4. package/SKILL.md +15 -0
  5. package/commands/claude/dv/design.md +2 -0
  6. package/commands/claude/dv/verify.md +2 -0
  7. package/commands/codex/prompts/dv-design.md +2 -0
  8. package/commands/codex/prompts/dv-verify.md +1 -0
  9. package/commands/gemini/dv/design.toml +2 -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 +2 -0
  15. package/docs/prompt-presets/README.md +1 -0
  16. package/docs/prompt-presets/desktop-app.md +4 -0
  17. package/docs/prompt-presets/mobile-app.md +4 -0
  18. package/docs/prompt-presets/tablet-app.md +4 -0
  19. package/docs/prompt-presets/web-app.md +4 -0
  20. package/docs/visual-adapters.md +9 -0
  21. package/docs/visual-assist-presets/README.md +4 -2
  22. package/docs/visual-assist-presets/desktop-app.md +2 -0
  23. package/docs/visual-assist-presets/mobile-app.md +2 -0
  24. package/docs/visual-assist-presets/tablet-app.md +2 -0
  25. package/docs/visual-assist-presets/web-app.md +2 -0
  26. package/docs/workflow-examples.md +9 -4
  27. package/docs/zh-CN/mcp-aware-gate-implementation.md +290 -0
  28. package/docs/zh-CN/mcp-aware-gate-tests.md +244 -0
  29. package/docs/zh-CN/mcp-aware-gate.md +249 -0
  30. package/docs/zh-CN/mode-use-cases.md +3 -0
  31. package/docs/zh-CN/prompt-presets/README.md +1 -0
  32. package/docs/zh-CN/prompt-presets/desktop-app.md +4 -0
  33. package/docs/zh-CN/prompt-presets/mobile-app.md +4 -0
  34. package/docs/zh-CN/prompt-presets/tablet-app.md +4 -0
  35. package/docs/zh-CN/prompt-presets/web-app.md +4 -0
  36. package/docs/zh-CN/visual-adapters.md +9 -0
  37. package/docs/zh-CN/visual-assist-presets/README.md +5 -3
  38. package/docs/zh-CN/visual-assist-presets/desktop-app.md +2 -0
  39. package/docs/zh-CN/visual-assist-presets/mobile-app.md +2 -0
  40. package/docs/zh-CN/visual-assist-presets/tablet-app.md +2 -0
  41. package/docs/zh-CN/visual-assist-presets/web-app.md +2 -0
  42. package/docs/zh-CN/workflow-examples.md +9 -4
  43. package/examples/greenfield-spec-markupflow/DA-VINCI.md +1 -0
  44. package/examples/greenfield-spec-markupflow/README.md +3 -0
  45. package/examples/greenfield-spec-markupflow/design-registry.md +3 -0
  46. package/examples/greenfield-spec-markupflow/pencil-design.md +4 -0
  47. package/lib/audit.js +348 -0
  48. package/lib/cli.js +47 -1
  49. package/lib/mcp-runtime-gate.js +342 -0
  50. package/package.json +3 -2
  51. package/references/artifact-templates.md +35 -3
  52. package/references/checkpoints.md +69 -1
  53. package/references/design-inputs.md +9 -1
  54. package/references/layout-hygiene.md +117 -0
  55. package/references/pencil-design-to-code.md +8 -0
  56. package/scripts/test-mcp-runtime-gate.js +199 -0
package/lib/audit.js ADDED
@@ -0,0 +1,348 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const IMAGE_EXPORT_PATTERN = /\.(png|jpe?g|webp|pdf)$/i;
5
+
6
+ function pathExists(targetPath) {
7
+ return fs.existsSync(targetPath);
8
+ }
9
+
10
+ function readTextIfExists(targetPath) {
11
+ if (!pathExists(targetPath)) {
12
+ return "";
13
+ }
14
+ return fs.readFileSync(targetPath, "utf8");
15
+ }
16
+
17
+ function listFilesRecursive(rootDir) {
18
+ if (!pathExists(rootDir)) {
19
+ return [];
20
+ }
21
+
22
+ return fs.readdirSync(rootDir, { withFileTypes: true }).flatMap((entry) => {
23
+ const fullPath = path.join(rootDir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ return listFilesRecursive(fullPath);
26
+ }
27
+ return [fullPath];
28
+ });
29
+ }
30
+
31
+ function listChildDirs(rootDir) {
32
+ if (!pathExists(rootDir)) {
33
+ return [];
34
+ }
35
+
36
+ return fs
37
+ .readdirSync(rootDir, { withFileTypes: true })
38
+ .filter((entry) => entry.isDirectory())
39
+ .map((entry) => path.join(rootDir, entry.name));
40
+ }
41
+
42
+ function relativeTo(projectRoot, targetPath) {
43
+ return path.relative(projectRoot, targetPath) || ".";
44
+ }
45
+
46
+ function collectRegisteredPenPaths(projectRoot, designRegistryPath) {
47
+ const registryText = readTextIfExists(designRegistryPath);
48
+ const matches = registryText.match(/\.da-vinci\/designs\/[^\s`]+\.pen/g) || [];
49
+ return [...new Set(matches)].map((relativePath) => path.join(projectRoot, relativePath));
50
+ }
51
+
52
+ function getNonEmptyChangeDirs(changesDir) {
53
+ return listChildDirs(changesDir).filter((changeDir) => listFilesRecursive(changeDir).length > 0);
54
+ }
55
+
56
+ function getExpectedChangeArtifacts(changeDir) {
57
+ return [
58
+ path.join(changeDir, "design-brief.md"),
59
+ path.join(changeDir, "design.md"),
60
+ path.join(changeDir, "pencil-design.md"),
61
+ path.join(changeDir, "pencil-bindings.md")
62
+ ];
63
+ }
64
+
65
+ function isAllowedExportPath(projectRoot, targetPath) {
66
+ const relativePath = relativeTo(projectRoot, targetPath);
67
+ return /^\.da-vinci\/changes\/[^/]+\/exports\//.test(relativePath);
68
+ }
69
+
70
+ function resolveScopedChangeDirs(projectRoot, changesDir, options, failures, warnings, notes) {
71
+ const requestedChangeId = options.changeId;
72
+ const requestedChangeDir = requestedChangeId ? path.join(changesDir, requestedChangeId) : null;
73
+
74
+ if (requestedChangeDir) {
75
+ if (!pathExists(requestedChangeDir)) {
76
+ failures.push(
77
+ `Requested change scope does not exist: ${relativeTo(projectRoot, requestedChangeDir)}`
78
+ );
79
+ return [];
80
+ }
81
+
82
+ return [requestedChangeDir];
83
+ }
84
+
85
+ const nonEmptyChangeDirs = getNonEmptyChangeDirs(changesDir);
86
+ if (nonEmptyChangeDirs.length === 1) {
87
+ notes.push(
88
+ `Inferred completion change scope: ${relativeTo(projectRoot, nonEmptyChangeDirs[0])}`
89
+ );
90
+ return nonEmptyChangeDirs;
91
+ }
92
+
93
+ if (nonEmptyChangeDirs.length === 0) {
94
+ failures.push(
95
+ "Completion audit requires a non-empty `.da-vinci/changes/<change-id>/` directory."
96
+ );
97
+ return [];
98
+ }
99
+
100
+ warnings.push(
101
+ "Multiple non-empty change directories exist. Pass `--change <change-id>` to run a strict completion audit."
102
+ );
103
+ failures.push("Completion audit scope is ambiguous without `--change <change-id>`.");
104
+ return [];
105
+ }
106
+
107
+ function addMissingArtifacts(projectRoot, artifactPaths, targetList) {
108
+ for (const artifactPath of artifactPaths) {
109
+ if (!pathExists(artifactPath)) {
110
+ targetList.push(`Missing required artifact: ${relativeTo(projectRoot, artifactPath)}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ function auditProject(projectPathInput, options = {}) {
116
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
117
+ const mode = options.mode || "integrity";
118
+ const daVinciDir = path.join(projectRoot, ".da-vinci");
119
+ const designsDir = path.join(daVinciDir, "designs");
120
+ const changesDir = path.join(daVinciDir, "changes");
121
+ const designRegistryPath = path.join(daVinciDir, "design-registry.md");
122
+
123
+ const failures = [];
124
+ const warnings = [];
125
+ const notes = [];
126
+
127
+ if (!["integrity", "completion"].includes(mode)) {
128
+ failures.push(`Unsupported audit mode: ${mode}`);
129
+ return {
130
+ projectRoot,
131
+ mode,
132
+ changeId: options.changeId || null,
133
+ status: "FAIL",
134
+ failures,
135
+ warnings,
136
+ notes
137
+ };
138
+ }
139
+
140
+ if (!pathExists(projectRoot)) {
141
+ failures.push(`Project path does not exist: ${projectRoot}`);
142
+ return {
143
+ projectRoot,
144
+ mode,
145
+ changeId: options.changeId || null,
146
+ status: "FAIL",
147
+ failures,
148
+ warnings,
149
+ notes
150
+ };
151
+ }
152
+
153
+ if (!pathExists(daVinciDir)) {
154
+ failures.push("Missing `.da-vinci/` directory.");
155
+ return {
156
+ projectRoot,
157
+ mode,
158
+ changeId: options.changeId || null,
159
+ status: "FAIL",
160
+ failures,
161
+ warnings,
162
+ notes
163
+ };
164
+ }
165
+
166
+ const integrityRequiredArtifacts = [path.join(projectRoot, "DA-VINCI.md"), designsDir];
167
+ addMissingArtifacts(projectRoot, integrityRequiredArtifacts, failures);
168
+
169
+ const completionRequiredArtifacts = [
170
+ path.join(projectRoot, "DA-VINCI.md"),
171
+ path.join(daVinciDir, "project-inventory.md"),
172
+ path.join(daVinciDir, "page-map.md"),
173
+ designRegistryPath,
174
+ designsDir
175
+ ];
176
+
177
+ if (mode === "completion") {
178
+ addMissingArtifacts(projectRoot, completionRequiredArtifacts, failures);
179
+ } else {
180
+ addMissingArtifacts(projectRoot, completionRequiredArtifacts.slice(1, 4), warnings);
181
+ }
182
+
183
+ const daVinciFiles = listFilesRecursive(daVinciDir);
184
+ const misplacedExports = daVinciFiles.filter(
185
+ (filePath) => IMAGE_EXPORT_PATTERN.test(filePath) && !isAllowedExportPath(projectRoot, filePath)
186
+ );
187
+
188
+ if (misplacedExports.length > 0) {
189
+ failures.push(
190
+ [
191
+ "Review exports were written outside `.da-vinci/changes/<change-id>/exports/`:",
192
+ ...misplacedExports.map((filePath) => ` - ${relativeTo(projectRoot, filePath)}`)
193
+ ].join("\n")
194
+ );
195
+ }
196
+
197
+ const designFiles = listFilesRecursive(designsDir);
198
+ const penFiles = designFiles.filter((filePath) => filePath.endsWith(".pen"));
199
+ const pollutedDesignFiles = designFiles.filter((filePath) => !filePath.endsWith(".pen"));
200
+
201
+ if (pollutedDesignFiles.length > 0) {
202
+ failures.push(
203
+ [
204
+ "`.da-vinci/designs/` contains non-`.pen` files:",
205
+ ...pollutedDesignFiles.map((filePath) => ` - ${relativeTo(projectRoot, filePath)}`)
206
+ ].join("\n")
207
+ );
208
+ }
209
+
210
+ if (penFiles.length === 0) {
211
+ const message = "No shell-visible project-local `.pen` file exists under `.da-vinci/designs/`.";
212
+ if (mode === "completion") {
213
+ failures.push(message);
214
+ } else {
215
+ warnings.push(message);
216
+ }
217
+ } else {
218
+ notes.push(
219
+ `Detected project-local .pen source(s): ${penFiles
220
+ .map((filePath) => relativeTo(projectRoot, filePath))
221
+ .join(", ")}`
222
+ );
223
+ }
224
+
225
+ const registeredPenPaths = collectRegisteredPenPaths(projectRoot, designRegistryPath);
226
+ for (const registeredPenPath of registeredPenPaths) {
227
+ if (!pathExists(registeredPenPath)) {
228
+ failures.push(
229
+ `Registered design source is missing on disk: ${relativeTo(projectRoot, registeredPenPath)}`
230
+ );
231
+ }
232
+ }
233
+
234
+ const changeDirs = listChildDirs(changesDir);
235
+ if (changeDirs.length === 0) {
236
+ warnings.push("No `.da-vinci/changes/<change-id>/` directory was found.");
237
+ }
238
+
239
+ const scopedChangeDirs =
240
+ mode === "completion"
241
+ ? resolveScopedChangeDirs(projectRoot, changesDir, options, failures, warnings, notes)
242
+ : changeDirs;
243
+
244
+ for (const changeDir of changeDirs) {
245
+ const changeFiles = listFilesRecursive(changeDir);
246
+ const changeRel = relativeTo(projectRoot, changeDir);
247
+
248
+ if (changeFiles.length === 0) {
249
+ if (mode === "completion" && scopedChangeDirs.includes(changeDir)) {
250
+ failures.push(`Completion scope is empty: ${changeRel}`);
251
+ }
252
+ warnings.push(`Empty change scaffold: ${changeRel}`);
253
+ continue;
254
+ }
255
+
256
+ const exportsDir = path.join(changeDir, "exports");
257
+ const exportsFiles = listFilesRecursive(exportsDir);
258
+ if (exportsFiles.length > 0) {
259
+ notes.push(`Detected review exports under ${relativeTo(projectRoot, exportsDir)}.`);
260
+ }
261
+
262
+ const expectedChangeArtifacts = getExpectedChangeArtifacts(changeDir);
263
+ const missingArtifacts = expectedChangeArtifacts.filter((artifactPath) => !pathExists(artifactPath));
264
+
265
+ if (mode === "completion" && scopedChangeDirs.includes(changeDir) && missingArtifacts.length > 0) {
266
+ failures.push(
267
+ [
268
+ `Completion scope is missing required change artifacts in ${changeRel}:`,
269
+ ...missingArtifacts.map((artifactPath) => ` - ${relativeTo(projectRoot, artifactPath)}`)
270
+ ].join("\n")
271
+ );
272
+ } else if (missingArtifacts.length > 0 && missingArtifacts.length < expectedChangeArtifacts.length) {
273
+ warnings.push(
274
+ [
275
+ `Partial change artifact set in ${changeRel}:`,
276
+ ...missingArtifacts.map((artifactPath) => ` - missing ${relativeTo(projectRoot, artifactPath)}`)
277
+ ].join("\n")
278
+ );
279
+ }
280
+
281
+ if (exportsFiles.length > 0 && !pathExists(path.join(changeDir, "pencil-design.md"))) {
282
+ failures.push(
283
+ `Exported screenshots exist in ${changeRel} but no pencil-design.md records the design pass.`
284
+ );
285
+ }
286
+
287
+ const specDirs = listChildDirs(path.join(changeDir, "specs"));
288
+ for (const specDir of specDirs) {
289
+ const specFile = path.join(specDir, "spec.md");
290
+ if (!pathExists(specFile)) {
291
+ warnings.push(`Spec scaffold exists without spec.md: ${relativeTo(projectRoot, specDir)}`);
292
+ }
293
+ }
294
+ }
295
+
296
+ const status = failures.length > 0 ? "FAIL" : warnings.length > 0 ? "WARN" : "PASS";
297
+
298
+ return {
299
+ projectRoot,
300
+ mode,
301
+ changeId: options.changeId || null,
302
+ status,
303
+ failures,
304
+ warnings,
305
+ notes
306
+ };
307
+ }
308
+
309
+ function formatAuditReport(result) {
310
+ const lines = [
311
+ "Da Vinci audit",
312
+ `Project: ${result.projectRoot}`,
313
+ `Mode: ${result.mode}`,
314
+ `Status: ${result.status}`
315
+ ];
316
+
317
+ if (result.changeId) {
318
+ lines.splice(3, 0, `Change: ${result.changeId}`);
319
+ }
320
+
321
+ if (result.failures.length > 0) {
322
+ lines.push("", "Failures:");
323
+ for (const failure of result.failures) {
324
+ lines.push(`- ${failure}`);
325
+ }
326
+ }
327
+
328
+ if (result.warnings.length > 0) {
329
+ lines.push("", "Warnings:");
330
+ for (const warning of result.warnings) {
331
+ lines.push(`- ${warning}`);
332
+ }
333
+ }
334
+
335
+ if (result.notes.length > 0) {
336
+ lines.push("", "Notes:");
337
+ for (const note of result.notes) {
338
+ lines.push(`- ${note}`);
339
+ }
340
+ }
341
+
342
+ return lines.join("\n");
343
+ }
344
+
345
+ module.exports = {
346
+ auditProject,
347
+ formatAuditReport
348
+ };
package/lib/cli.js CHANGED
@@ -5,6 +5,7 @@ const {
5
5
  getStatus,
6
6
  validateAssets
7
7
  } = require("./install");
8
+ const { auditProject, formatAuditReport } = require("./audit");
8
9
 
9
10
  function getOption(args, name) {
10
11
  const direct = args.find((arg) => arg.startsWith(`${name}=`));
@@ -20,6 +21,31 @@ function getOption(args, name) {
20
21
  return undefined;
21
22
  }
22
23
 
24
+ function getPositionalArgs(args, optionsWithValues = []) {
25
+ const positional = [];
26
+
27
+ for (let index = 0; index < args.length; index += 1) {
28
+ const arg = args[index];
29
+
30
+ if (optionsWithValues.includes(arg)) {
31
+ index += 1;
32
+ continue;
33
+ }
34
+
35
+ if (optionsWithValues.some((name) => arg.startsWith(`${name}=`))) {
36
+ continue;
37
+ }
38
+
39
+ if (arg.startsWith("-")) {
40
+ continue;
41
+ }
42
+
43
+ positional.push(arg);
44
+ }
45
+
46
+ return positional;
47
+ }
48
+
23
49
  function formatStatus(status) {
24
50
  return [
25
51
  `Da Vinci v${status.version}`,
@@ -40,11 +66,15 @@ function printHelp() {
40
66
  " da-vinci uninstall --platform codex,claude,gemini",
41
67
  " da-vinci status",
42
68
  " da-vinci validate-assets",
69
+ " da-vinci audit [project-path]",
43
70
  " da-vinci --version",
44
71
  "",
45
72
  "Options:",
46
73
  " --platform <value> codex, claude, gemini, or all",
47
- " --home <path> override HOME for installation targets"
74
+ " --home <path> override HOME for installation targets",
75
+ " --project <path> override project path for audit",
76
+ " --mode <value> integrity or completion",
77
+ " --change <id> scope completion audit to one change id"
48
78
  ].join("\n")
49
79
  );
50
80
  }
@@ -52,6 +82,7 @@ function printHelp() {
52
82
  async function runCli(argv) {
53
83
  const [command] = argv;
54
84
  const homeDir = getOption(argv, "--home");
85
+ const positionalArgs = getPositionalArgs(argv.slice(1), ["--home", "--platform", "--project", "--mode", "--change"]);
55
86
 
56
87
  if (!command || command === "help" || command === "--help" || command === "-h") {
57
88
  printHelp();
@@ -92,6 +123,21 @@ async function runCli(argv) {
92
123
  return;
93
124
  }
94
125
 
126
+ if (command === "audit") {
127
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
128
+ const mode = getOption(argv, "--mode");
129
+ const changeId = getOption(argv, "--change");
130
+ const result = auditProject(projectPath, { mode, changeId });
131
+ const report = formatAuditReport(result);
132
+
133
+ if (result.status === "FAIL") {
134
+ throw new Error(report);
135
+ }
136
+
137
+ console.log(report);
138
+ return;
139
+ }
140
+
95
141
  throw new Error(`Unknown command: ${command}`);
96
142
  }
97
143