@xenonbyte/da-vinci-workflow 0.1.26 → 0.2.2

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 (45) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +28 -65
  3. package/README.zh-CN.md +28 -65
  4. package/bin/da-vinci-tui.js +8 -0
  5. package/commands/claude/dv/continue.md +5 -0
  6. package/commands/codex/prompts/dv-continue.md +6 -1
  7. package/commands/gemini/dv/continue.toml +5 -0
  8. package/commands/templates/dv-continue.shared.md +33 -0
  9. package/docs/dv-command-reference.md +35 -0
  10. package/docs/execution-chain-migration.md +46 -0
  11. package/docs/execution-chain-plan.md +125 -0
  12. package/docs/prompt-entrypoints.md +8 -0
  13. package/docs/skill-usage.md +217 -0
  14. package/docs/workflow-examples.md +10 -0
  15. package/docs/workflow-overview.md +26 -0
  16. package/docs/zh-CN/dv-command-reference.md +35 -0
  17. package/docs/zh-CN/execution-chain-migration.md +46 -0
  18. package/docs/zh-CN/prompt-entrypoints.md +8 -0
  19. package/docs/zh-CN/skill-usage.md +217 -0
  20. package/docs/zh-CN/workflow-examples.md +10 -0
  21. package/docs/zh-CN/workflow-overview.md +26 -0
  22. package/lib/artifact-parsers.js +120 -0
  23. package/lib/audit.js +61 -0
  24. package/lib/cli.js +351 -13
  25. package/lib/diff-spec.js +242 -0
  26. package/lib/execution-signals.js +136 -0
  27. package/lib/lint-bindings.js +143 -0
  28. package/lib/lint-spec.js +408 -0
  29. package/lib/lint-tasks.js +176 -0
  30. package/lib/planning-parsers.js +567 -0
  31. package/lib/scaffold.js +193 -0
  32. package/lib/scope-check.js +603 -0
  33. package/lib/sidecars.js +369 -0
  34. package/lib/supervisor-review.js +28 -3
  35. package/lib/utils.js +10 -2
  36. package/lib/verify.js +652 -0
  37. package/lib/workflow-contract.js +107 -0
  38. package/lib/workflow-persisted-state.js +297 -0
  39. package/lib/workflow-state.js +785 -0
  40. package/package.json +13 -3
  41. package/references/artifact-templates.md +26 -0
  42. package/references/checkpoints.md +14 -0
  43. package/references/modes.md +10 -0
  44. package/tui/catalog.js +1190 -0
  45. package/tui/index.js +727 -0
