@xenonbyte/da-vinci-workflow 0.1.26 → 0.2.1

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 (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +17 -65
  3. package/README.zh-CN.md +17 -65
  4. package/commands/claude/dv/continue.md +5 -0
  5. package/commands/codex/prompts/dv-continue.md +6 -1
  6. package/commands/gemini/dv/continue.toml +5 -0
  7. package/commands/templates/dv-continue.shared.md +33 -0
  8. package/docs/dv-command-reference.md +31 -0
  9. package/docs/execution-chain-migration.md +46 -0
  10. package/docs/execution-chain-plan.md +125 -0
  11. package/docs/prompt-entrypoints.md +6 -0
  12. package/docs/workflow-examples.md +10 -0
  13. package/docs/workflow-overview.md +25 -0
  14. package/docs/zh-CN/dv-command-reference.md +31 -0
  15. package/docs/zh-CN/execution-chain-migration.md +46 -0
  16. package/docs/zh-CN/prompt-entrypoints.md +6 -0
  17. package/docs/zh-CN/workflow-examples.md +10 -0
  18. package/docs/zh-CN/workflow-overview.md +25 -0
  19. package/lib/artifact-parsers.js +120 -0
  20. package/lib/audit.js +61 -0
  21. package/lib/cli.js +328 -13
  22. package/lib/diff-spec.js +242 -0
  23. package/lib/execution-signals.js +136 -0
  24. package/lib/lint-bindings.js +143 -0
  25. package/lib/lint-spec.js +408 -0
  26. package/lib/lint-tasks.js +176 -0
  27. package/lib/planning-parsers.js +567 -0
  28. package/lib/scaffold.js +193 -0
  29. package/lib/scope-check.js +603 -0
  30. package/lib/sidecars.js +369 -0
  31. package/lib/supervisor-review.js +28 -3
  32. package/lib/utils.js +10 -2
  33. package/lib/verify.js +652 -0
  34. package/lib/workflow-contract.js +107 -0
  35. package/lib/workflow-persisted-state.js +297 -0
  36. package/lib/workflow-state.js +785 -0
  37. package/package.json +10 -2
  38. package/references/artifact-templates.md +26 -0
  39. package/references/checkpoints.md +14 -0
  40. package/references/modes.md +10 -0
@@ -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
+ };
@@ -0,0 +1,408 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { pathExists } = require("./utils");
4
+ const { STATUS } = require("./workflow-contract");
5
+ const { parseRuntimeSpecMarkdown } = require("./artifact-parsers");
6
+ const { resolveChangeDir, detectSpecFiles } = require("./planning-parsers");
7
+
8
+ const MAX_EXAMPLE_ITEMS = 3;
9
+ const TESTABLE_ACCEPTANCE_PATTERN =
10
+ /\b(must|shall|should|when|then|if|display|show|hide|render|return|emit|within|ms|seconds?|error|success|true|false)\b/i;
11
+ const VAGUE_ACCEPTANCE_PATTERN =
12
+ /\b(good|nice|better|clean|intuitive|seamless|smooth|easy|simple|beautiful|fast)\b/i;
13
+ const WORD_PATTERN = /[a-z][a-z0-9_-]{2,}/gi;
14
+ const STOP_WORDS = new Set([
15
+ "the",
16
+ "and",
17
+ "for",
18
+ "with",
19
+ "from",
20
+ "that",
21
+ "this",
22
+ "then",
23
+ "when",
24
+ "into",
25
+ "only",
26
+ "each",
27
+ "user",
28
+ "users",
29
+ "page",
30
+ "state",
31
+ "states",
32
+ "input",
33
+ "inputs",
34
+ "output",
35
+ "outputs",
36
+ "should",
37
+ "must",
38
+ "shall",
39
+ "able",
40
+ "about",
41
+ "after",
42
+ "before",
43
+ "during",
44
+ "while",
45
+ "where",
46
+ "there",
47
+ "have",
48
+ "has",
49
+ "into"
50
+ ]);
51
+ const EXPLICIT_CONTRADICTORY_PAIRS = [
52
+ ["enabled", "disabled"],
53
+ ["active", "inactive"],
54
+ ["online", "offline"],
55
+ ["visible", "hidden"],
56
+ ["open", "closed"],
57
+ ["expanded", "collapsed"],
58
+ ["authenticated", "unauthenticated"],
59
+ ["authorized", "unauthorized"]
60
+ ];
61
+
62
+ function buildResultEnvelope(projectRoot, strict) {
63
+ return {
64
+ status: STATUS.PASS,
65
+ failures: [],
66
+ warnings: [],
67
+ notes: [],
68
+ projectRoot,
69
+ changeId: null,
70
+ strict,
71
+ specs: [],
72
+ summary: {
73
+ checked: 0
74
+ }
75
+ };
76
+ }
77
+
78
+ function finalizeResult(result) {
79
+ const hasFindings = result.failures.length > 0 || result.warnings.length > 0;
80
+ if (!hasFindings) {
81
+ result.status = STATUS.PASS;
82
+ return result;
83
+ }
84
+
85
+ if (result.strict) {
86
+ result.status = STATUS.BLOCK;
87
+ return result;
88
+ }
89
+
90
+ result.status = STATUS.WARN;
91
+ return result;
92
+ }
93
+
94
+ function normalizeText(value) {
95
+ return String(value || "")
96
+ .toLowerCase()
97
+ .replace(/[`*_~]/g, "")
98
+ .replace(/\s+/g, " ")
99
+ .trim();
100
+ }
101
+
102
+ function toKeywordSet(value) {
103
+ const words = String(value || "").match(WORD_PATTERN) || [];
104
+ return new Set(
105
+ words
106
+ .map((word) => String(word || "").toLowerCase())
107
+ .filter((word) => word && !STOP_WORDS.has(word))
108
+ );
109
+ }
110
+
111
+ function containsAnyKeyword(text, keywords) {
112
+ if (!Array.isArray(keywords) || keywords.length === 0) {
113
+ return false;
114
+ }
115
+ const source = normalizeText(text);
116
+ return keywords.some((keyword) => source.includes(keyword));
117
+ }
118
+
119
+ function expandCoverageKeywords(keywords) {
120
+ const expanded = [];
121
+ for (const keyword of keywords || []) {
122
+ if (!keyword) {
123
+ continue;
124
+ }
125
+ expanded.push(keyword);
126
+
127
+ if (keyword === "success") {
128
+ expanded.push("succeed", "succeeds", "succeeded");
129
+ continue;
130
+ }
131
+ if (keyword === "error") {
132
+ expanded.push("fail", "fails", "failed", "failure");
133
+ continue;
134
+ }
135
+ if (keyword === "loading") {
136
+ expanded.push("load", "spinner");
137
+ }
138
+ }
139
+ return dedupe(expanded);
140
+ }
141
+
142
+ function dedupe(values) {
143
+ return Array.from(new Set((values || []).filter(Boolean)));
144
+ }
145
+
146
+ function appendSectionPresenceFindings(relativePath, parsed, failures) {
147
+ for (const heading of parsed.missingSections) {
148
+ failures.push(`${relativePath}: missing required section \`${heading}\`.`);
149
+ }
150
+ for (const heading of parsed.emptySections) {
151
+ failures.push(`${relativePath}: required section \`${heading}\` is empty.`);
152
+ }
153
+
154
+ const requiredItemSections = [
155
+ "behavior",
156
+ "states",
157
+ "inputs",
158
+ "outputs",
159
+ "acceptance",
160
+ "edgeCases"
161
+ ];
162
+ for (const sectionId of requiredItemSections) {
163
+ const section = parsed.sections[sectionId];
164
+ if (!section || !section.present || section.empty) {
165
+ continue;
166
+ }
167
+ if (section.items.length === 0) {
168
+ failures.push(
169
+ `${relativePath}: section \`${section.heading}\` is present but no list or paragraph items were parsed.`
170
+ );
171
+ }
172
+ }
173
+ }
174
+
175
+ function appendAcceptanceQualityFindings(relativePath, parsed, warnings) {
176
+ const acceptanceItems = parsed.sections.acceptance ? parsed.sections.acceptance.items : [];
177
+ for (const item of acceptanceItems) {
178
+ const normalized = String(item || "").trim();
179
+ if (!normalized) {
180
+ continue;
181
+ }
182
+
183
+ const hasTestableSignal = TESTABLE_ACCEPTANCE_PATTERN.test(normalized);
184
+ const looksVague = VAGUE_ACCEPTANCE_PATTERN.test(normalized);
185
+ const hasMetricHint = /\d/.test(normalized);
186
+ if (hasTestableSignal && (!looksVague || hasMetricHint)) {
187
+ continue;
188
+ }
189
+
190
+ warnings.push(
191
+ `${relativePath}: acceptance item may be non-testable: "${normalized.slice(0, 120)}".`
192
+ );
193
+ }
194
+ }
195
+
196
+ function appendStateContradictionFindings(relativePath, parsed, warnings) {
197
+ const stateItems = parsed.sections.states ? parsed.sections.states.items : [];
198
+ const normalizedStates = stateItems.map((value) => normalizeText(value)).filter(Boolean);
199
+ const uniqueStates = new Set(normalizedStates);
200
+
201
+ if (uniqueStates.size !== normalizedStates.length) {
202
+ warnings.push(`${relativePath}: duplicate state names were detected.`);
203
+ }
204
+
205
+ for (const [left, right] of EXPLICIT_CONTRADICTORY_PAIRS) {
206
+ if (uniqueStates.has(left) && uniqueStates.has(right)) {
207
+ warnings.push(`${relativePath}: contradictory state pair detected: "${left}" and "${right}".`);
208
+ }
209
+ }
210
+
211
+ const prefixedNegationMap = new Map();
212
+ for (const state of uniqueStates) {
213
+ const match = state.match(/^(?:not|non)\s+(.+)$/);
214
+ if (!match) {
215
+ continue;
216
+ }
217
+ const positive = String(match[1] || "").trim();
218
+ if (!positive) {
219
+ continue;
220
+ }
221
+ prefixedNegationMap.set(positive, state);
222
+ }
223
+
224
+ for (const [positive, negative] of prefixedNegationMap.entries()) {
225
+ if (uniqueStates.has(positive)) {
226
+ warnings.push(
227
+ `${relativePath}: contradictory state pair detected: "${positive}" and "${negative}".`
228
+ );
229
+ }
230
+ }
231
+ }
232
+
233
+ function appendCoverageFindings(relativePath, parsed, warnings) {
234
+ const behaviorItems = parsed.sections.behavior ? parsed.sections.behavior.items : [];
235
+ const stateItems = parsed.sections.states ? parsed.sections.states.items : [];
236
+ const acceptanceItems = parsed.sections.acceptance ? parsed.sections.acceptance.items : [];
237
+
238
+ const behaviorText = behaviorItems.join(" ");
239
+ const acceptanceText = acceptanceItems.join(" ");
240
+ const combinedCoverageText = `${behaviorText} ${acceptanceText}`.trim();
241
+
242
+ for (const stateItem of stateItems) {
243
+ const keywords = expandCoverageKeywords(Array.from(toKeywordSet(stateItem)));
244
+ if (keywords.length === 0) {
245
+ continue;
246
+ }
247
+ if (!containsAnyKeyword(combinedCoverageText, keywords)) {
248
+ warnings.push(
249
+ `${relativePath}: state coverage gap: "${stateItem}" does not appear in behavior or acceptance sections.`
250
+ );
251
+ }
252
+ }
253
+
254
+ const uncoveredBehaviorItems = [];
255
+ for (const behaviorItem of behaviorItems) {
256
+ const keywords = Array.from(toKeywordSet(behaviorItem));
257
+ if (keywords.length === 0) {
258
+ continue;
259
+ }
260
+ if (!containsAnyKeyword(acceptanceText, keywords)) {
261
+ uncoveredBehaviorItems.push(behaviorItem);
262
+ }
263
+ }
264
+
265
+ if (uncoveredBehaviorItems.length > 0) {
266
+ const preview = uncoveredBehaviorItems.slice(0, MAX_EXAMPLE_ITEMS).join(" | ");
267
+ warnings.push(
268
+ `${relativePath}: acceptance coverage may miss behavior items (${uncoveredBehaviorItems.length}): ${preview}`
269
+ );
270
+ }
271
+ }
272
+
273
+ function resolveChange(projectRoot, requestedChangeId, failures, notes) {
274
+ const resolved = resolveChangeDir(projectRoot, requestedChangeId);
275
+ failures.push(...resolved.failures);
276
+ notes.push(...resolved.notes);
277
+ return resolved.changeDir;
278
+ }
279
+
280
+ function lintRuntimeSpecs(projectPathInput, options = {}) {
281
+ const projectRoot = path.resolve(projectPathInput || process.cwd());
282
+ const strict = options.strict === true;
283
+ const requestedChangeId = options.changeId ? String(options.changeId).trim() : "";
284
+ const result = buildResultEnvelope(projectRoot, strict);
285
+
286
+ if (!pathExists(projectRoot)) {
287
+ result.failures.push(`Project path does not exist: ${projectRoot}`);
288
+ result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
289
+ return finalizeResult(result);
290
+ }
291
+
292
+ const changeDir = resolveChange(projectRoot, requestedChangeId, result.failures, result.notes);
293
+ if (!changeDir) {
294
+ result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
295
+ return finalizeResult(result);
296
+ }
297
+ result.changeId = path.basename(changeDir);
298
+
299
+ const specsDir = path.join(changeDir, "specs");
300
+ const specFiles = detectSpecFiles(specsDir);
301
+ if (specFiles.length === 0) {
302
+ result.failures.push("No runtime spec files found under `.da-vinci/changes/<change-id>/specs/*/spec.md`.");
303
+ result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
304
+ return finalizeResult(result);
305
+ }
306
+
307
+ for (const specPath of specFiles) {
308
+ const relativePath = path.relative(projectRoot, specPath) || specPath;
309
+ const raw = fs.readFileSync(specPath, "utf8");
310
+ const parsed = parseRuntimeSpecMarkdown(raw);
311
+ const fileFailures = [];
312
+ const fileWarnings = [];
313
+ const fileNotes = [];
314
+
315
+ appendSectionPresenceFindings(relativePath, parsed, fileFailures);
316
+ appendAcceptanceQualityFindings(relativePath, parsed, fileWarnings);
317
+ appendStateContradictionFindings(relativePath, parsed, fileWarnings);
318
+ appendCoverageFindings(relativePath, parsed, fileWarnings);
319
+
320
+ if (/^\s*##\s+ADDED Requirements\b/im.test(raw)) {
321
+ fileNotes.push(
322
+ `${relativePath}: OpenSpec planning sections detected. Runtime lint still requires Da Vinci runtime headings.`
323
+ );
324
+ }
325
+
326
+ result.failures.push(...fileFailures);
327
+ result.warnings.push(...fileWarnings);
328
+ result.notes.push(...fileNotes);
329
+ result.specs.push({
330
+ path: relativePath,
331
+ status:
332
+ fileFailures.length > 0 || fileWarnings.length > 0
333
+ ? strict
334
+ ? STATUS.BLOCK
335
+ : STATUS.WARN
336
+ : STATUS.PASS,
337
+ failures: fileFailures,
338
+ warnings: fileWarnings,
339
+ notes: fileNotes,
340
+ parsed: {
341
+ missingSections: parsed.missingSections,
342
+ emptySections: parsed.emptySections,
343
+ itemCounts: {
344
+ behavior: parsed.sections.behavior.items.length,
345
+ states: parsed.sections.states.items.length,
346
+ inputs: parsed.sections.inputs.items.length,
347
+ outputs: parsed.sections.outputs.items.length,
348
+ acceptance: parsed.sections.acceptance.items.length,
349
+ edgeCases: parsed.sections.edgeCases.items.length
350
+ }
351
+ }
352
+ });
353
+ }
354
+
355
+ result.summary.checked = result.specs.length;
356
+ result.failures = dedupe(result.failures);
357
+ result.warnings = dedupe(result.warnings);
358
+ result.notes = dedupe(result.notes);
359
+ result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
360
+
361
+ return finalizeResult(result);
362
+ }
363
+
364
+ function formatLintSpecReport(result) {
365
+ const lines = [
366
+ "Da Vinci lint-spec",
367
+ `Project: ${result.projectRoot}`,
368
+ `Change: ${result.changeId || "(not selected)"}`,
369
+ `Strict mode: ${result.strict ? "yes" : "no"}`,
370
+ `Status: ${result.status}`,
371
+ `Spec files checked: ${result.summary.checked}`
372
+ ];
373
+
374
+ if (result.specs.length > 0) {
375
+ lines.push("", "Spec results:");
376
+ for (const spec of result.specs) {
377
+ lines.push(`- ${spec.path}: ${spec.status}`);
378
+ }
379
+ }
380
+
381
+ if (result.failures.length > 0) {
382
+ lines.push("", "Failures:");
383
+ for (const failure of result.failures) {
384
+ lines.push(`- ${failure}`);
385
+ }
386
+ }
387
+
388
+ if (result.warnings.length > 0) {
389
+ lines.push("", "Warnings:");
390
+ for (const warning of result.warnings) {
391
+ lines.push(`- ${warning}`);
392
+ }
393
+ }
394
+
395
+ if (result.notes.length > 0) {
396
+ lines.push("", "Notes:");
397
+ for (const note of result.notes) {
398
+ lines.push(`- ${note}`);
399
+ }
400
+ }
401
+
402
+ return lines.join("\n");
403
+ }
404
+
405
+ module.exports = {
406
+ lintRuntimeSpecs,
407
+ formatLintSpecReport
408
+ };