@xenonbyte/da-vinci-workflow 0.2.5 → 0.2.7
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 +32 -0
- package/README.md +15 -9
- package/README.zh-CN.md +16 -9
- package/docs/dv-command-reference.md +21 -3
- package/docs/execution-chain-migration.md +14 -3
- package/docs/maintainer-bootstrap.md +102 -0
- package/docs/pencil-rendering-workflow.md +1 -1
- package/docs/skill-usage.md +31 -0
- package/docs/workflow-overview.md +40 -5
- package/docs/zh-CN/dv-command-reference.md +19 -3
- package/docs/zh-CN/maintainer-bootstrap.md +101 -0
- package/docs/zh-CN/pencil-rendering-workflow.md +1 -1
- package/docs/zh-CN/skill-usage.md +30 -0
- package/docs/zh-CN/workflow-overview.md +38 -5
- package/lib/audit.js +19 -0
- package/lib/cli/helpers.js +63 -2
- package/lib/cli.js +119 -2
- package/lib/gate-utils.js +56 -0
- package/lib/install.js +134 -6
- package/lib/isolated-worker-handoff.js +181 -0
- package/lib/lint-bindings.js +41 -28
- package/lib/lint-spec.js +403 -109
- package/lib/lint-tasks.js +571 -21
- package/lib/maintainer-readiness.js +317 -0
- package/lib/planning-parsers.js +190 -1
- package/lib/planning-quality-utils.js +81 -0
- package/lib/planning-signal-freshness.js +205 -0
- package/lib/scope-check.js +751 -82
- package/lib/sidecars.js +396 -1
- package/lib/supervisor-review.js +117 -6
- package/lib/task-execution.js +88 -16
- package/lib/task-review.js +14 -8
- package/lib/utils.js +15 -0
- package/lib/workflow-persisted-state.js +52 -32
- package/lib/workflow-state.js +1241 -249
- package/package.json +3 -2
package/lib/lint-spec.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const { pathExists } = require("./utils");
|
|
3
|
+
const { pathExists, readTextIfExists } = require("./utils");
|
|
4
4
|
const { STATUS } = require("./workflow-contract");
|
|
5
5
|
const { parseRuntimeSpecMarkdown } = require("./artifact-parsers");
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
normalizeText,
|
|
8
|
+
tokenizeNormalizedWords,
|
|
9
|
+
textContainsAnyNormalizedToken,
|
|
10
|
+
detectSpecFiles,
|
|
11
|
+
parseAmbiguityRecords,
|
|
12
|
+
readChangeArtifacts,
|
|
13
|
+
readArtifactTexts
|
|
14
|
+
} = require("./planning-parsers");
|
|
15
|
+
const { buildGateEnvelope, finalizeGateEnvelope, dedupe } = require("./gate-utils");
|
|
16
|
+
const {
|
|
17
|
+
buildStopWordSet,
|
|
18
|
+
buildBasePlanningResultEnvelope,
|
|
19
|
+
finalizePlanningResult,
|
|
20
|
+
resolveChangeWithFindings
|
|
21
|
+
} = require("./planning-quality-utils");
|
|
7
22
|
|
|
8
23
|
const MAX_EXAMPLE_ITEMS = 3;
|
|
9
24
|
const TESTABLE_ACCEPTANCE_PATTERN =
|
|
@@ -11,43 +26,7 @@ const TESTABLE_ACCEPTANCE_PATTERN =
|
|
|
11
26
|
const VAGUE_ACCEPTANCE_PATTERN =
|
|
12
27
|
/\b(good|nice|better|clean|intuitive|seamless|smooth|easy|simple|beautiful|fast)\b/i;
|
|
13
28
|
const WORD_PATTERN = /[a-z][a-z0-9_-]{2,}/gi;
|
|
14
|
-
const STOP_WORDS =
|
|
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
|
-
]);
|
|
29
|
+
const STOP_WORDS = buildStopWordSet();
|
|
51
30
|
const EXPLICIT_CONTRADICTORY_PAIRS = [
|
|
52
31
|
["enabled", "disabled"],
|
|
53
32
|
["active", "inactive"],
|
|
@@ -58,16 +37,66 @@ const EXPLICIT_CONTRADICTORY_PAIRS = [
|
|
|
58
37
|
["authenticated", "unauthenticated"],
|
|
59
38
|
["authorized", "unauthorized"]
|
|
60
39
|
];
|
|
40
|
+
const PLACEHOLDER_AMBIGUITY_PATTERN = /\b(TODO|TBD|unknown)\b/i;
|
|
41
|
+
const PRINCIPLE_DRIFT_PATTERNS = [
|
|
42
|
+
{
|
|
43
|
+
id: "spec_only_truth",
|
|
44
|
+
message: "artifact implies spec-only truth and conflicts with requirement/design/code truth boundaries",
|
|
45
|
+
pattern:
|
|
46
|
+
/\b(spec|specification)\b[\s\S]{0,60}\b(only|single|sole)\b[\s\S]{0,40}\b(truth|source of truth)\b/i
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "design_truth_erasure",
|
|
50
|
+
message: "artifact implies design truth can be ignored in planning",
|
|
51
|
+
pattern: /\b(ignore|skip|bypass)\b[\s\S]{0,40}\b(design|pencil)\b[\s\S]{0,40}\btruth\b/i
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "second_principle_source",
|
|
55
|
+
message: "artifact introduces a new constitution-style principle source",
|
|
56
|
+
pattern: /\b(constitution\.md|new constitution|principle manifesto)\b/i
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
const PRINCIPLE_TRUTH_BOUNDARY_CONTRADICTION_PATTERNS = [
|
|
60
|
+
{
|
|
61
|
+
id: "design_controls_behavior",
|
|
62
|
+
message: "artifact assigns behavior authority to design/pencil instead of requirements",
|
|
63
|
+
pattern:
|
|
64
|
+
/\b(pencil|design)\b[\s\S]{0,48}\b(decide|decides|determine|determines|govern|governs|define|defines|control|controls|drive|drives)\b[\s\S]{0,48}\bbehavior\b/i
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "requirements_controls_presentation",
|
|
68
|
+
message: "artifact assigns presentation authority to requirements/spec instead of design/pencil",
|
|
69
|
+
pattern:
|
|
70
|
+
/\b(requirement|requirements|spec|specification)\b[\s\S]{0,48}\b(decide|decides|determine|determines|govern|governs|define|defines|control|controls|drive|drives)\b[\s\S]{0,48}\b(presentation|visual|layout|ui)\b/i
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "code_controls_planning_truth",
|
|
74
|
+
message: "artifact assigns planning truth authority to code instead of requirements/design boundaries",
|
|
75
|
+
pattern:
|
|
76
|
+
/\b(code|implementation)\b[\s\S]{0,48}\b(decide|decides|determine|determines|govern|governs|define|defines|control|controls|drive|drives)\b[\s\S]{0,48}\b(behavior|presentation|visual|layout|ui|requirements?)\b/i
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
const ACCEPTED_AMBIGUITY_CLASSES = new Set([
|
|
80
|
+
"behavior",
|
|
81
|
+
"scope",
|
|
82
|
+
"page",
|
|
83
|
+
"surface",
|
|
84
|
+
"state",
|
|
85
|
+
"input",
|
|
86
|
+
"data",
|
|
87
|
+
"acceptance",
|
|
88
|
+
"design_dependency",
|
|
89
|
+
"design-dependency"
|
|
90
|
+
]);
|
|
61
91
|
|
|
62
92
|
function buildResultEnvelope(projectRoot, strict) {
|
|
63
93
|
return {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
strict,
|
|
94
|
+
...buildBasePlanningResultEnvelope(projectRoot, strict),
|
|
95
|
+
gates: {
|
|
96
|
+
principleInheritance: null,
|
|
97
|
+
clarify: null,
|
|
98
|
+
scenarioQuality: null
|
|
99
|
+
},
|
|
71
100
|
specs: [],
|
|
72
101
|
summary: {
|
|
73
102
|
checked: 0
|
|
@@ -75,45 +104,12 @@ function buildResultEnvelope(projectRoot, strict) {
|
|
|
75
104
|
};
|
|
76
105
|
}
|
|
77
106
|
|
|
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
107
|
function toKeywordSet(value) {
|
|
103
|
-
|
|
104
|
-
return new Set(
|
|
105
|
-
words
|
|
106
|
-
.map((word) => String(word || "").toLowerCase())
|
|
107
|
-
.filter((word) => word && !STOP_WORDS.has(word))
|
|
108
|
-
);
|
|
108
|
+
return new Set(tokenizeNormalizedWords(value, { pattern: WORD_PATTERN, stopWords: STOP_WORDS }));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function containsAnyKeyword(text, keywords) {
|
|
112
|
-
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
const source = normalizeText(text);
|
|
116
|
-
return keywords.some((keyword) => source.includes(keyword));
|
|
112
|
+
return textContainsAnyNormalizedToken(text, keywords);
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
function expandCoverageKeywords(keywords) {
|
|
@@ -139,8 +135,298 @@ function expandCoverageKeywords(keywords) {
|
|
|
139
135
|
return dedupe(expanded);
|
|
140
136
|
}
|
|
141
137
|
|
|
142
|
-
function
|
|
143
|
-
|
|
138
|
+
function collectPrincipleInheritanceGate(projectRoot, artifactEntries, strict = false) {
|
|
139
|
+
const gate = buildGateEnvelope("principleInheritance");
|
|
140
|
+
const repoPrinciplesPath = path.join(projectRoot, "openspec", "config.yaml");
|
|
141
|
+
const projectPrinciplesPath = path.join(projectRoot, "DA-VINCI.md");
|
|
142
|
+
const repoPrinciplesText = readTextIfExists(repoPrinciplesPath);
|
|
143
|
+
const projectPrinciplesText = readTextIfExists(projectPrinciplesPath);
|
|
144
|
+
if (!pathExists(repoPrinciplesPath)) {
|
|
145
|
+
gate.compatibility.push("repo-level principle source is missing (`openspec/config.yaml`).");
|
|
146
|
+
}
|
|
147
|
+
if (!pathExists(projectPrinciplesPath)) {
|
|
148
|
+
gate.compatibility.push("project-level principle source is missing (`DA-VINCI.md`); repo-level baseline only.");
|
|
149
|
+
} else if (projectPrinciplesText && /constitution\.md|principle manifesto|new constitution/i.test(projectPrinciplesText)) {
|
|
150
|
+
gate.blocking.push(
|
|
151
|
+
`DA-VINCI.md introduces a second constitution-style principle source and violates inheritance contract.`
|
|
152
|
+
);
|
|
153
|
+
gate.evidence.push("DA-VINCI.md");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const repoHasTruthBoundary =
|
|
157
|
+
/requirements.*behavior[\s\S]{0,80}pencil.*presentation[\s\S]{0,80}code/sim.test(
|
|
158
|
+
String(repoPrinciplesText || "")
|
|
159
|
+
) || /requirements decide behavior/i.test(String(repoPrinciplesText || ""));
|
|
160
|
+
const collectTruthBoundaryContradictions = (text) => {
|
|
161
|
+
const findings = [];
|
|
162
|
+
const payload = String(text || "");
|
|
163
|
+
for (const pattern of PRINCIPLE_TRUTH_BOUNDARY_CONTRADICTION_PATTERNS) {
|
|
164
|
+
if (!pattern.pattern.test(payload)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
findings.push(pattern.message);
|
|
168
|
+
}
|
|
169
|
+
return dedupe(findings);
|
|
170
|
+
};
|
|
171
|
+
if (repoHasTruthBoundary && projectPrinciplesText) {
|
|
172
|
+
for (const pattern of PRINCIPLE_DRIFT_PATTERNS) {
|
|
173
|
+
if (!pattern.pattern.test(projectPrinciplesText)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
gate.blocking.push(
|
|
177
|
+
`DA-VINCI.md conflicts with repo-level principle baseline (\`openspec/config.yaml\`): ${pattern.message}.`
|
|
178
|
+
);
|
|
179
|
+
gate.evidence.push("openspec/config.yaml");
|
|
180
|
+
gate.evidence.push("DA-VINCI.md");
|
|
181
|
+
}
|
|
182
|
+
for (const message of collectTruthBoundaryContradictions(projectPrinciplesText)) {
|
|
183
|
+
gate.blocking.push(
|
|
184
|
+
`DA-VINCI.md conflicts with repo-level principle baseline (\`openspec/config.yaml\`): ${message}.`
|
|
185
|
+
);
|
|
186
|
+
gate.evidence.push("openspec/config.yaml");
|
|
187
|
+
gate.evidence.push("DA-VINCI.md");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const entry of artifactEntries) {
|
|
192
|
+
const text = String(entry.text || "");
|
|
193
|
+
for (const pattern of PRINCIPLE_DRIFT_PATTERNS) {
|
|
194
|
+
if (!pattern.pattern.test(text)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
gate.blocking.push(
|
|
198
|
+
`${entry.path}: ${pattern.message} (violated source: openspec/config.yaml + DA-VINCI.md inheritance boundary).`
|
|
199
|
+
);
|
|
200
|
+
gate.evidence.push(entry.path);
|
|
201
|
+
}
|
|
202
|
+
if (repoHasTruthBoundary) {
|
|
203
|
+
for (const message of collectTruthBoundaryContradictions(text)) {
|
|
204
|
+
gate.blocking.push(
|
|
205
|
+
`${entry.path}: ${message} (violated source: openspec/config.yaml + DA-VINCI.md inheritance boundary).`
|
|
206
|
+
);
|
|
207
|
+
gate.evidence.push(entry.path);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return finalizeGateEnvelope(gate, {
|
|
213
|
+
strict,
|
|
214
|
+
// Missing project-level refinements are compatibility-only and must not become WARN by default.
|
|
215
|
+
warnOnCompatibility: false
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function collectClarifyGate(artifactEntries, strict = false) {
|
|
220
|
+
const gate = buildGateEnvelope("clarify", { includeBounded: true });
|
|
221
|
+
let explicitRecordCount = 0;
|
|
222
|
+
for (const entry of artifactEntries) {
|
|
223
|
+
const parsed = parseAmbiguityRecords(entry.text);
|
|
224
|
+
const explicitRecordLines = new Set((parsed.records || []).map((record) => Number(record.line)));
|
|
225
|
+
for (const malformed of parsed.malformed) {
|
|
226
|
+
gate.advisory.push(
|
|
227
|
+
`${entry.path}:${malformed.line}: malformed ambiguity record (${malformed.reason}).`
|
|
228
|
+
);
|
|
229
|
+
gate.evidence.push(`${entry.path}:${malformed.line}`);
|
|
230
|
+
}
|
|
231
|
+
for (const record of parsed.records) {
|
|
232
|
+
explicitRecordCount += 1;
|
|
233
|
+
const metadata = record.metadata || {};
|
|
234
|
+
const summary = `${entry.path}:${record.line}: ${record.ambiguityClass} - ${record.summary}`;
|
|
235
|
+
gate.evidence.push(`${entry.path}:${record.line}`);
|
|
236
|
+
if (!ACCEPTED_AMBIGUITY_CLASSES.has(record.ambiguityClass)) {
|
|
237
|
+
gate.advisory.push(
|
|
238
|
+
`${entry.path}:${record.line}: ambiguity class "${record.ambiguityClass}" is non-canonical and should be normalized.`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (record.severity === "blocking") {
|
|
242
|
+
gate.blocking.push(summary);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (record.severity === "bounded") {
|
|
246
|
+
const missingKeys = ["owner", "impact", "next", "artifact"].filter((key) => !metadata[key]);
|
|
247
|
+
if (missingKeys.length > 0) {
|
|
248
|
+
gate.blocking.push(`${summary} (bounded record missing: ${missingKeys.join(", ")}).`);
|
|
249
|
+
} else {
|
|
250
|
+
gate.bounded.push(summary);
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
gate.advisory.push(summary);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const lines = String(entry.text || "").replace(/\r\n?/g, "\n").split("\n");
|
|
258
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
259
|
+
const line = String(lines[lineIndex] || "");
|
|
260
|
+
if (!PLACEHOLDER_AMBIGUITY_PATTERN.test(line)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (explicitRecordLines.has(lineIndex + 1)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
gate.blocking.push(
|
|
267
|
+
`${entry.path}:${lineIndex + 1}: unresolved placeholder-style ambiguity (${line.trim().slice(0, 120)}).`
|
|
268
|
+
);
|
|
269
|
+
gate.evidence.push(`${entry.path}:${lineIndex + 1}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (explicitRecordCount === 0) {
|
|
274
|
+
gate.compatibility.push(
|
|
275
|
+
"no explicit ambiguity records found; fallback placeholder heuristics applied (legacy compatibility)."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return finalizeGateEnvelope(gate, {
|
|
280
|
+
strict,
|
|
281
|
+
includeBounded: true,
|
|
282
|
+
warnOnBounded: false
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isScenarioShapedAcceptance(itemText) {
|
|
287
|
+
const text = String(itemText || "");
|
|
288
|
+
const hasTrigger = /\b(when|if|on|after|before|while)\b/i.test(text);
|
|
289
|
+
const hasOutcomeVerb =
|
|
290
|
+
/\b(show|shows|render|renders|return|returns|emit|emits|display|displays|appear|appears|visible|hidden|error|success|retry|fallback|within)\b/i.test(
|
|
291
|
+
text
|
|
292
|
+
);
|
|
293
|
+
const hasMeasuredOutcome =
|
|
294
|
+
/\b(within|at\s+most|no\s+more\s+than|less\s+than|greater\s+than|at\s+least)\s+\d+\b/i.test(text) ||
|
|
295
|
+
/\b\d+(?:\.\d+)?\s*(ms|s|sec|secs|seconds|minutes?|hours?|px|%)\b/i.test(text);
|
|
296
|
+
const hasOutcome = hasOutcomeVerb || hasMeasuredOutcome;
|
|
297
|
+
return hasTrigger && hasOutcome;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function collectScenarioQualityGate(specRecords, strict = false) {
|
|
301
|
+
const gate = buildGateEnvelope("scenarioQuality");
|
|
302
|
+
for (const record of specRecords) {
|
|
303
|
+
const acceptanceItems =
|
|
304
|
+
record && record.parsed && record.parsed.sections && record.parsed.sections.acceptance
|
|
305
|
+
? record.parsed.sections.acceptance.items
|
|
306
|
+
: [];
|
|
307
|
+
const behaviorItems =
|
|
308
|
+
record && record.parsed && record.parsed.sections && record.parsed.sections.behavior
|
|
309
|
+
? record.parsed.sections.behavior.items
|
|
310
|
+
: [];
|
|
311
|
+
const stateItems =
|
|
312
|
+
record && record.parsed && record.parsed.sections && record.parsed.sections.states
|
|
313
|
+
? record.parsed.sections.states.items
|
|
314
|
+
: [];
|
|
315
|
+
const inputItems =
|
|
316
|
+
record && record.parsed && record.parsed.sections && record.parsed.sections.inputs
|
|
317
|
+
? record.parsed.sections.inputs.items
|
|
318
|
+
: [];
|
|
319
|
+
const outputItems =
|
|
320
|
+
record && record.parsed && record.parsed.sections && record.parsed.sections.outputs
|
|
321
|
+
? record.parsed.sections.outputs.items
|
|
322
|
+
: [];
|
|
323
|
+
const edgeCaseItems =
|
|
324
|
+
record && record.parsed && record.parsed.sections && record.parsed.sections.edgeCases
|
|
325
|
+
? record.parsed.sections.edgeCases.items
|
|
326
|
+
: [];
|
|
327
|
+
|
|
328
|
+
if (!Array.isArray(acceptanceItems) || acceptanceItems.length === 0) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const shaped = acceptanceItems.filter((item) => isScenarioShapedAcceptance(item));
|
|
332
|
+
if (shaped.length === 0) {
|
|
333
|
+
gate.blocking.push(
|
|
334
|
+
`${record.path}: acceptance section is non-empty but has no scenario-shaped acceptance item.`
|
|
335
|
+
);
|
|
336
|
+
gate.evidence.push(`${record.path}:Acceptance`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const hasScenarioIds = acceptanceItems.some((item) => /\bscenario[-_ ]?\d+\b/i.test(String(item || "")));
|
|
341
|
+
if (!hasScenarioIds) {
|
|
342
|
+
gate.compatibility.push(
|
|
343
|
+
`${record.path}: acceptance remains compatible without explicit scenario ids (legacy runtime schema).`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (shaped.length < acceptanceItems.length) {
|
|
348
|
+
gate.advisory.push(
|
|
349
|
+
`${record.path}: ${acceptanceItems.length - shaped.length} acceptance item(s) remain weakly scenario-shaped.`
|
|
350
|
+
);
|
|
351
|
+
gate.evidence.push(`${record.path}:Acceptance`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const behaviorText = behaviorItems.join(" ");
|
|
355
|
+
const acceptanceText = acceptanceItems.join(" ");
|
|
356
|
+
const behaviorAcceptanceText = `${behaviorText} ${acceptanceText}`.trim();
|
|
357
|
+
for (const stateItem of stateItems) {
|
|
358
|
+
const keywords = expandCoverageKeywords(Array.from(toKeywordSet(stateItem)));
|
|
359
|
+
if (keywords.length === 0) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (!containsAnyKeyword(behaviorAcceptanceText, keywords)) {
|
|
363
|
+
gate.advisory.push(
|
|
364
|
+
`${record.path}: state/behavior/acceptance coherence gap for state "${stateItem}".`
|
|
365
|
+
);
|
|
366
|
+
gate.evidence.push(`${record.path}:States`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const inputItem of inputItems) {
|
|
371
|
+
const keywords = Array.from(toKeywordSet(inputItem));
|
|
372
|
+
if (keywords.length === 0) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (!containsAnyKeyword(`${behaviorText} ${acceptanceText}`, keywords)) {
|
|
376
|
+
gate.advisory.push(
|
|
377
|
+
`${record.path}: input linkage gap for "${inputItem}" (not connected to behavior/acceptance).`
|
|
378
|
+
);
|
|
379
|
+
gate.evidence.push(`${record.path}:Inputs`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const outputItem of outputItems) {
|
|
383
|
+
const keywords = Array.from(toKeywordSet(outputItem));
|
|
384
|
+
if (keywords.length === 0) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!containsAnyKeyword(`${behaviorText} ${acceptanceText}`, keywords)) {
|
|
388
|
+
gate.advisory.push(
|
|
389
|
+
`${record.path}: output linkage gap for "${outputItem}" (not connected to behavior/acceptance).`
|
|
390
|
+
);
|
|
391
|
+
gate.evidence.push(`${record.path}:Outputs`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const edgeCaseItem of edgeCaseItems) {
|
|
395
|
+
const keywords = Array.from(toKeywordSet(edgeCaseItem));
|
|
396
|
+
if (keywords.length === 0) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const sourceText = `${behaviorText} ${acceptanceText} ${stateItems.join(" ")} ${inputItems.join(" ")}`;
|
|
400
|
+
if (!containsAnyKeyword(sourceText, keywords)) {
|
|
401
|
+
gate.advisory.push(
|
|
402
|
+
`${record.path}: edge-case linkage gap for "${edgeCaseItem}" (not traceable to behavior/state/input/acceptance).`
|
|
403
|
+
);
|
|
404
|
+
gate.evidence.push(`${record.path}:Edge Cases`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return finalizeGateEnvelope(gate, {
|
|
409
|
+
strict,
|
|
410
|
+
warnOnCompatibility: false
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function attachGateFindings(result, gate) {
|
|
415
|
+
const gateId = gate && gate.id ? gate.id : "unknown";
|
|
416
|
+
for (const finding of gate.blocking || []) {
|
|
417
|
+
result.failures.push(`[gate:${gateId}] ${finding}`);
|
|
418
|
+
}
|
|
419
|
+
for (const finding of gate.advisory || []) {
|
|
420
|
+
result.warnings.push(`[gate:${gateId}] ${finding}`);
|
|
421
|
+
}
|
|
422
|
+
for (const finding of gate.compatibility || []) {
|
|
423
|
+
result.notes.push(`[gate:${gateId}] ${finding}`);
|
|
424
|
+
}
|
|
425
|
+
if (gateId === "clarify") {
|
|
426
|
+
for (const finding of gate.bounded || []) {
|
|
427
|
+
result.notes.push(`[gate:${gateId}] bounded ambiguity: ${finding}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
144
430
|
}
|
|
145
431
|
|
|
146
432
|
function appendSectionPresenceFindings(relativePath, parsed, failures) {
|
|
@@ -232,24 +518,10 @@ function appendStateContradictionFindings(relativePath, parsed, warnings) {
|
|
|
232
518
|
|
|
233
519
|
function appendCoverageFindings(relativePath, parsed, warnings) {
|
|
234
520
|
const behaviorItems = parsed.sections.behavior ? parsed.sections.behavior.items : [];
|
|
235
|
-
const stateItems = parsed.sections.states ? parsed.sections.states.items : [];
|
|
236
521
|
const acceptanceItems = parsed.sections.acceptance ? parsed.sections.acceptance.items : [];
|
|
237
522
|
|
|
238
523
|
const behaviorText = behaviorItems.join(" ");
|
|
239
524
|
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
525
|
|
|
254
526
|
const uncoveredBehaviorItems = [];
|
|
255
527
|
for (const behaviorItem of behaviorItems) {
|
|
@@ -270,13 +542,6 @@ function appendCoverageFindings(relativePath, parsed, warnings) {
|
|
|
270
542
|
}
|
|
271
543
|
}
|
|
272
544
|
|
|
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
545
|
function lintRuntimeSpecs(projectPathInput, options = {}) {
|
|
281
546
|
const projectRoot = path.resolve(projectPathInput || process.cwd());
|
|
282
547
|
const strict = options.strict === true;
|
|
@@ -286,13 +551,13 @@ function lintRuntimeSpecs(projectPathInput, options = {}) {
|
|
|
286
551
|
if (!pathExists(projectRoot)) {
|
|
287
552
|
result.failures.push(`Project path does not exist: ${projectRoot}`);
|
|
288
553
|
result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
|
|
289
|
-
return
|
|
554
|
+
return finalizePlanningResult(result);
|
|
290
555
|
}
|
|
291
556
|
|
|
292
|
-
const changeDir =
|
|
557
|
+
const changeDir = resolveChangeWithFindings(projectRoot, requestedChangeId, result.failures, result.notes);
|
|
293
558
|
if (!changeDir) {
|
|
294
559
|
result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
|
|
295
|
-
return
|
|
560
|
+
return finalizePlanningResult(result);
|
|
296
561
|
}
|
|
297
562
|
result.changeId = path.basename(changeDir);
|
|
298
563
|
|
|
@@ -301,13 +566,19 @@ function lintRuntimeSpecs(projectPathInput, options = {}) {
|
|
|
301
566
|
if (specFiles.length === 0) {
|
|
302
567
|
result.failures.push("No runtime spec files found under `.da-vinci/changes/<change-id>/specs/*/spec.md`.");
|
|
303
568
|
result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
|
|
304
|
-
return
|
|
569
|
+
return finalizePlanningResult(result);
|
|
305
570
|
}
|
|
306
571
|
|
|
572
|
+
const specRecords = [];
|
|
307
573
|
for (const specPath of specFiles) {
|
|
308
574
|
const relativePath = path.relative(projectRoot, specPath) || specPath;
|
|
309
575
|
const raw = fs.readFileSync(specPath, "utf8");
|
|
310
576
|
const parsed = parseRuntimeSpecMarkdown(raw);
|
|
577
|
+
specRecords.push({
|
|
578
|
+
path: relativePath,
|
|
579
|
+
text: raw,
|
|
580
|
+
parsed
|
|
581
|
+
});
|
|
311
582
|
const fileFailures = [];
|
|
312
583
|
const fileWarnings = [];
|
|
313
584
|
const fileNotes = [];
|
|
@@ -352,13 +623,36 @@ function lintRuntimeSpecs(projectPathInput, options = {}) {
|
|
|
352
623
|
});
|
|
353
624
|
}
|
|
354
625
|
|
|
626
|
+
const changeArtifacts = readChangeArtifacts(projectRoot, result.changeId);
|
|
627
|
+
const changeArtifactTexts = readArtifactTexts(changeArtifacts);
|
|
628
|
+
const artifactEntries = [
|
|
629
|
+
...Object.entries(changeArtifactTexts)
|
|
630
|
+
.filter(([, text]) => String(text || "").trim())
|
|
631
|
+
.map(([key, text]) => ({
|
|
632
|
+
path: path.relative(projectRoot, changeArtifacts[`${key}Path`]) || changeArtifacts[`${key}Path`],
|
|
633
|
+
text
|
|
634
|
+
})),
|
|
635
|
+
...specRecords.map((record) => ({
|
|
636
|
+
path: record.path,
|
|
637
|
+
text: record.text
|
|
638
|
+
}))
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
result.gates.principleInheritance = collectPrincipleInheritanceGate(projectRoot, artifactEntries, strict);
|
|
642
|
+
result.gates.clarify = collectClarifyGate(artifactEntries, strict);
|
|
643
|
+
result.gates.scenarioQuality = collectScenarioQualityGate(specRecords, strict);
|
|
644
|
+
|
|
645
|
+
attachGateFindings(result, result.gates.principleInheritance);
|
|
646
|
+
attachGateFindings(result, result.gates.clarify);
|
|
647
|
+
attachGateFindings(result, result.gates.scenarioQuality);
|
|
648
|
+
|
|
355
649
|
result.summary.checked = result.specs.length;
|
|
356
650
|
result.failures = dedupe(result.failures);
|
|
357
651
|
result.warnings = dedupe(result.warnings);
|
|
358
652
|
result.notes = dedupe(result.notes);
|
|
359
653
|
result.notes.push("lint-spec uses advisory mode by default; use `--strict` to block on findings.");
|
|
360
654
|
|
|
361
|
-
return
|
|
655
|
+
return finalizePlanningResult(result);
|
|
362
656
|
}
|
|
363
657
|
|
|
364
658
|
function formatLintSpecReport(result) {
|