@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.
- package/CHANGELOG.md +31 -0
- package/README.md +28 -65
- package/README.zh-CN.md +28 -65
- package/bin/da-vinci-tui.js +8 -0
- package/commands/claude/dv/continue.md +5 -0
- package/commands/codex/prompts/dv-continue.md +6 -1
- package/commands/gemini/dv/continue.toml +5 -0
- package/commands/templates/dv-continue.shared.md +33 -0
- package/docs/dv-command-reference.md +35 -0
- package/docs/execution-chain-migration.md +46 -0
- package/docs/execution-chain-plan.md +125 -0
- package/docs/prompt-entrypoints.md +8 -0
- package/docs/skill-usage.md +217 -0
- package/docs/workflow-examples.md +10 -0
- package/docs/workflow-overview.md +26 -0
- package/docs/zh-CN/dv-command-reference.md +35 -0
- package/docs/zh-CN/execution-chain-migration.md +46 -0
- package/docs/zh-CN/prompt-entrypoints.md +8 -0
- package/docs/zh-CN/skill-usage.md +217 -0
- package/docs/zh-CN/workflow-examples.md +10 -0
- package/docs/zh-CN/workflow-overview.md +26 -0
- package/lib/artifact-parsers.js +120 -0
- package/lib/audit.js +61 -0
- package/lib/cli.js +351 -13
- package/lib/diff-spec.js +242 -0
- package/lib/execution-signals.js +136 -0
- package/lib/lint-bindings.js +143 -0
- package/lib/lint-spec.js +408 -0
- package/lib/lint-tasks.js +176 -0
- package/lib/planning-parsers.js +567 -0
- package/lib/scaffold.js +193 -0
- package/lib/scope-check.js +603 -0
- package/lib/sidecars.js +369 -0
- package/lib/supervisor-review.js +28 -3
- package/lib/utils.js +10 -2
- package/lib/verify.js +652 -0
- package/lib/workflow-contract.js +107 -0
- package/lib/workflow-persisted-state.js +297 -0
- package/lib/workflow-state.js +785 -0
- package/package.json +13 -3
- package/references/artifact-templates.md +26 -0
- package/references/checkpoints.md +14 -0
- package/references/modes.md +10 -0
- package/tui/catalog.js +1190 -0
- package/tui/index.js +727 -0
package/lib/diff-spec.js
ADDED
|
@@ -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
|
+
};
|