@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,242 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { STATUS } = require("./workflow-contract");
4
+ const { pathExists } = require("./utils");
5
+ const { generatePlanningSidecars } = require("./sidecars");
6
+ const { digestObject } = require("./planning-parsers");
7
+
8
+ function keyByText(items) {
9
+ const map = new Map();
10
+ for (const item of items || []) {
11
+ const text = String(item && item.text ? item.text : item || "").trim();
12
+ if (!text) {
13
+ continue;
14
+ }
15
+ map.set(text, item);
16
+ }
17
+ return map;
18
+ }
19
+
20
+ function readJsonFileIfExists(filePath) {
21
+ if (!filePath || !pathExists(filePath)) {
22
+ return null;
23
+ }
24
+ try {
25
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
26
+ } catch (_error) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function diffTextCollection(baseItems, headItems) {
32
+ const baseMap = keyByText(baseItems);
33
+ const headMap = keyByText(headItems);
34
+ const added = [];
35
+ const removed = [];
36
+ const modified = [];
37
+
38
+ for (const text of headMap.keys()) {
39
+ if (!baseMap.has(text)) {
40
+ added.push(text);
41
+ }
42
+ }
43
+ for (const text of baseMap.keys()) {
44
+ if (!headMap.has(text)) {
45
+ removed.push(text);
46
+ }
47
+ }
48
+
49
+ for (const text of headMap.keys()) {
50
+ if (!baseMap.has(text)) {
51
+ continue;
52
+ }
53
+ const baseRecord = baseMap.get(text) || {};
54
+ const headRecord = headMap.get(text) || {};
55
+ if (String(baseRecord.specPath || "") !== String(headRecord.specPath || "")) {
56
+ modified.push({
57
+ text,
58
+ from: String(baseRecord.specPath || ""),
59
+ to: String(headRecord.specPath || "")
60
+ });
61
+ }
62
+ }
63
+
64
+ return {
65
+ added: added.sort((a, b) => a.localeCompare(b)),
66
+ removed: removed.sort((a, b) => a.localeCompare(b)),
67
+ modified
68
+ };
69
+ }
70
+
71
+ function hasAnyDiff(diff) {
72
+ return (
73
+ (diff.added && diff.added.length > 0) ||
74
+ (diff.removed && diff.removed.length > 0) ||
75
+ (diff.modified && diff.modified.length > 0)
76
+ );
77
+ }
78
+
79
+ function chooseBaseSidecars(projectRoot, changeId, fromDir) {
80
+ const sidecarDir = fromDir || path.join(projectRoot, ".da-vinci", "changes", changeId, "sidecars");
81
+ return {
82
+ spec: readJsonFileIfExists(path.join(sidecarDir, "spec.index.json")),
83
+ tasks: readJsonFileIfExists(path.join(sidecarDir, "tasks.index.json")),
84
+ pageMap: readJsonFileIfExists(path.join(sidecarDir, "page-map.index.json")),
85
+ bindings: readJsonFileIfExists(path.join(sidecarDir, "bindings.index.json")),
86
+ sidecarDir
87
+ };
88
+ }
89
+
90
+ function diffSpec(projectPathInput, options = {}) {
91
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
92
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
93
+ const sidecarResult = generatePlanningSidecars(projectRoot, {
94
+ changeId: requestedChangeId,
95
+ write: false
96
+ });
97
+ if (sidecarResult.failures.length > 0 || !sidecarResult.changeId) {
98
+ return {
99
+ status: STATUS.BLOCK,
100
+ failures: sidecarResult.failures,
101
+ warnings: sidecarResult.warnings,
102
+ notes: sidecarResult.notes,
103
+ projectRoot,
104
+ changeId: sidecarResult.changeId || null,
105
+ diff: null
106
+ };
107
+ }
108
+
109
+ const changeId = sidecarResult.changeId;
110
+ const baseSidecars = chooseBaseSidecars(projectRoot, changeId, options.fromDir);
111
+ const headSidecars = sidecarResult.sidecars;
112
+
113
+ if (!baseSidecars.spec) {
114
+ return {
115
+ status: STATUS.WARN,
116
+ failures: [],
117
+ warnings: [
118
+ `No baseline sidecars found in ${baseSidecars.sidecarDir}. Run \`da-vinci generate-sidecars\` before diff-spec.`
119
+ ],
120
+ notes: [
121
+ "diff-spec currently stays under the `diff-spec` surface and includes broader planning summaries for page-map/bindings/tasks."
122
+ ],
123
+ projectRoot,
124
+ changeId,
125
+ diff: {
126
+ spec: null,
127
+ tasks: null,
128
+ pageMap: null,
129
+ bindings: null
130
+ }
131
+ };
132
+ }
133
+
134
+ const baseSpecCollections = baseSidecars.spec.collections || {};
135
+ const headSpecCollections = headSidecars["spec.index.json"].collections || {};
136
+ const baseSpecDigest = digestObject(baseSidecars.spec);
137
+ const headSpecDigest = digestObject(headSidecars["spec.index.json"]);
138
+ const specDiff = {
139
+ behavior: diffTextCollection(baseSpecCollections.behavior || [], headSpecCollections.behavior || []),
140
+ states: diffTextCollection(baseSpecCollections.states || [], headSpecCollections.states || []),
141
+ inputs: diffTextCollection(baseSpecCollections.inputs || [], headSpecCollections.inputs || []),
142
+ outputs: diffTextCollection(baseSpecCollections.outputs || [], headSpecCollections.outputs || []),
143
+ acceptance: diffTextCollection(baseSpecCollections.acceptance || [], headSpecCollections.acceptance || [])
144
+ };
145
+
146
+ const tasksDiff = {
147
+ taskGroups: diffTextCollection(
148
+ (baseSidecars.tasks && baseSidecars.tasks.taskGroups
149
+ ? baseSidecars.tasks.taskGroups.map((item) => ({ text: `${item.id}: ${item.title}` }))
150
+ : []),
151
+ headSidecars["tasks.index.json"].taskGroups.map((item) => ({ text: `${item.id}: ${item.title}` }))
152
+ )
153
+ };
154
+
155
+ const pageMapDiff = {
156
+ pages: diffTextCollection(
157
+ (baseSidecars.pageMap && baseSidecars.pageMap.pages
158
+ ? baseSidecars.pageMap.pages.map((page) => ({ text: page }))
159
+ : []),
160
+ headSidecars["page-map.index.json"].pages.map((page) => ({ text: page }))
161
+ )
162
+ };
163
+
164
+ const bindingsDiff = {
165
+ mappings: diffTextCollection(
166
+ (baseSidecars.bindings && baseSidecars.bindings.mappings
167
+ ? baseSidecars.bindings.mappings.map((item) => ({
168
+ text: `${item.implementation} -> ${item.designPage}`
169
+ }))
170
+ : []),
171
+ headSidecars["bindings.index.json"].mappings.map((item) => ({
172
+ text: `${item.implementation} -> ${item.designPage}`
173
+ }))
174
+ )
175
+ };
176
+
177
+ const hasChanges =
178
+ Object.values(specDiff).some((entry) => hasAnyDiff(entry)) ||
179
+ baseSpecDigest !== headSpecDigest ||
180
+ hasAnyDiff(tasksDiff.taskGroups) ||
181
+ hasAnyDiff(pageMapDiff.pages) ||
182
+ hasAnyDiff(bindingsDiff.mappings);
183
+
184
+ return {
185
+ status: hasChanges ? STATUS.WARN : STATUS.PASS,
186
+ failures: [],
187
+ warnings: hasChanges ? ["Planning sidecar differences detected."] : [],
188
+ notes: [
189
+ "diff-spec includes normalized spec deltas and broader planning summaries (tasks/page-map/bindings)."
190
+ ],
191
+ projectRoot,
192
+ changeId,
193
+ diff: {
194
+ spec: specDiff,
195
+ specDigestChanged: baseSpecDigest !== headSpecDigest,
196
+ tasks: tasksDiff,
197
+ pageMap: pageMapDiff,
198
+ bindings: bindingsDiff
199
+ }
200
+ };
201
+ }
202
+
203
+ function formatDiffSpecReport(result) {
204
+ const lines = [
205
+ "Da Vinci diff-spec",
206
+ `Project: ${result.projectRoot}`,
207
+ `Change: ${result.changeId || "(not selected)"}`,
208
+ `Status: ${result.status}`
209
+ ];
210
+ if (result.failures && result.failures.length > 0) {
211
+ lines.push("", "Failures:");
212
+ for (const failure of result.failures) {
213
+ lines.push(`- ${failure}`);
214
+ }
215
+ }
216
+ if (result.warnings && result.warnings.length > 0) {
217
+ lines.push("", "Warnings:");
218
+ for (const warning of result.warnings) {
219
+ lines.push(`- ${warning}`);
220
+ }
221
+ }
222
+ if (result.diff && result.diff.spec) {
223
+ lines.push("", "Spec deltas:");
224
+ for (const [section, entry] of Object.entries(result.diff.spec)) {
225
+ lines.push(
226
+ `- ${section}: +${entry.added.length} / -${entry.removed.length} / ~${entry.modified.length}`
227
+ );
228
+ }
229
+ }
230
+ if (result.notes && result.notes.length > 0) {
231
+ lines.push("", "Notes:");
232
+ for (const note of result.notes) {
233
+ lines.push(`- ${note}`);
234
+ }
235
+ }
236
+ return lines.join("\n");
237
+ }
238
+
239
+ module.exports = {
240
+ diffSpec,
241
+ formatDiffSpecReport
242
+ };
@@ -0,0 +1,136 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { pathExists, writeFileAtomic } = require("./utils");
4
+
5
+ function resolveSignalsDir(projectRoot) {
6
+ return path.join(projectRoot, ".da-vinci", "state", "execution-signals");
7
+ }
8
+
9
+ function parseSignalFileName(fileName) {
10
+ const base = String(fileName || "").replace(/\.json$/i, "");
11
+ const separatorIndex = base.indexOf("__");
12
+ if (separatorIndex === -1) {
13
+ return {
14
+ changeId: "",
15
+ surface: ""
16
+ };
17
+ }
18
+ return {
19
+ changeId: base.slice(0, separatorIndex),
20
+ surface: base.slice(separatorIndex + 2)
21
+ };
22
+ }
23
+
24
+ function sanitizeSurfaceName(surface) {
25
+ return String(surface || "")
26
+ .trim()
27
+ .toLowerCase()
28
+ .replace(/[^a-z0-9._-]+/g, "-")
29
+ .replace(/^-+|-+$/g, "");
30
+ }
31
+
32
+ function buildSignalPath(projectRoot, changeId, surface) {
33
+ const safeSurface = sanitizeSurfaceName(surface);
34
+ const safeChangeId = String(changeId || "global")
35
+ .trim()
36
+ .replace(/[^A-Za-z0-9._-]+/g, "-");
37
+ return path.join(resolveSignalsDir(projectRoot), `${safeChangeId}__${safeSurface}.json`);
38
+ }
39
+
40
+ function writeExecutionSignal(projectRoot, payload) {
41
+ const changeId = payload && payload.changeId ? String(payload.changeId).trim() : "global";
42
+ const signal = {
43
+ version: 1,
44
+ surface: payload.surface,
45
+ status: payload.status,
46
+ advisory: payload.advisory !== false,
47
+ strict: payload.strict === true,
48
+ failures: Array.isArray(payload.failures) ? payload.failures : [],
49
+ warnings: Array.isArray(payload.warnings) ? payload.warnings : [],
50
+ notes: Array.isArray(payload.notes) ? payload.notes : [],
51
+ timestamp: new Date().toISOString(),
52
+ changeId
53
+ };
54
+
55
+ const targetPath = buildSignalPath(projectRoot, changeId, payload.surface);
56
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
57
+ writeFileAtomic(targetPath, `${JSON.stringify(signal, null, 2)}\n`);
58
+ return targetPath;
59
+ }
60
+
61
+ function readExecutionSignals(projectRoot, options = {}) {
62
+ const changeId = options.changeId ? String(options.changeId).trim() : "";
63
+ const signalsDir = resolveSignalsDir(projectRoot);
64
+ if (!pathExists(signalsDir)) {
65
+ return [];
66
+ }
67
+
68
+ const entries = fs
69
+ .readdirSync(signalsDir, { withFileTypes: true })
70
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"));
71
+ const loaded = [];
72
+
73
+ for (const entry of entries) {
74
+ const absolutePath = path.join(signalsDir, entry.name);
75
+ const parsedName = parseSignalFileName(entry.name);
76
+ try {
77
+ const payload = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
78
+ if (changeId && String(payload.changeId || "") !== changeId) {
79
+ continue;
80
+ }
81
+ loaded.push({
82
+ ...payload,
83
+ path: absolutePath
84
+ });
85
+ } catch (error) {
86
+ const discoveredChangeId = String(parsedName.changeId || "").trim();
87
+ if (changeId && discoveredChangeId && discoveredChangeId !== changeId) {
88
+ continue;
89
+ }
90
+ let timestamp = new Date().toISOString();
91
+ try {
92
+ const stat = fs.statSync(absolutePath);
93
+ timestamp = new Date(stat.mtimeMs).toISOString();
94
+ } catch (_statError) {
95
+ // Keep fallback timestamp.
96
+ }
97
+ loaded.push({
98
+ version: 1,
99
+ surface: "signal-file-parse",
100
+ status: "WARN",
101
+ advisory: true,
102
+ strict: false,
103
+ failures: [],
104
+ warnings: [
105
+ `Malformed execution signal ignored: ${entry.name} (${error && error.message ? error.message : "parse error"})`
106
+ ],
107
+ notes: [],
108
+ timestamp,
109
+ changeId: discoveredChangeId || "global",
110
+ path: absolutePath
111
+ });
112
+ }
113
+ }
114
+
115
+ return loaded.sort((left, right) =>
116
+ String(right.timestamp || "").localeCompare(String(left.timestamp || ""))
117
+ );
118
+ }
119
+
120
+ function summarizeSignalsBySurface(signals) {
121
+ const summary = {};
122
+ for (const signal of signals || []) {
123
+ const key = sanitizeSurfaceName(signal.surface || "");
124
+ if (!key || summary[key]) {
125
+ continue;
126
+ }
127
+ summary[key] = signal;
128
+ }
129
+ return summary;
130
+ }
131
+
132
+ module.exports = {
133
+ writeExecutionSignal,
134
+ readExecutionSignals,
135
+ summarizeSignalsBySurface
136
+ };
@@ -0,0 +1,143 @@
1
+ const path = require("path");
2
+ const { STATUS } = require("./workflow-contract");
3
+ const {
4
+ unique,
5
+ resolveImplementationLanding,
6
+ resolveChangeDir,
7
+ parseBindingsArtifact,
8
+ readChangeArtifacts,
9
+ readArtifactTexts
10
+ } = require("./planning-parsers");
11
+
12
+ function buildEnvelope(projectRoot, strict) {
13
+ return {
14
+ status: STATUS.PASS,
15
+ failures: [],
16
+ warnings: [],
17
+ notes: [],
18
+ projectRoot,
19
+ changeId: null,
20
+ strict,
21
+ summary: {
22
+ mappings: 0,
23
+ malformed: 0
24
+ }
25
+ };
26
+ }
27
+
28
+ function finalize(result) {
29
+ result.failures = unique(result.failures);
30
+ result.warnings = unique(result.warnings);
31
+ result.notes = unique(result.notes);
32
+ const hasFindings = result.failures.length > 0 || result.warnings.length > 0;
33
+ if (!hasFindings) {
34
+ result.status = STATUS.PASS;
35
+ return result;
36
+ }
37
+ result.status = result.strict ? STATUS.BLOCK : STATUS.WARN;
38
+ return result;
39
+ }
40
+
41
+ function lintBindings(projectPathInput, options = {}) {
42
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
43
+ const strict = options.strict === true;
44
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
45
+ const result = buildEnvelope(projectRoot, strict);
46
+
47
+ const resolved = resolveChangeDir(projectRoot, requestedChangeId);
48
+ result.failures.push(...resolved.failures);
49
+ result.notes.push(...resolved.notes);
50
+ if (!resolved.changeDir) {
51
+ result.notes.push("lint-bindings defaults to advisory mode; pass `--strict` to block on findings.");
52
+ return finalize(result);
53
+ }
54
+ result.changeId = resolved.changeId;
55
+
56
+ const artifactPaths = readChangeArtifacts(projectRoot, resolved.changeId);
57
+ const artifacts = readArtifactTexts(artifactPaths);
58
+ if (!artifacts.bindings) {
59
+ result.failures.push("Missing `pencil-bindings.md` for lint-bindings.");
60
+ result.notes.push("lint-bindings defaults to advisory mode; pass `--strict` to block on findings.");
61
+ return finalize(result);
62
+ }
63
+
64
+ const parsed = parseBindingsArtifact(artifacts.bindings);
65
+ result.summary.mappings = parsed.mappings.length;
66
+ result.summary.malformed = parsed.malformed.length;
67
+
68
+ if (parsed.mappings.length === 0) {
69
+ result.failures.push("No implementation-to-Pencil mappings were parsed from `pencil-bindings.md`.");
70
+ }
71
+ if (parsed.malformed.length > 0) {
72
+ for (const malformed of parsed.malformed) {
73
+ result.warnings.push(`Malformed binding mapping entry: "${malformed}".`);
74
+ }
75
+ }
76
+
77
+ for (const mapping of parsed.mappings) {
78
+ if (!mapping.implementation || !mapping.designPage) {
79
+ result.warnings.push(`Malformed binding mapping entry: "${mapping.raw}".`);
80
+ continue;
81
+ }
82
+
83
+ const landing = resolveImplementationLanding(projectRoot, mapping.implementation);
84
+ if (!landing) {
85
+ const noteContainsIntentionalGap = parsed.notes.some((note) =>
86
+ /missing|todo|gap|pending|temporary/i.test(note)
87
+ );
88
+ if (noteContainsIntentionalGap) {
89
+ result.warnings.push(
90
+ `Unresolved implementation landing for "${mapping.implementation}" (allowed by explicit notes).`
91
+ );
92
+ } else {
93
+ result.warnings.push(`Unresolved implementation landing for "${mapping.implementation}".`);
94
+ }
95
+ }
96
+
97
+ if (mapping.designSource && !String(mapping.designSource).includes(".pen")) {
98
+ result.warnings.push(
99
+ `Binding source for "${mapping.implementation}" does not look like a .pen path: "${mapping.designSource}".`
100
+ );
101
+ }
102
+ }
103
+
104
+ result.notes.push("lint-bindings defaults to advisory mode; pass `--strict` to block on findings.");
105
+ return finalize(result);
106
+ }
107
+
108
+ function formatLintBindingsReport(result) {
109
+ const lines = [
110
+ "Da Vinci lint-bindings",
111
+ `Project: ${result.projectRoot}`,
112
+ `Change: ${result.changeId || "(not selected)"}`,
113
+ `Strict mode: ${result.strict ? "yes" : "no"}`,
114
+ `Status: ${result.status}`,
115
+ `Mappings: ${result.summary.mappings}`,
116
+ `Malformed entries: ${result.summary.malformed}`
117
+ ];
118
+
119
+ if (result.failures.length > 0) {
120
+ lines.push("", "Failures:");
121
+ for (const failure of result.failures) {
122
+ lines.push(`- ${failure}`);
123
+ }
124
+ }
125
+ if (result.warnings.length > 0) {
126
+ lines.push("", "Warnings:");
127
+ for (const warning of result.warnings) {
128
+ lines.push(`- ${warning}`);
129
+ }
130
+ }
131
+ if (result.notes.length > 0) {
132
+ lines.push("", "Notes:");
133
+ for (const note of result.notes) {
134
+ lines.push(`- ${note}`);
135
+ }
136
+ }
137
+ return lines.join("\n");
138
+ }
139
+
140
+ module.exports = {
141
+ lintBindings,
142
+ formatLintBindingsReport
143
+ };