@@ -0,0 +1,567 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const crypto = require("crypto");
4
+ const { getMarkdownSection } = require("./audit-parsers");
5
+ const { parseRuntimeSpecMarkdown } = require("./artifact-parsers");
6
+ const { pathExists, readTextIfExists } = require("./utils");
7
+
8
+ const LIST_ITEM_PATTERN = /^\s*(?:[-*+]|\d+[.)])\s+(.+?)\s*$/;
9
+ const DEFAULT_SCAN_MAX_DEPTH = 32;
10
+ const DEFAULT_SPEC_SCAN_MAX_DEPTH = 64;
11
+
12
+ function toPositiveInteger(value, fallback) {
13
+ const parsed = Number(value);
14
+ if (!Number.isFinite(parsed)) {
15
+ return fallback;
16
+ }
17
+ const normalized = Math.floor(parsed);
18
+ if (normalized <= 0) {
19
+ return fallback;
20
+ }
21
+ return normalized;
22
+ }
23
+
24
+ function normalizeText(value) {
25
+ return String(value || "")
26
+ .toLowerCase()
27
+ .replace(/[`*_~]/g, "")
28
+ .replace(/\s+/g, " ")
29
+ .trim();
30
+ }
31
+
32
+ function parseListItems(sectionText) {
33
+ const normalized = String(sectionText || "").replace(/\r\n?/g, "\n").trim();
34
+ if (!normalized) {
35
+ return [];
36
+ }
37
+
38
+ const lines = normalized.split("\n");
39
+ const items = [];
40
+ for (const line of lines) {
41
+ const match = line.match(LIST_ITEM_PATTERN);
42
+ if (!match) {
43
+ continue;
44
+ }
45
+ const value = String(match[1] || "").trim();
46
+ if (value) {
47
+ items.push(value);
48
+ }
49
+ }
50
+ if (items.length > 0) {
51
+ return items;
52
+ }
53
+
54
+ return normalized
55
+ .split(/\n\s*\n/g)
56
+ .map((item) => item.trim())
57
+ .filter(Boolean);
58
+ }
59
+
60
+ function unique(values) {
61
+ return Array.from(new Set((values || []).filter(Boolean)));
62
+ }
63
+
64
+ function resolveImplementationLanding(projectRoot, implementationToken) {
65
+ const knownExtensions = [".html", ".tsx", ".jsx", ".ts", ".js"];
66
+ const seen = new Set();
67
+ const candidates = [];
68
+ const addCandidate = (relativePath) => {
69
+ const normalized = String(relativePath || "").trim();
70
+ if (!normalized || seen.has(normalized)) {
71
+ return;
72
+ }
73
+ seen.add(normalized);
74
+ candidates.push(normalized);
75
+ };
76
+ const addFileCandidates = (basePath) => {
77
+ addCandidate(basePath);
78
+ for (const extension of knownExtensions) {
79
+ addCandidate(`${basePath}${extension}`);
80
+ }
81
+ };
82
+ const addIndexCandidates = (dirPath) => {
83
+ for (const extension of knownExtensions) {
84
+ addCandidate(path.join(dirPath, `index${extension}`));
85
+ }
86
+ };
87
+ const addPageCandidates = (dirPath) => {
88
+ for (const extension of knownExtensions) {
89
+ addCandidate(path.join(dirPath, `page${extension}`));
90
+ }
91
+ };
92
+ const resolveFileCandidate = (relativePath) => {
93
+ const absolutePath = path.join(projectRoot, relativePath);
94
+ try {
95
+ const stat = fs.statSync(absolutePath);
96
+ return stat.isFile() ? absolutePath : null;
97
+ } catch (_error) {
98
+ return null;
99
+ }
100
+ };
101
+
102
+ const normalized = String(implementationToken || "").trim();
103
+ if (!normalized) {
104
+ return null;
105
+ }
106
+
107
+ if (normalized === "/" || normalized === "index" || normalized === "/index") {
108
+ addFileCandidates("index");
109
+ addFileCandidates(path.join("pages", "index"));
110
+ addIndexCandidates("pages");
111
+ addFileCandidates(path.join("src", "pages", "index"));
112
+ addIndexCandidates(path.join("src", "pages"));
113
+ addFileCandidates(path.join("app", "page"));
114
+ addPageCandidates("app");
115
+ addFileCandidates(path.join("src", "app", "page"));
116
+ addPageCandidates(path.join("src", "app"));
117
+ } else {
118
+ const routePath = normalized.replace(/^\//, "").replace(/\/+$/, "");
119
+ addFileCandidates(routePath);
120
+ addIndexCandidates(routePath);
121
+
122
+ addFileCandidates(path.join("src", routePath));
123
+ addIndexCandidates(path.join("src", routePath));
124
+
125
+ addFileCandidates(path.join("pages", routePath));
126
+ addIndexCandidates(path.join("pages", routePath));
127
+ addFileCandidates(path.join("src", "pages", routePath));
128
+ addIndexCandidates(path.join("src", "pages", routePath));
129
+
130
+ addFileCandidates(path.join("app", routePath, "page"));
131
+ addPageCandidates(path.join("app", routePath));
132
+ addFileCandidates(path.join("src", "app", routePath, "page"));
133
+ addPageCandidates(path.join("src", "app", routePath));
134
+ }
135
+
136
+ for (const candidate of candidates) {
137
+ const resolved = resolveFileCandidate(candidate);
138
+ if (resolved) {
139
+ return resolved;
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+ function listImmediateDirs(targetDir) {
146
+ if (!pathExists(targetDir)) {
147
+ return [];
148
+ }
149
+
150
+ let entries = [];
151
+ try {
152
+ entries = fs.readdirSync(targetDir, { withFileTypes: true });
153
+ } catch (_error) {
154
+ return [];
155
+ }
156
+
157
+ return entries
158
+ .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
159
+ .map((entry) => path.join(targetDir, entry.name));
160
+ }
161
+
162
+ function hasAnyFile(targetDir, options = {}) {
163
+ if (!pathExists(targetDir)) {
164
+ return false;
165
+ }
166
+
167
+ const maxDepth = toPositiveInteger(options.maxDepth, DEFAULT_SCAN_MAX_DEPTH);
168
+ const stack = [{ dir: targetDir, depth: 0 }];
169
+ const visited = new Set();
170
+ while (stack.length > 0) {
171
+ const current = stack.pop();
172
+ if (current.depth > maxDepth) {
173
+ continue;
174
+ }
175
+
176
+ let resolvedCurrent;
177
+ try {
178
+ resolvedCurrent = fs.realpathSync(current.dir);
179
+ } catch (_error) {
180
+ continue;
181
+ }
182
+ if (visited.has(resolvedCurrent)) {
183
+ continue;
184
+ }
185
+ visited.add(resolvedCurrent);
186
+
187
+ let entries = [];
188
+ try {
189
+ entries = fs.readdirSync(current.dir, { withFileTypes: true });
190
+ } catch (_error) {
191
+ continue;
192
+ }
193
+ for (const entry of entries) {
194
+ if (entry.isFile()) {
195
+ return true;
196
+ }
197
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
198
+ stack.push({
199
+ dir: path.join(current.dir, entry.name),
200
+ depth: current.depth + 1
201
+ });
202
+ }
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+
208
+ function pickLatestChange(changeDirs) {
209
+ if (!Array.isArray(changeDirs) || changeDirs.length === 0) {
210
+ return null;
211
+ }
212
+
213
+ const withMtime = changeDirs
214
+ .map((changeDir) => {
215
+ try {
216
+ const stat = fs.statSync(changeDir);
217
+ return {
218
+ changeDir,
219
+ mtimeMs: stat.mtimeMs
220
+ };
221
+ } catch (_error) {
222
+ return null;
223
+ }
224
+ })
225
+ .filter(Boolean)
226
+ .sort((left, right) => right.mtimeMs - left.mtimeMs);
227
+ if (withMtime.length === 0) {
228
+ return null;
229
+ }
230
+ return withMtime[0].changeDir;
231
+ }
232
+
233
+ function resolveChangeDir(projectRoot, requestedChangeId = "") {
234
+ const workflowRoot = path.join(projectRoot, ".da-vinci");
235
+ const changesRoot = path.join(workflowRoot, "changes");
236
+ const response = {
237
+ changeDir: null,
238
+ changeId: null,
239
+ failures: [],
240
+ notes: []
241
+ };
242
+
243
+ if (!pathExists(workflowRoot)) {
244
+ response.failures.push("Missing `.da-vinci/` directory.");
245
+ return response;
246
+ }
247
+
248
+ const changeDirs = listImmediateDirs(changesRoot).filter((changeDir) => hasAnyFile(changeDir));
249
+ if (changeDirs.length === 0) {
250
+ response.failures.push("No non-empty change directory found under `.da-vinci/changes/`.");
251
+ return response;
252
+ }
253
+
254
+ if (requestedChangeId) {
255
+ const requestedDir = path.join(changesRoot, requestedChangeId);
256
+ if (!pathExists(requestedDir) || !hasAnyFile(requestedDir)) {
257
+ response.failures.push(
258
+ `Requested change was not found or is empty: .da-vinci/changes/${requestedChangeId}`
259
+ );
260
+ return response;
261
+ }
262
+ response.changeDir = requestedDir;
263
+ response.changeId = requestedChangeId;
264
+ return response;
265
+ }
266
+
267
+ if (changeDirs.length === 1) {
268
+ response.changeDir = changeDirs[0];
269
+ response.changeId = path.basename(changeDirs[0]);
270
+ return response;
271
+ }
272
+
273
+ const changeIds = changeDirs.map((changeDir) => path.basename(changeDir)).sort();
274
+ response.failures.push("Multiple non-empty change directories found. Re-run with `--change <change-id>`.");
275
+ response.notes.push(`Available change ids: ${changeIds.join(", ")}`);
276
+ const latest = pickLatestChange(changeDirs);
277
+ if (latest) {
278
+ response.notes.push(`Latest inferred change for context only: ${path.basename(latest)}`);
279
+ }
280
+ return response;
281
+ }
282
+
283
+ function detectSpecFiles(specsDir, options = {}) {
284
+ if (!pathExists(specsDir)) {
285
+ return [];
286
+ }
287
+
288
+ const maxDepth = toPositiveInteger(options.maxDepth, DEFAULT_SPEC_SCAN_MAX_DEPTH);
289
+ const discovered = [];
290
+ const stack = [{ dir: specsDir, depth: 0 }];
291
+ const visited = new Set();
292
+ while (stack.length > 0) {
293
+ const current = stack.pop();
294
+ if (current.depth > maxDepth) {
295
+ continue;
296
+ }
297
+
298
+ let resolvedCurrent;
299
+ try {
300
+ resolvedCurrent = fs.realpathSync(current.dir);
301
+ } catch (_error) {
302
+ continue;
303
+ }
304
+ if (visited.has(resolvedCurrent)) {
305
+ continue;
306
+ }
307
+ visited.add(resolvedCurrent);
308
+
309
+ let entries = [];
310
+ try {
311
+ entries = fs.readdirSync(current.dir, { withFileTypes: true });
312
+ } catch (_error) {
313
+ continue;
314
+ }
315
+ for (const entry of entries) {
316
+ const absolutePath = path.join(current.dir, entry.name);
317
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
318
+ stack.push({
319
+ dir: absolutePath,
320
+ depth: current.depth + 1
321
+ });
322
+ continue;
323
+ }
324
+ if (entry.isFile() && entry.name === "spec.md") {
325
+ discovered.push(absolutePath);
326
+ }
327
+ }
328
+ }
329
+ return discovered.sort();
330
+ }
331
+
332
+ function parseProposalArtifact(text) {
333
+ return {
334
+ scopeItems: parseListItems(getMarkdownSection(text, "Scope")),
335
+ nonGoalItems: parseListItems(getMarkdownSection(text, "Non-Goals"))
336
+ };
337
+ }
338
+
339
+ function parsePageMapArtifact(text) {
340
+ const canonicalItems = parseListItems(getMarkdownSection(text, "Canonical Pages"));
341
+ const statesPerPageItems = parseListItems(getMarkdownSection(text, "States Per Page"));
342
+ const pages = canonicalItems
343
+ .map((item) => String(item || "").split("->")[0].replace(/`/g, "").trim())
344
+ .filter(Boolean);
345
+
346
+ return {
347
+ canonicalItems,
348
+ statesPerPageItems,
349
+ pages: unique(pages)
350
+ };
351
+ }
352
+
353
+ function parseTasksArtifact(text) {
354
+ const lines = String(text || "").replace(/\r\n?/g, "\n").split("\n");
355
+ const taskGroups = [];
356
+ const checklistItems = [];
357
+ const checkpointItems = [];
358
+ const sections = [];
359
+ let currentSection = null;
360
+
361
+ for (const line of lines) {
362
+ const groupMatch = line.match(/^\s{0,3}##\s+(\d+(?:\.\d+)*)\.\s+(.+)$/);
363
+ if (groupMatch) {
364
+ const group = {
365
+ id: groupMatch[1],
366
+ title: String(groupMatch[2] || "").trim()
367
+ };
368
+ taskGroups.push(group);
369
+ if (currentSection) {
370
+ sections.push(currentSection);
371
+ }
372
+ currentSection = {
373
+ id: group.id,
374
+ title: group.title,
375
+ checklistItems: []
376
+ };
377
+ continue;
378
+ }
379
+
380
+ const checklistMatch = line.match(/^\s*-\s*\[([ xX])\]\s+(.+)$/);
381
+ if (checklistMatch) {
382
+ const checked = String(checklistMatch[1] || "").toLowerCase() === "x";
383
+ const textValue = String(checklistMatch[2] || "").trim();
384
+ if (!textValue) {
385
+ continue;
386
+ }
387
+ checklistItems.push({
388
+ checked,
389
+ text: textValue
390
+ });
391
+ if (currentSection) {
392
+ currentSection.checklistItems.push({
393
+ checked,
394
+ text: textValue
395
+ });
396
+ }
397
+ if (/checkpoint/i.test(textValue)) {
398
+ checkpointItems.push({
399
+ checked,
400
+ text: textValue
401
+ });
402
+ }
403
+ }
404
+ }
405
+ if (currentSection) {
406
+ sections.push(currentSection);
407
+ }
408
+
409
+ return {
410
+ taskGroups,
411
+ checklistItems,
412
+ checkpointItems,
413
+ sections,
414
+ text: String(text || "")
415
+ };
416
+ }
417
+
418
+ function parseBindingsArtifact(text) {
419
+ const bindingSections = [
420
+ "Bindings",
421
+ "Page To Pencil Page",
422
+ "Bound Pages",
423
+ "Page Mapping"
424
+ ];
425
+
426
+ let sectionText = "";
427
+ for (const heading of bindingSections) {
428
+ sectionText = getMarkdownSection(text, heading);
429
+ if (sectionText) {
430
+ break;
431
+ }
432
+ }
433
+
434
+ const items = parseListItems(sectionText);
435
+ const mappings = [];
436
+ const malformed = [];
437
+
438
+ for (const item of items) {
439
+ const segments = String(item || "")
440
+ .split("->")
441
+ .map((segment) => segment.replace(/`/g, "").trim())
442
+ .filter(Boolean);
443
+
444
+ if (segments.length < 2) {
445
+ malformed.push(item);
446
+ continue;
447
+ }
448
+
449
+ const mapping = {
450
+ implementation: segments[0],
451
+ designPage: segments[segments.length - 1],
452
+ designSource: segments.length >= 3 ? segments[1] : "",
453
+ raw: item
454
+ };
455
+
456
+ const idMatch = mapping.designPage.match(/\(([^)]+)\)\s*$/);
457
+ if (idMatch) {
458
+ mapping.screenId = String(idMatch[1] || "").trim();
459
+ mapping.designPage = mapping.designPage.replace(/\(([^)]+)\)\s*$/, "").trim();
460
+ } else {
461
+ mapping.screenId = "";
462
+ }
463
+
464
+ mappings.push(mapping);
465
+ }
466
+
467
+ const notes = parseListItems(getMarkdownSection(text, "Notes"));
468
+ const sharedBindings = parseListItems(getMarkdownSection(text, "Shared Bindings"));
469
+
470
+ return {
471
+ mappings,
472
+ malformed,
473
+ notes,
474
+ sharedBindings
475
+ };
476
+ }
477
+
478
+ function parseVerificationArtifact(text) {
479
+ return {
480
+ requirementCoverage: parseListItems(getMarkdownSection(text, "Requirement Coverage")),
481
+ designCoverage: parseListItems(getMarkdownSection(text, "Design Coverage")),
482
+ behaviorDrift: parseListItems(getMarkdownSection(text, "Behavior Drift")),
483
+ bindingDrift: parseListItems(getMarkdownSection(text, "Binding Drift")),
484
+ outcome: parseListItems(getMarkdownSection(text, "Outcome"))
485
+ };
486
+ }
487
+
488
+ function parseRuntimeSpecs(changeDir, projectRoot) {
489
+ const specsDir = path.join(changeDir, "specs");
490
+ const specFiles = detectSpecFiles(specsDir);
491
+ const records = [];
492
+
493
+ for (const specFile of specFiles) {
494
+ const text = fs.readFileSync(specFile, "utf8");
495
+ const parsed = parseRuntimeSpecMarkdown(text);
496
+ records.push({
497
+ path: path.relative(projectRoot, specFile) || specFile,
498
+ text,
499
+ parsed
500
+ });
501
+ }
502
+
503
+ return records;
504
+ }
505
+
506
+ function digestObject(value) {
507
+ return crypto
508
+ .createHash("sha256")
509
+ .update(JSON.stringify(value))
510
+ .digest("hex");
511
+ }
512
+
513
+ function digestFile(filePath) {
514
+ if (!pathExists(filePath)) {
515
+ return "";
516
+ }
517
+ const payload = fs.readFileSync(filePath);
518
+ return crypto.createHash("sha256").update(payload).digest("hex");
519
+ }
520
+
521
+ function readChangeArtifacts(projectRoot, changeId) {
522
+ const changeDir = path.join(projectRoot, ".da-vinci", "changes", changeId);
523
+ const pageMapPath = path.join(projectRoot, ".da-vinci", "page-map.md");
524
+ const fallbackPageMapPath = path.join(changeDir, "page-map.md");
525
+ return {
526
+ changeDir,
527
+ proposalPath: path.join(changeDir, "proposal.md"),
528
+ pageMapPath: pathExists(pageMapPath) ? pageMapPath : fallbackPageMapPath,
529
+ tasksPath: path.join(changeDir, "tasks.md"),
530
+ bindingsPath: path.join(changeDir, "pencil-bindings.md"),
531
+ pencilDesignPath: path.join(changeDir, "pencil-design.md"),
532
+ verificationPath: path.join(changeDir, "verification.md")
533
+ };
534
+ }
535
+
536
+ function readArtifactTexts(paths) {
537
+ return {
538
+ proposal: readTextIfExists(paths.proposalPath),
539
+ pageMap: readTextIfExists(paths.pageMapPath),
540
+ tasks: readTextIfExists(paths.tasksPath),
541
+ bindings: readTextIfExists(paths.bindingsPath),
542
+ pencilDesign: readTextIfExists(paths.pencilDesignPath),
543
+ verification: readTextIfExists(paths.verificationPath)
544
+ };
545
+ }
546
+
547
+ module.exports = {
548
+ normalizeText,
549
+ parseListItems,
550
+ unique,
551
+ resolveImplementationLanding,
552
+ listImmediateDirs,
553
+ hasAnyFile,
554
+ pickLatestChange,
555
+ resolveChangeDir,
556
+ detectSpecFiles,
557
+ parseProposalArtifact,
558
+ parsePageMapArtifact,
559
+ parseTasksArtifact,
560
+ parseBindingsArtifact,
561
+ parseVerificationArtifact,
562
+ parseRuntimeSpecs,
563
+ digestObject,
564
+ digestFile,
565
+ readChangeArtifacts,
566
+ readArtifactTexts
567
+ };