@osovv/vv-opencode 1.4.5 → 1.6.0
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 +23 -0
- package/README.md +47 -16
- package/dist/commands/patch-provider.d.ts +5 -5
- package/dist/commands/patch-provider.js +10 -6
- package/dist/commands/patch-provider.js.map +1 -1
- package/dist/lib/agent-tool-catalog.d.ts +144 -0
- package/dist/lib/agent-tool-catalog.js +2551 -0
- package/dist/lib/agent-tool-catalog.js.map +1 -0
- package/dist/lib/agent-tool-contract.d.ts +200 -0
- package/dist/lib/agent-tool-contract.js +407 -0
- package/dist/lib/agent-tool-contract.js.map +1 -0
- package/dist/lib/opencode/agent-registrations.d.ts +33 -0
- package/dist/lib/opencode/agent-registrations.js +469 -0
- package/dist/lib/opencode/agent-registrations.js.map +1 -0
- package/dist/lib/opencode/inspection.d.ts +84 -0
- package/dist/lib/opencode/inspection.js +367 -0
- package/dist/lib/opencode/inspection.js.map +1 -0
- package/dist/lib/opencode/model-overrides.d.ts +19 -0
- package/dist/lib/opencode/model-overrides.js +221 -0
- package/dist/lib/opencode/model-overrides.js.map +1 -0
- package/dist/lib/opencode/paths.d.ts +21 -0
- package/dist/lib/opencode/paths.js +62 -0
- package/dist/lib/opencode/paths.js.map +1 -0
- package/dist/lib/opencode/plugin-registration.d.ts +17 -0
- package/dist/lib/opencode/plugin-registration.js +210 -0
- package/dist/lib/opencode/plugin-registration.js.map +1 -0
- package/dist/lib/opencode/shared-utils.d.ts +30 -0
- package/dist/lib/opencode/shared-utils.js +183 -0
- package/dist/lib/opencode/shared-utils.js.map +1 -0
- package/dist/lib/opencode/vvoc-config-io.d.ts +9 -0
- package/dist/lib/opencode/vvoc-config-io.js +58 -0
- package/dist/lib/opencode/vvoc-config-io.js.map +1 -0
- package/dist/lib/opencode.d.ts +15 -174
- package/dist/lib/opencode.js +19 -1387
- package/dist/lib/opencode.js.map +1 -1
- package/dist/lib/spec-lint/cross-file-rules.d.ts +2 -0
- package/dist/lib/spec-lint/cross-file-rules.js +33 -0
- package/dist/lib/spec-lint/cross-file-rules.js.map +1 -0
- package/dist/lib/spec-lint/delegated-extraction.d.ts +8 -0
- package/dist/lib/spec-lint/delegated-extraction.js +112 -0
- package/dist/lib/spec-lint/delegated-extraction.js.map +1 -0
- package/dist/lib/spec-lint/lint.d.ts +7 -0
- package/dist/lib/spec-lint/lint.js +92 -0
- package/dist/lib/spec-lint/lint.js.map +1 -0
- package/dist/lib/spec-lint/parser.d.ts +101 -0
- package/dist/lib/spec-lint/parser.js +482 -0
- package/dist/lib/spec-lint/parser.js.map +1 -0
- package/dist/lib/spec-lint/plan-rules.d.ts +38 -0
- package/dist/lib/spec-lint/plan-rules.js +745 -0
- package/dist/lib/spec-lint/plan-rules.js.map +1 -0
- package/dist/lib/spec-lint/spec-rules.d.ts +8 -0
- package/dist/lib/spec-lint/spec-rules.js +188 -0
- package/dist/lib/spec-lint/spec-rules.js.map +1 -0
- package/dist/lib/spec-lint.d.ts +4 -63
- package/dist/lib/spec-lint.js +13 -1505
- package/dist/lib/spec-lint.js.map +1 -1
- package/dist/lib/vvoc-preset-registry.d.ts +7 -7
- package/dist/lib/vvoc-preset-registry.js +8 -8
- package/dist/lib/vvoc-preset-registry.js.map +1 -1
- package/dist/lib/workflow-contract.d.ts +12 -0
- package/dist/lib/workflow-contract.js +71 -18
- package/dist/lib/workflow-contract.js.map +1 -1
- package/dist/plugins/hashline-edit/index.js +102 -97
- package/dist/plugins/hashline-edit/index.js.map +1 -1
- package/dist/plugins/hashline-edit/normalize-edits.d.ts +2 -8
- package/dist/plugins/hashline-edit/normalize-edits.js +32 -17
- package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
- package/dist/plugins/hashline-edit/schemas.d.ts +246 -0
- package/dist/plugins/hashline-edit/schemas.js +659 -0
- package/dist/plugins/hashline-edit/schemas.js.map +1 -0
- package/dist/plugins/hashline-edit/str-replace-editor.d.ts +6 -17
- package/dist/plugins/hashline-edit/str-replace-editor.js +19 -21
- package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -1
- package/dist/plugins/hashline-edit/tool-description.d.ts +1 -0
- package/dist/plugins/hashline-edit/tool-description.js +20 -4
- package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
- package/dist/plugins/web-tools/fetch-service.d.ts +8 -5
- package/dist/plugins/web-tools/fetch-service.js +30 -45
- package/dist/plugins/web-tools/fetch-service.js.map +1 -1
- package/dist/plugins/web-tools/index.js +23 -5
- package/dist/plugins/web-tools/index.js.map +1 -1
- package/dist/plugins/web-tools/schemas.d.ts +469 -0
- package/dist/plugins/web-tools/schemas.js +523 -0
- package/dist/plugins/web-tools/schemas.js.map +1 -0
- package/dist/plugins/web-tools/search-service.d.ts +6 -3
- package/dist/plugins/web-tools/search-service.js +24 -29
- package/dist/plugins/web-tools/search-service.js.map +1 -1
- package/dist/plugins/workflow/authority.d.ts +6 -0
- package/dist/plugins/workflow/authority.js +45 -10
- package/dist/plugins/workflow/authority.js.map +1 -1
- package/dist/plugins/workflow/authorization.d.ts +30 -0
- package/dist/plugins/workflow/authorization.js +147 -0
- package/dist/plugins/workflow/authorization.js.map +1 -0
- package/dist/plugins/workflow/checkpoints.d.ts +41 -2
- package/dist/plugins/workflow/checkpoints.js +176 -47
- package/dist/plugins/workflow/checkpoints.js.map +1 -1
- package/dist/plugins/workflow/delegated.d.ts +15 -1
- package/dist/plugins/workflow/delegated.js +65 -35
- package/dist/plugins/workflow/delegated.js.map +1 -1
- package/dist/plugins/workflow/execution.d.ts +15 -30
- package/dist/plugins/workflow/execution.js +133 -33
- package/dist/plugins/workflow/execution.js.map +1 -1
- package/dist/plugins/workflow/index.js +197 -435
- package/dist/plugins/workflow/index.js.map +1 -1
- package/dist/plugins/workflow/input-validation.d.ts +48 -0
- package/dist/plugins/workflow/input-validation.js +536 -0
- package/dist/plugins/workflow/input-validation.js.map +1 -0
- package/dist/plugins/workflow/inspection.d.ts +60 -0
- package/dist/plugins/workflow/inspection.js +316 -0
- package/dist/plugins/workflow/inspection.js.map +1 -0
- package/dist/plugins/workflow/protocol.d.ts +2 -0
- package/dist/plugins/workflow/protocol.js +26 -4
- package/dist/plugins/workflow/protocol.js.map +1 -1
- package/dist/plugins/workflow/recovery.d.ts +18 -0
- package/dist/plugins/workflow/recovery.js +217 -0
- package/dist/plugins/workflow/recovery.js.map +1 -0
- package/dist/plugins/workflow/repair.d.ts +1 -1
- package/dist/plugins/workflow/repair.js +9 -10
- package/dist/plugins/workflow/repair.js.map +1 -1
- package/dist/plugins/workflow/results.d.ts +2566 -0
- package/dist/plugins/workflow/results.js +1111 -0
- package/dist/plugins/workflow/results.js.map +1 -0
- package/dist/plugins/workflow/schemas.d.ts +398 -0
- package/dist/plugins/workflow/schemas.js +399 -0
- package/dist/plugins/workflow/schemas.js.map +1 -0
- package/dist/plugins/workflow/state.d.ts +3 -2
- package/dist/plugins/workflow/state.js +15 -8
- package/dist/plugins/workflow/state.js.map +1 -1
- package/dist/plugins/workflow/system-instruction.md +17 -1
- package/dist/plugins/workflow/tooling.d.ts +11 -82
- package/dist/plugins/workflow/tooling.js +478 -461
- package/dist/plugins/workflow/tooling.js.map +1 -1
- package/dist/plugins/workflow/transactions.d.ts +13 -0
- package/dist/plugins/workflow/transactions.js +13 -3
- package/dist/plugins/workflow/transactions.js.map +1 -1
- package/package.json +6 -3
- package/schemas/vvoc/v3.json +1 -1
- package/templates/agents/vv-code-reviewer.md +13 -4
- package/templates/agents/vv-implementer.md +16 -9
- package/templates/agents/vv-spec-reviewer.md +13 -4
- package/templates/skills/vv-execute/SKILL.md +3 -3
- package/templates/skills/vv-execute/references/tool-contracts.md +1750 -0
- package/templates/skills/vv-review/SKILL.md +1 -1
package/dist/lib/spec-lint.js
CHANGED
|
@@ -1,1523 +1,31 @@
|
|
|
1
1
|
// FILE: src/lib/spec-lint.ts
|
|
2
|
-
// VERSION: 1.
|
|
2
|
+
// VERSION: 1.2.0
|
|
3
3
|
// START_MODULE_CONTRACT
|
|
4
|
-
// PURPOSE:
|
|
5
|
-
// SCOPE:
|
|
6
|
-
// DEPENDS: [
|
|
4
|
+
// PURPOSE: Public re-export barrel for the strict .vvoc spec-package XML linter.
|
|
5
|
+
// SCOPE: Re-export the complete previous public API over the zone modules in src/lib/spec-lint/ (parser, spec-rules, plan-rules, cross-file-rules, delegated-extraction, lint) plus the workflow-contract delegated vocabulary re-exports, so every ../lib/spec-lint.js and ./spec-lint.js import keeps resolving unchanged under moduleResolution NodeNext.
|
|
6
|
+
// DEPENDS: [src/lib/workflow-contract.ts, src/lib/spec-lint/parser.ts, src/lib/spec-lint/spec-rules.ts, src/lib/spec-lint/plan-rules.ts, src/lib/spec-lint/cross-file-rules.ts, src/lib/spec-lint/delegated-extraction.ts, src/lib/spec-lint/lint.ts]
|
|
7
7
|
// LINKS: [M-SPEC-LINT, M-WORKFLOW-CONTRACT, M-PLUGIN-SPEC-GUARD, M-CLI-COMMANDS, M-WORKFLOW-CHECKPOINTS]
|
|
8
|
-
// ROLE:
|
|
9
|
-
// MAP_MODE:
|
|
8
|
+
// ROLE: BARREL
|
|
9
|
+
// MAP_MODE: SUMMARY
|
|
10
10
|
// END_MODULE_CONTRACT
|
|
11
11
|
//
|
|
12
12
|
// START_MODULE_MAP
|
|
13
|
-
// LINT_VERSION
|
|
14
|
-
//
|
|
15
|
-
// SpecLintSeverity - Finding severity levels error and warning.
|
|
16
|
-
// SpecLintFinding - One rule violation with rule id, message, file label, and 1-based line.
|
|
17
|
-
// SpecLintVerdict - Per-artifact lint result with kind, ok flag, and findings.
|
|
18
|
-
// SpecLintArtifactInput - One artifact to lint identified by a file label and raw content.
|
|
19
|
-
// SpecLintOptions - Options for lint runs (skipCrossFile for single-file contexts).
|
|
20
|
-
// DelegatedReviewer - Canonical delegated checkpoint reviewer roles (spec, code); re-exported from the common contract.
|
|
21
|
-
// DelegatedTaskDefinition - Typed delegated obligation for one declared plan task; re-exported from the common contract.
|
|
22
|
-
// DelegatedCheckpointDefinition - Typed delegated obligation for one declared review checkpoint; re-exported from the common contract.
|
|
23
|
-
// DelegatedPlanDefinition - Typed obligations extracted from a delegated execution section; re-exported from the common contract.
|
|
24
|
-
// DelegatedPlanExtractionResult - Success payload or error list returned by delegated extraction; re-exported from the common contract.
|
|
25
|
-
// DeclaredScopePathResult - Normalized workspace-relative path or a rejection reason; re-exported from the common contract.
|
|
26
|
-
// normalizeDeclaredScopePath - Text-level canonical normalization for declared scope file paths; re-exported from the common contract.
|
|
27
|
-
// extractDelegatedPlanDefinition - Strictly extract and validate typed delegated obligations from plan content.
|
|
28
|
-
// parseSpecXml - Strict xmlMode parse producing a positioned element tree or well-formedness findings.
|
|
29
|
-
// lintSpecArtifacts - Lint a set of artifacts together, applying cross-file rules between plans and specs.
|
|
30
|
-
// detectSpecArtifactKind - Map a root element name onto an artifact kind.
|
|
31
|
-
// isSpecArchivePath - True when a file label sits inside an archive directory.
|
|
13
|
+
// LINT_VERSION, SpecLintArtifactKind, SpecLintSeverity, SpecLintFinding, SpecLintVerdict, SpecLintArtifactInput, SpecLintOptions, parseSpecXml, extractDelegatedPlanDefinition, isSpecArchivePath, detectSpecArtifactKind, lintSpecArtifacts - Public linter API owned by the zone modules.
|
|
14
|
+
// DelegatedReviewer, DelegatedTaskDefinition, DelegatedCheckpointDefinition, DelegatedPlanDefinition, DelegatedPlanExtractionResult, DeclaredScopePathResult, normalizeDeclaredScopePath - Workflow-contract vocabulary re-exports for existing linter consumers.
|
|
32
15
|
// END_MODULE_MAP
|
|
33
16
|
//
|
|
34
17
|
// START_CHANGE_SUMMARY
|
|
35
|
-
// LAST_CHANGE: [C-
|
|
18
|
+
// LAST_CHANGE: [C-SPEC-LINT-RULES-SPLIT-R1 - Replaced the 1766-line monolith body with a re-export-only barrel over zone modules; the public API and every consumer import are unchanged and LINT_VERSION stays 2.]
|
|
36
19
|
// END_CHANGE_SUMMARY
|
|
37
|
-
import { Tokenizer } from "htmlparser2";
|
|
38
20
|
import { normalizeDeclaredScopePath, } from "./workflow-contract.js";
|
|
39
|
-
// START_BLOCK_PUBLIC_TYPES
|
|
40
|
-
export const LINT_VERSION = 2;
|
|
41
|
-
const IDENTITY_PATTERNS = {
|
|
42
|
-
component: /^COMPONENT-[A-Z0-9]+(-[A-Z0-9]+)*$/,
|
|
43
|
-
task: /^TASK-T-\d{3,}$/,
|
|
44
|
-
wave: /^WAVE-\d+$/,
|
|
45
|
-
checkpoint: /^CHECKPOINT-R-\d{3,}$/,
|
|
46
|
-
};
|
|
47
|
-
const TASK_ID_REF = /^T-\d{3,}$/;
|
|
48
|
-
const DOC_STATUSES = new Set(["draft", "approved", "applied"]);
|
|
49
|
-
const TASK_STATUSES = new Set(["pending", "in_progress", "done", "skipped"]);
|
|
50
|
-
const EXECUTION_MODES = new Set(["inline", "classic", "delegated"]);
|
|
51
|
-
const CHECKPOINT_KINDS = new Set(["milestone", "final"]);
|
|
52
|
-
const DELEGATED_REVIEWERS = new Set(["spec", "code"]);
|
|
53
|
-
const RESERVED_PACKAGE_SLUGS = new Set(["draft", "archive", "template", "plan", "spec", "vvoc"]);
|
|
54
|
-
const PACKAGE_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]([a-z0-9_-]*[a-z0-9_])?$/;
|
|
55
|
-
// END_BLOCK_PARSER_TYPES
|
|
56
|
-
// START_BLOCK_LINE_INDEX
|
|
57
|
-
/** 1-based line number for a character index in source text. */
|
|
58
|
-
function lineAt(content, index) {
|
|
59
|
-
let line = 1;
|
|
60
|
-
const stop = Math.max(0, Math.min(index, content.length));
|
|
61
|
-
for (let i = 0; i < stop; i++) {
|
|
62
|
-
if (content.charCodeAt(i) === 10)
|
|
63
|
-
line++;
|
|
64
|
-
}
|
|
65
|
-
return line;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Strict XML parse over htmlparser2's xmlMode Tokenizer event stream.
|
|
69
|
-
* The low-level tokenizer is used directly (not Parser) because the Parser
|
|
70
|
-
* silently drops unmatched closing tags and implies closes at end-of-input;
|
|
71
|
-
* strict artifacts need both violation classes reported. Builds a positioned
|
|
72
|
-
* element tree and reports well-formedness violations: tokenizer errors,
|
|
73
|
-
* mismatched or unmatched closing tags, unclosed elements, stray top-level
|
|
74
|
-
* content, multiple roots, and content after the root element.
|
|
75
|
-
*/
|
|
76
|
-
export function parseSpecXml(content, file) {
|
|
77
|
-
const findings = [];
|
|
78
|
-
const rootCandidates = [];
|
|
79
|
-
const stack = [];
|
|
80
|
-
let rootClosed = false;
|
|
81
|
-
let pendingAttribNames = [];
|
|
82
|
-
let pendingAttribName = null;
|
|
83
|
-
const current = () => stack[stack.length - 1];
|
|
84
|
-
const tokenizer = new Tokenizer({ xmlMode: true, recognizeSelfClosing: true, decodeEntities: false }, {
|
|
85
|
-
onopentagname(start, endIndex) {
|
|
86
|
-
const name = content.slice(start, endIndex);
|
|
87
|
-
const line = lineAt(content, start);
|
|
88
|
-
pendingAttribNames = [];
|
|
89
|
-
pendingAttribName = null;
|
|
90
|
-
if (rootClosed) {
|
|
91
|
-
findings.push({
|
|
92
|
-
severity: "error",
|
|
93
|
-
rule: "xml.content-after-root",
|
|
94
|
-
message: `element <${name}> appears after the root element closed`,
|
|
95
|
-
file,
|
|
96
|
-
line,
|
|
97
|
-
});
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const node = {
|
|
101
|
-
name,
|
|
102
|
-
line,
|
|
103
|
-
attribs: {},
|
|
104
|
-
children: [],
|
|
105
|
-
text: "",
|
|
106
|
-
cdataCount: 0,
|
|
107
|
-
attribNames: [],
|
|
108
|
-
};
|
|
109
|
-
const parent = current();
|
|
110
|
-
if (parent) {
|
|
111
|
-
parent.children.push(node);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
rootCandidates.push(node);
|
|
115
|
-
}
|
|
116
|
-
stack.push(node);
|
|
117
|
-
},
|
|
118
|
-
onattribname(start, endIndex) {
|
|
119
|
-
pendingAttribName = content.slice(start, endIndex);
|
|
120
|
-
},
|
|
121
|
-
onattribdata() {
|
|
122
|
-
// attribute values are irrelevant: the format forbids attributes outright
|
|
123
|
-
},
|
|
124
|
-
onattribentity() {
|
|
125
|
-
// no-op: attribute values are not inspected
|
|
126
|
-
},
|
|
127
|
-
onattribend() {
|
|
128
|
-
if (pendingAttribName)
|
|
129
|
-
pendingAttribNames.push(pendingAttribName);
|
|
130
|
-
pendingAttribName = null;
|
|
131
|
-
},
|
|
132
|
-
onopentagend(endIndex) {
|
|
133
|
-
const node = current();
|
|
134
|
-
if (!node)
|
|
135
|
-
return;
|
|
136
|
-
if (pendingAttribNames.length > 0) {
|
|
137
|
-
findings.push({
|
|
138
|
-
severity: "error",
|
|
139
|
-
rule: "attr.forbidden",
|
|
140
|
-
message: `element <${node.name}> uses XML attributes (${pendingAttribNames.join(", ")}); the artifact format allows child elements only`,
|
|
141
|
-
file,
|
|
142
|
-
line: node.line,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
void endIndex;
|
|
146
|
-
},
|
|
147
|
-
onselfclosingtag() {
|
|
148
|
-
const node = current();
|
|
149
|
-
if (!node)
|
|
150
|
-
return;
|
|
151
|
-
if (pendingAttribNames.length > 0) {
|
|
152
|
-
findings.push({
|
|
153
|
-
severity: "error",
|
|
154
|
-
rule: "attr.forbidden",
|
|
155
|
-
message: `element <${node.name}> uses XML attributes (${pendingAttribNames.join(", ")}); the artifact format allows child elements only`,
|
|
156
|
-
file,
|
|
157
|
-
line: node.line,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
pendingAttribNames = [];
|
|
161
|
-
stack.pop();
|
|
162
|
-
if (stack.length === 0)
|
|
163
|
-
rootClosed = true;
|
|
164
|
-
},
|
|
165
|
-
onclosetag(start, endIndex) {
|
|
166
|
-
const name = content.slice(start, endIndex);
|
|
167
|
-
const line = lineAt(content, start);
|
|
168
|
-
const top = current();
|
|
169
|
-
if (!top) {
|
|
170
|
-
findings.push({
|
|
171
|
-
severity: "error",
|
|
172
|
-
rule: rootClosed ? "xml.content-after-root" : "xml.stray-close",
|
|
173
|
-
message: `closing tag </${name}> has no matching open element`,
|
|
174
|
-
file,
|
|
175
|
-
line,
|
|
176
|
-
});
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (top.name !== name) {
|
|
180
|
-
const namedIndex = stack.findIndex((n) => n.name === name);
|
|
181
|
-
if (namedIndex === -1) {
|
|
182
|
-
findings.push({
|
|
183
|
-
severity: "error",
|
|
184
|
-
rule: "xml.mismatched-close",
|
|
185
|
-
message: `closing tag </${name}> does not match any open element (innermost open is <${top.name}> from line ${top.line})`,
|
|
186
|
-
file,
|
|
187
|
-
line,
|
|
188
|
-
});
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
for (let i = stack.length - 1; i > namedIndex; i--) {
|
|
192
|
-
findings.push({
|
|
193
|
-
severity: "error",
|
|
194
|
-
rule: "xml.unclosed",
|
|
195
|
-
message: `element <${stack[i].name}> opened on line ${stack[i].line} is closed implicitly by </${name}> and must be closed explicitly`,
|
|
196
|
-
file,
|
|
197
|
-
line: stack[i].line,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
stack.length = namedIndex;
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
stack.pop();
|
|
204
|
-
}
|
|
205
|
-
if (stack.length === 0)
|
|
206
|
-
rootClosed = true;
|
|
207
|
-
},
|
|
208
|
-
ontext(start, endIndex) {
|
|
209
|
-
const raw = content.slice(start, endIndex);
|
|
210
|
-
const parent = current();
|
|
211
|
-
if (parent) {
|
|
212
|
-
parent.text += raw;
|
|
213
|
-
}
|
|
214
|
-
else if (raw.trim()) {
|
|
215
|
-
findings.push({
|
|
216
|
-
severity: "error",
|
|
217
|
-
rule: rootClosed ? "xml.content-after-root" : "xml.stray-text",
|
|
218
|
-
message: `text content outside the root element: ${JSON.stringify(raw.trim().slice(0, 40))}`,
|
|
219
|
-
file,
|
|
220
|
-
line: lineAt(content, start),
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
},
|
|
224
|
-
oncdata() {
|
|
225
|
-
const parent = current();
|
|
226
|
-
if (parent)
|
|
227
|
-
parent.cdataCount++;
|
|
228
|
-
},
|
|
229
|
-
oncomment() {
|
|
230
|
-
// comments are legal noise in artifacts and carry no contract meaning
|
|
231
|
-
},
|
|
232
|
-
ondeclaration() {
|
|
233
|
-
// XML declarations and DOCTYPE are tolerated without contract meaning
|
|
234
|
-
},
|
|
235
|
-
onprocessinginstruction() {
|
|
236
|
-
// processing instructions are tolerated without contract meaning
|
|
237
|
-
},
|
|
238
|
-
ontextentity() {
|
|
239
|
-
// unreachable with decodeEntities disabled; raw slices keep entities verbatim
|
|
240
|
-
},
|
|
241
|
-
onend() {
|
|
242
|
-
// required by the tokenizer interface; end-of-input handling happens after tokenizer.end() returns
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
tokenizer.write(content);
|
|
246
|
-
tokenizer.end();
|
|
247
|
-
for (const unclosed of stack) {
|
|
248
|
-
findings.push({
|
|
249
|
-
severity: "error",
|
|
250
|
-
rule: "xml.unclosed",
|
|
251
|
-
message: `element <${unclosed.name}> opened on line ${unclosed.line} is never closed`,
|
|
252
|
-
file,
|
|
253
|
-
line: unclosed.line,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
if (rootCandidates.length > 1) {
|
|
257
|
-
findings.push({
|
|
258
|
-
severity: "error",
|
|
259
|
-
rule: "xml.multiple-roots",
|
|
260
|
-
message: `document has ${rootCandidates.length} root elements; exactly one is allowed`,
|
|
261
|
-
file,
|
|
262
|
-
line: rootCandidates[1].line,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
return { root: rootCandidates[0] ?? null, findings };
|
|
266
|
-
}
|
|
267
|
-
const SPEC_CONTRACT = {
|
|
268
|
-
spec: {
|
|
269
|
-
names: [
|
|
270
|
-
"status",
|
|
271
|
-
"goal",
|
|
272
|
-
"architecture",
|
|
273
|
-
"tech_stack",
|
|
274
|
-
"components",
|
|
275
|
-
"data_flow",
|
|
276
|
-
"error_handling",
|
|
277
|
-
"testing",
|
|
278
|
-
"non_goals",
|
|
279
|
-
],
|
|
280
|
-
},
|
|
281
|
-
testing: { names: ["strategy", "coverage"] },
|
|
282
|
-
components: { identity: "component" },
|
|
283
|
-
non_goals: { names: ["non_goal"] },
|
|
284
|
-
};
|
|
285
|
-
const COMPONENT_CHILDREN = ["name", "responsibility", "depends_on"];
|
|
286
|
-
const PLAN_CONTRACT = {
|
|
287
|
-
plan: {
|
|
288
|
-
names: [
|
|
289
|
-
"spec",
|
|
290
|
-
"design_context",
|
|
291
|
-
"created",
|
|
292
|
-
"status",
|
|
293
|
-
"meta",
|
|
294
|
-
"architecture",
|
|
295
|
-
"tasks",
|
|
296
|
-
"execution",
|
|
297
|
-
],
|
|
298
|
-
},
|
|
299
|
-
meta: { names: ["summary", "waves", "affected_modules", "complexity"] },
|
|
300
|
-
architecture: { identity: "component" },
|
|
301
|
-
tasks: { identity: "wave" },
|
|
302
|
-
execution: { names: ["mode", "review_checkpoints"] },
|
|
303
|
-
review_checkpoints: { identity: "checkpoint" },
|
|
304
|
-
};
|
|
305
|
-
const TASK_CHILDREN = [
|
|
306
|
-
"title",
|
|
307
|
-
"file",
|
|
308
|
-
"status",
|
|
309
|
-
"description",
|
|
310
|
-
"depends_on",
|
|
311
|
-
"snippet",
|
|
312
|
-
"acceptance",
|
|
313
|
-
"verification",
|
|
314
|
-
"write_scope",
|
|
315
|
-
];
|
|
316
|
-
const TASK_DEPENDS_CHILDREN = ["task_id"];
|
|
317
|
-
const ACCEPTANCE_CHILDREN = ["criterion"];
|
|
318
|
-
const VERIFICATION_CHILDREN = ["command"];
|
|
319
|
-
const WAVE_CHILDREN = ["goal"];
|
|
320
|
-
const PLAN_FILE_CHILDREN = ["path", "role"];
|
|
321
|
-
const TASK_WRITE_SCOPE_CHILDREN = ["file"];
|
|
322
|
-
const CHECKPOINT_CHILDREN = [
|
|
323
|
-
"kind",
|
|
324
|
-
"after_wave",
|
|
325
|
-
"covers",
|
|
326
|
-
"scope",
|
|
327
|
-
"reviewers",
|
|
328
|
-
"acceptance",
|
|
329
|
-
"verification",
|
|
330
|
-
];
|
|
331
|
-
const CHECKPOINT_COVERS_CHILDREN = ["task_id"];
|
|
332
|
-
const CHECKPOINT_SCOPE_CHILDREN = ["file"];
|
|
333
|
-
const CHECKPOINT_REVIEWERS_CHILDREN = ["reviewer"];
|
|
334
|
-
const DESIGN_CONTEXT_CONTRACT = {
|
|
335
|
-
"design-context": {
|
|
336
|
-
names: ["decisions", "assumptions", "deferred", "scenarios", "external_constraints"],
|
|
337
|
-
},
|
|
338
|
-
decisions: { names: ["decision"] },
|
|
339
|
-
decision: { names: ["topic", "choice", "rationale", "alternatives_considered"] },
|
|
340
|
-
alternatives_considered: { names: ["alternative"] },
|
|
341
|
-
alternative: { names: ["name", "reason_rejected"] },
|
|
342
|
-
assumptions: { names: ["assumption"] },
|
|
343
|
-
assumption: { names: ["statement", "confidence", "fragile_if"] },
|
|
344
|
-
deferred: { names: ["deferred_decision"] },
|
|
345
|
-
deferred_decision: { names: ["decision", "why_deferred", "revisit_trigger"] },
|
|
346
|
-
scenarios: { names: ["scenario"] },
|
|
347
|
-
scenario: { names: ["name", "context", "implications"] },
|
|
348
|
-
external_constraints: { names: ["constraint"] },
|
|
349
|
-
constraint: { names: ["source", "impact"] },
|
|
350
|
-
};
|
|
351
|
-
// END_BLOCK_CONTRACT_TABLES
|
|
352
|
-
// START_BLOCK_LIFECYNESS_HELPER
|
|
353
|
-
/**
|
|
354
|
-
* Completeness rules apply only to known non-draft statuses. An invalid
|
|
355
|
-
* status is reported once by the vocabulary rule without cascading
|
|
356
|
-
* emptiness noise for a document that may still be mid-composition.
|
|
357
|
-
*/
|
|
358
|
-
function requiresCompleteness(root) {
|
|
359
|
-
const status = textOf(child(root, "status"));
|
|
360
|
-
return status === "approved" || status === "applied";
|
|
361
|
-
}
|
|
362
|
-
// END_BLOCK_LIFECYNESS_HELPER
|
|
363
|
-
// START_BLOCK_TREE_HELPERS
|
|
364
|
-
function child(node, name) {
|
|
365
|
-
return node.children.find((c) => c.name === name);
|
|
366
|
-
}
|
|
367
|
-
function children(node, name) {
|
|
368
|
-
return node.children.filter((c) => c.name === name);
|
|
369
|
-
}
|
|
370
|
-
function textOf(node) {
|
|
371
|
-
return node ? node.text.trim() : "";
|
|
372
|
-
}
|
|
373
|
-
function nonEmpty(node) {
|
|
374
|
-
return textOf(node) !== "";
|
|
375
|
-
}
|
|
376
|
-
// END_BLOCK_TREE_HELPERS
|
|
377
21
|
// START_BLOCK_DELEGATED_TYPES
|
|
378
22
|
// The delegated obligation vocabulary and the pure declared-path normalizer
|
|
379
23
|
// live in the dependency-free common contract so the native linter and the
|
|
380
24
|
// common runtime share one representation. These names remain re-exported here
|
|
381
25
|
// for existing linter consumers.
|
|
382
26
|
export { normalizeDeclaredScopePath, };
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
*/
|
|
388
|
-
function collectDelegatedTaskFacts(root) {
|
|
389
|
-
const waveOrder = [];
|
|
390
|
-
const tasks = [];
|
|
391
|
-
const duplicateTaskElements = [];
|
|
392
|
-
const seenTasks = new Set();
|
|
393
|
-
const tasksNode = child(root, "tasks");
|
|
394
|
-
if (!tasksNode)
|
|
395
|
-
return { waveOrder, tasks, duplicateTaskElements };
|
|
396
|
-
for (const wave of tasksNode.children) {
|
|
397
|
-
if (!IDENTITY_PATTERNS.wave.test(wave.name))
|
|
398
|
-
continue;
|
|
399
|
-
if (!waveOrder.includes(wave.name))
|
|
400
|
-
waveOrder.push(wave.name);
|
|
401
|
-
for (const task of wave.children) {
|
|
402
|
-
if (!IDENTITY_PATTERNS.task.test(task.name))
|
|
403
|
-
continue;
|
|
404
|
-
if (seenTasks.has(task.name)) {
|
|
405
|
-
duplicateTaskElements.push(task.name);
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
seenTasks.add(task.name);
|
|
409
|
-
const writeScopeNode = child(task, "write_scope");
|
|
410
|
-
const writeScope = [];
|
|
411
|
-
const writeScopeDuplicates = [];
|
|
412
|
-
if (writeScopeNode) {
|
|
413
|
-
for (const fileNode of writeScopeNode.children) {
|
|
414
|
-
if (fileNode.name !== "file")
|
|
415
|
-
continue;
|
|
416
|
-
const normalized = normalizeDeclaredScopePath(textOf(fileNode));
|
|
417
|
-
if (!normalized.ok)
|
|
418
|
-
continue;
|
|
419
|
-
if (writeScope.includes(normalized.path)) {
|
|
420
|
-
// Duplicate write-scope entries are rejected by the lint rules;
|
|
421
|
-
// here the duplicate is recorded so the rule can point at it.
|
|
422
|
-
if (!writeScopeDuplicates.includes(normalized.path)) {
|
|
423
|
-
writeScopeDuplicates.push(normalized.path);
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
writeScope.push(normalized.path);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
tasks.push({
|
|
431
|
-
taskElement: task.name,
|
|
432
|
-
taskId: task.name.slice("TASK-".length),
|
|
433
|
-
wave: wave.name,
|
|
434
|
-
line: task.line,
|
|
435
|
-
writeScope,
|
|
436
|
-
writeScopeDeclared: writeScopeNode !== undefined,
|
|
437
|
-
writeScopeDuplicates,
|
|
438
|
-
primaryFile: (() => {
|
|
439
|
-
const normalized = normalizeDeclaredScopePath(textOf(child(task, "file")));
|
|
440
|
-
return normalized.ok ? normalized.path : "";
|
|
441
|
-
})(),
|
|
442
|
-
title: textOf(child(task, "title")).trim(),
|
|
443
|
-
acceptance: (() => {
|
|
444
|
-
const acceptanceNode = child(task, "acceptance");
|
|
445
|
-
return acceptanceNode
|
|
446
|
-
? children(acceptanceNode, "criterion")
|
|
447
|
-
.map((entry) => textOf(entry).trim())
|
|
448
|
-
.filter(Boolean)
|
|
449
|
-
: [];
|
|
450
|
-
})(),
|
|
451
|
-
verification: (() => {
|
|
452
|
-
const verificationNode = child(task, "verification");
|
|
453
|
-
return verificationNode
|
|
454
|
-
? children(verificationNode, "command")
|
|
455
|
-
.map((entry) => textOf(entry).trim())
|
|
456
|
-
.filter(Boolean)
|
|
457
|
-
: [];
|
|
458
|
-
})(),
|
|
459
|
-
dependsOn: (() => {
|
|
460
|
-
const dependsNode = child(task, "depends_on");
|
|
461
|
-
return dependsNode
|
|
462
|
-
? children(dependsNode, "task_id")
|
|
463
|
-
.map((entry) => textOf(entry).trim())
|
|
464
|
-
.filter(Boolean)
|
|
465
|
-
: [];
|
|
466
|
-
})(),
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return { waveOrder, tasks, duplicateTaskElements };
|
|
471
|
-
}
|
|
472
|
-
// END_BLOCK_DELEGATED_FACTS
|
|
473
|
-
// START_BLOCK_DELEGATED_VALIDATION
|
|
474
|
-
/** Emit write-scope obligations for every declared delegated task. */
|
|
475
|
-
function lintDelegatedWriteScopes(facts, completeness, findings, file) {
|
|
476
|
-
for (const task of facts.tasks) {
|
|
477
|
-
if (task.writeScopeDuplicates.length > 0) {
|
|
478
|
-
findings.push({
|
|
479
|
-
severity: "error",
|
|
480
|
-
rule: "execution.scope_path",
|
|
481
|
-
message: `task ${task.taskId} declares write_scope path ${JSON.stringify(task.writeScopeDuplicates[0])} more than once; the runtime normalizer rejects duplicate scope entries`,
|
|
482
|
-
file,
|
|
483
|
-
line: task.line,
|
|
484
|
-
});
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
if (!task.writeScopeDeclared || task.writeScope.length === 0) {
|
|
488
|
-
findings.push({
|
|
489
|
-
severity: completeness ? "error" : "warning",
|
|
490
|
-
rule: "lifecycle.required",
|
|
491
|
-
message: `task ${task.taskId} declares no write_scope; delegated tasks must list their workspace-relative write scope`,
|
|
492
|
-
file,
|
|
493
|
-
line: task.line,
|
|
494
|
-
});
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (task.primaryFile && !task.writeScope.includes(task.primaryFile)) {
|
|
498
|
-
findings.push({
|
|
499
|
-
severity: "error",
|
|
500
|
-
rule: "execution.write_scope",
|
|
501
|
-
message: `task ${task.taskId} write_scope does not include its primary file ${task.primaryFile}`,
|
|
502
|
-
file,
|
|
503
|
-
line: task.line,
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Validate the optional <execution> section. Structural violations of declared
|
|
510
|
-
* content (unknown modes/children, duplicate identities, dangling references,
|
|
511
|
-
* future-wave coverage, multiple or misplaced finals, incomplete declared
|
|
512
|
-
* coverage) are always errors; presence obligations for approved/applied plans
|
|
513
|
-
* escalate from draft warnings.
|
|
514
|
-
*/
|
|
515
|
-
function lintExecutionSection(root, file, facts, completeness, findings) {
|
|
516
|
-
const executionNode = child(root, "execution");
|
|
517
|
-
if (!executionNode)
|
|
518
|
-
return;
|
|
519
|
-
checkChildren(executionNode, PLAN_CONTRACT.execution, file, findings);
|
|
520
|
-
const mode = textOf(child(executionNode, "mode"));
|
|
521
|
-
if (!EXECUTION_MODES.has(mode)) {
|
|
522
|
-
findings.push({
|
|
523
|
-
severity: "error",
|
|
524
|
-
rule: "execution.mode",
|
|
525
|
-
message: `execution mode "${mode}" is not one of inline, classic, delegated`,
|
|
526
|
-
file,
|
|
527
|
-
line: child(executionNode, "mode")?.line ?? executionNode.line,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
const reviewCheckpointsNode = child(executionNode, "review_checkpoints");
|
|
531
|
-
if (reviewCheckpointsNode && mode !== "delegated") {
|
|
532
|
-
findings.push({
|
|
533
|
-
severity: "error",
|
|
534
|
-
rule: "execution.checkpoints_mode",
|
|
535
|
-
message: `review_checkpoints are only valid with execution mode delegated, not "${mode}"`,
|
|
536
|
-
file,
|
|
537
|
-
line: reviewCheckpointsNode.line,
|
|
538
|
-
});
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (mode !== "delegated")
|
|
542
|
-
return;
|
|
543
|
-
if (!reviewCheckpointsNode) {
|
|
544
|
-
findings.push({
|
|
545
|
-
severity: completeness ? "error" : "warning",
|
|
546
|
-
rule: "lifecycle.required",
|
|
547
|
-
message: `delegated execution declares no review_checkpoints; the plan is ${completeness ? "approved or applied" : "draft"} and must declare its checkpoint obligations`,
|
|
548
|
-
file,
|
|
549
|
-
line: executionNode.line,
|
|
550
|
-
});
|
|
551
|
-
// Task write-scope obligations still apply even without declared checkpoints.
|
|
552
|
-
lintDelegatedWriteScopes(facts, completeness, findings, file);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
checkChildren(reviewCheckpointsNode, PLAN_CONTRACT.review_checkpoints, file, findings);
|
|
556
|
-
const obligation = (message, line, rule = "lifecycle.required") => {
|
|
557
|
-
findings.push({
|
|
558
|
-
severity: completeness ? "error" : "warning",
|
|
559
|
-
rule,
|
|
560
|
-
message,
|
|
561
|
-
file,
|
|
562
|
-
line,
|
|
563
|
-
});
|
|
564
|
-
};
|
|
565
|
-
const waveIndex = new Map(facts.waveOrder.map((wave, index) => [wave, index]));
|
|
566
|
-
const taskById = new Map(facts.tasks.map((task) => [task.taskId, task]));
|
|
567
|
-
const seenCheckpoints = new Set();
|
|
568
|
-
const validCheckpoints = [];
|
|
569
|
-
for (const checkpoint of reviewCheckpointsNode.children) {
|
|
570
|
-
if (!IDENTITY_PATTERNS.checkpoint.test(checkpoint.name)) {
|
|
571
|
-
findings.push({
|
|
572
|
-
severity: "error",
|
|
573
|
-
rule: "identity.pattern",
|
|
574
|
-
message: `checkpoint element <${checkpoint.name}> does not match the CHECKPOINT-R-NNN pattern`,
|
|
575
|
-
file,
|
|
576
|
-
line: checkpoint.line,
|
|
577
|
-
});
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
if (seenCheckpoints.has(checkpoint.name)) {
|
|
581
|
-
findings.push({
|
|
582
|
-
severity: "error",
|
|
583
|
-
rule: "identity.duplicate",
|
|
584
|
-
message: `checkpoint ${checkpoint.name} is declared more than once`,
|
|
585
|
-
file,
|
|
586
|
-
line: checkpoint.line,
|
|
587
|
-
});
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
seenCheckpoints.add(checkpoint.name);
|
|
591
|
-
checkChildren(checkpoint, { names: CHECKPOINT_CHILDREN }, file, findings);
|
|
592
|
-
const kind = textOf(child(checkpoint, "kind"));
|
|
593
|
-
if (!CHECKPOINT_KINDS.has(kind)) {
|
|
594
|
-
findings.push({
|
|
595
|
-
severity: "error",
|
|
596
|
-
rule: "execution.checkpoint_kind",
|
|
597
|
-
message: `checkpoint ${checkpoint.name} kind "${kind}" is not one of milestone, final`,
|
|
598
|
-
file,
|
|
599
|
-
line: child(checkpoint, "kind")?.line ?? checkpoint.line,
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
const afterWaveNode = child(checkpoint, "after_wave");
|
|
603
|
-
const afterWave = textOf(afterWaveNode);
|
|
604
|
-
let afterWaveKnown = false;
|
|
605
|
-
if (!IDENTITY_PATTERNS.wave.test(afterWave) || !waveIndex.has(afterWave)) {
|
|
606
|
-
findings.push({
|
|
607
|
-
severity: "error",
|
|
608
|
-
rule: "ref.dangling",
|
|
609
|
-
message: `checkpoint ${checkpoint.name} after_wave "${afterWave}" is not a declared wave in this plan`,
|
|
610
|
-
file,
|
|
611
|
-
line: afterWaveNode?.line ?? checkpoint.line,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
else {
|
|
615
|
-
afterWaveKnown = true;
|
|
616
|
-
}
|
|
617
|
-
const coversNode = child(checkpoint, "covers");
|
|
618
|
-
const covers = [];
|
|
619
|
-
if (coversNode) {
|
|
620
|
-
checkChildren(coversNode, { names: CHECKPOINT_COVERS_CHILDREN }, file, findings);
|
|
621
|
-
for (const taskRef of coversNode.children) {
|
|
622
|
-
if (taskRef.name !== "task_id")
|
|
623
|
-
continue;
|
|
624
|
-
const value = textOf(taskRef);
|
|
625
|
-
if (!TASK_ID_REF.test(value) || !taskById.has(value)) {
|
|
626
|
-
findings.push({
|
|
627
|
-
severity: "error",
|
|
628
|
-
rule: "ref.dangling",
|
|
629
|
-
message: `checkpoint ${checkpoint.name} covers task_id "${value}" which is not a declared task in this plan`,
|
|
630
|
-
file,
|
|
631
|
-
line: taskRef.line,
|
|
632
|
-
});
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
covers.push(value);
|
|
636
|
-
const coveredTask = taskById.get(value);
|
|
637
|
-
if (kind === "milestone" &&
|
|
638
|
-
afterWaveKnown &&
|
|
639
|
-
coveredTask &&
|
|
640
|
-
(waveIndex.get(coveredTask.wave) ?? -1) > (waveIndex.get(afterWave) ?? -1)) {
|
|
641
|
-
findings.push({
|
|
642
|
-
severity: "error",
|
|
643
|
-
rule: "execution.future_coverage",
|
|
644
|
-
message: `checkpoint ${checkpoint.name} (milestone after ${afterWave}) covers ${value} from a later wave ${coveredTask.wave}`,
|
|
645
|
-
file,
|
|
646
|
-
line: taskRef.line,
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
const scopeNode = child(checkpoint, "scope");
|
|
652
|
-
const scope = [];
|
|
653
|
-
if (scopeNode) {
|
|
654
|
-
checkChildren(scopeNode, { names: CHECKPOINT_SCOPE_CHILDREN }, file, findings);
|
|
655
|
-
for (const fileNode of scopeNode.children) {
|
|
656
|
-
if (fileNode.name !== "file")
|
|
657
|
-
continue;
|
|
658
|
-
const normalized = normalizeDeclaredScopePath(textOf(fileNode));
|
|
659
|
-
if (!normalized.ok) {
|
|
660
|
-
findings.push({
|
|
661
|
-
severity: "error",
|
|
662
|
-
rule: "execution.scope_path",
|
|
663
|
-
message: `checkpoint ${checkpoint.name} declares a malformed scope path (${normalized.reason}): ${JSON.stringify(textOf(fileNode))}`,
|
|
664
|
-
file,
|
|
665
|
-
line: fileNode.line,
|
|
666
|
-
});
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
if (scope.includes(normalized.path)) {
|
|
670
|
-
findings.push({
|
|
671
|
-
severity: "error",
|
|
672
|
-
rule: "execution.scope_path",
|
|
673
|
-
message: `checkpoint ${checkpoint.name} declares scope path ${JSON.stringify(normalized.path)} more than once; the runtime normalizer rejects duplicate scope entries`,
|
|
674
|
-
file,
|
|
675
|
-
line: fileNode.line,
|
|
676
|
-
});
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
scope.push(normalized.path);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const reviewersNode = child(checkpoint, "reviewers");
|
|
683
|
-
const reviewers = [];
|
|
684
|
-
if (reviewersNode) {
|
|
685
|
-
checkChildren(reviewersNode, { names: CHECKPOINT_REVIEWERS_CHILDREN }, file, findings);
|
|
686
|
-
for (const reviewerNode of reviewersNode.children) {
|
|
687
|
-
if (reviewerNode.name !== "reviewer")
|
|
688
|
-
continue;
|
|
689
|
-
const value = textOf(reviewerNode);
|
|
690
|
-
if (!DELEGATED_REVIEWERS.has(value)) {
|
|
691
|
-
findings.push({
|
|
692
|
-
severity: "error",
|
|
693
|
-
rule: "execution.reviewer",
|
|
694
|
-
message: `checkpoint ${checkpoint.name} reviewer "${value}" is not one of spec, code`,
|
|
695
|
-
file,
|
|
696
|
-
line: reviewerNode.line,
|
|
697
|
-
});
|
|
698
|
-
continue;
|
|
699
|
-
}
|
|
700
|
-
if (reviewers.includes(value)) {
|
|
701
|
-
findings.push({
|
|
702
|
-
severity: "error",
|
|
703
|
-
rule: "execution.reviewer_duplicate",
|
|
704
|
-
message: `checkpoint ${checkpoint.name} declares reviewer "${value}" more than once`,
|
|
705
|
-
file,
|
|
706
|
-
line: reviewerNode.line,
|
|
707
|
-
});
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
reviewers.push(value);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
const acceptanceNode = child(checkpoint, "acceptance");
|
|
714
|
-
if (acceptanceNode)
|
|
715
|
-
checkChildren(acceptanceNode, { names: ACCEPTANCE_CHILDREN }, file, findings);
|
|
716
|
-
const verificationNode = child(checkpoint, "verification");
|
|
717
|
-
if (verificationNode)
|
|
718
|
-
checkChildren(verificationNode, { names: VERIFICATION_CHILDREN }, file, findings);
|
|
719
|
-
if (covers.length === 0) {
|
|
720
|
-
obligation(`checkpoint ${checkpoint.name} covers no tasks; delegated checkpoints must list covered task_ids`, coversNode?.line ?? checkpoint.line);
|
|
721
|
-
}
|
|
722
|
-
if (scope.length === 0) {
|
|
723
|
-
obligation(`checkpoint ${checkpoint.name} declares an empty scope; delegated checkpoints must list reviewed files`, scopeNode?.line ?? checkpoint.line);
|
|
724
|
-
}
|
|
725
|
-
if (reviewers.length === 0) {
|
|
726
|
-
obligation(`checkpoint ${checkpoint.name} declares no reviewers; delegated checkpoints require a non-empty spec/code reviewer set`, reviewersNode?.line ?? checkpoint.line);
|
|
727
|
-
}
|
|
728
|
-
if (!acceptanceNode || children(acceptanceNode, "criterion").length === 0) {
|
|
729
|
-
obligation(`checkpoint ${checkpoint.name} has no acceptance criteria`, acceptanceNode?.line ?? checkpoint.line);
|
|
730
|
-
}
|
|
731
|
-
if (!verificationNode || children(verificationNode, "command").length === 0) {
|
|
732
|
-
obligation(`checkpoint ${checkpoint.name} has no verification commands`, verificationNode?.line ?? checkpoint.line);
|
|
733
|
-
}
|
|
734
|
-
validCheckpoints.push({
|
|
735
|
-
node: checkpoint,
|
|
736
|
-
id: checkpoint.name,
|
|
737
|
-
kind,
|
|
738
|
-
afterWave,
|
|
739
|
-
covers,
|
|
740
|
-
scope,
|
|
741
|
-
reviewers,
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
// Delegated task write-scope obligations.
|
|
745
|
-
lintDelegatedWriteScopes(facts, completeness, findings, file);
|
|
746
|
-
const finals = validCheckpoints.filter((c) => c.kind === "final");
|
|
747
|
-
if (finals.length > 1) {
|
|
748
|
-
for (const final of finals.slice(1)) {
|
|
749
|
-
findings.push({
|
|
750
|
-
severity: "error",
|
|
751
|
-
rule: "execution.final_count",
|
|
752
|
-
message: `checkpoint ${final.id} is an additional final checkpoint; exactly one final checkpoint is allowed`,
|
|
753
|
-
file,
|
|
754
|
-
line: final.node.line,
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
const declaredCheckpointCount = reviewCheckpointsNode.children.filter((c) => IDENTITY_PATTERNS.checkpoint.test(c.name)).length;
|
|
759
|
-
if (finals.length === 0 && declaredCheckpointCount > 0) {
|
|
760
|
-
obligation("delegated execution declares no final checkpoint; exactly one final checkpoint is required", reviewCheckpointsNode.line, "execution.final_count");
|
|
761
|
-
}
|
|
762
|
-
if (finals.length === 1) {
|
|
763
|
-
const finalCheckpoint = finals[0];
|
|
764
|
-
const lastWave = facts.waveOrder[facts.waveOrder.length - 1];
|
|
765
|
-
if (lastWave &&
|
|
766
|
-
finalCheckpoint.afterWave !== lastWave &&
|
|
767
|
-
waveIndex.has(finalCheckpoint.afterWave)) {
|
|
768
|
-
findings.push({
|
|
769
|
-
severity: "error",
|
|
770
|
-
rule: "execution.final_wave",
|
|
771
|
-
message: `final checkpoint ${finalCheckpoint.id} must sit after the last declared wave ${lastWave}, not ${finalCheckpoint.afterWave}`,
|
|
772
|
-
file,
|
|
773
|
-
line: finalCheckpoint.node.line,
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
for (const task of facts.tasks) {
|
|
777
|
-
if (!finalCheckpoint.covers.includes(task.taskId)) {
|
|
778
|
-
findings.push({
|
|
779
|
-
severity: "error",
|
|
780
|
-
rule: "execution.final_coverage",
|
|
781
|
-
message: `final checkpoint ${finalCheckpoint.id} does not cover task ${task.taskId}; the final checkpoint must cover every declared task`,
|
|
782
|
-
file,
|
|
783
|
-
line: finalCheckpoint.node.line,
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
const declaredWriteScopes = new Set(facts.tasks.flatMap((task) => task.writeScope));
|
|
788
|
-
for (const scopeFile of declaredWriteScopes) {
|
|
789
|
-
if (!finalCheckpoint.scope.includes(scopeFile)) {
|
|
790
|
-
findings.push({
|
|
791
|
-
severity: "error",
|
|
792
|
-
rule: "execution.final_scope",
|
|
793
|
-
message: `final checkpoint ${finalCheckpoint.id} scope does not cover declared task write scope file ${scopeFile}`,
|
|
794
|
-
file,
|
|
795
|
-
line: finalCheckpoint.node.line,
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
// END_BLOCK_DELEGATED_VALIDATION
|
|
802
|
-
// START_BLOCK_DELEGATED_EXTRACTION
|
|
803
|
-
/**
|
|
804
|
-
* Strictly extract typed delegated obligations from plan content. Returns
|
|
805
|
-
* ok:false with every validation message when the plan does not declare a
|
|
806
|
-
* structurally complete and valid delegated execution section, so runtime
|
|
807
|
-
* registration and lint share one contract instead of two interpretations.
|
|
808
|
-
*/
|
|
809
|
-
export function extractDelegatedPlanDefinition(planContent, file = "plan.xml") {
|
|
810
|
-
const parsed = parseSpecXml(planContent, file);
|
|
811
|
-
const parseErrors = parsed.findings
|
|
812
|
-
.filter((f) => f.severity === "error")
|
|
813
|
-
.map((f) => `${file}: ${f.message}`);
|
|
814
|
-
if (!parsed.root || parseErrors.length > 0) {
|
|
815
|
-
return {
|
|
816
|
-
ok: false,
|
|
817
|
-
errors: parseErrors.length > 0 ? parseErrors : [`${file}: document has no root element`],
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
if (parsed.root.name !== "plan") {
|
|
821
|
-
return { ok: false, errors: [`${file}: root element <${parsed.root.name}> is not a plan`] };
|
|
822
|
-
}
|
|
823
|
-
const facts = collectDelegatedTaskFacts(parsed.root);
|
|
824
|
-
if (facts.duplicateTaskElements.length > 0) {
|
|
825
|
-
return {
|
|
826
|
-
ok: false,
|
|
827
|
-
errors: facts.duplicateTaskElements.map((name) => `${file}: task ${name} is declared more than once`),
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
const findings = [];
|
|
831
|
-
lintExecutionSection(parsed.root, file, facts, true, findings);
|
|
832
|
-
const errors = findings.filter((f) => f.severity === "error").map((f) => `${file}: ${f.message}`);
|
|
833
|
-
if (errors.length > 0) {
|
|
834
|
-
return { ok: false, errors };
|
|
835
|
-
}
|
|
836
|
-
const executionNode = child(parsed.root, "execution");
|
|
837
|
-
if (!executionNode || textOf(child(executionNode, "mode")) !== "delegated") {
|
|
838
|
-
return { ok: false, errors: [`${file}: plan does not declare execution mode delegated`] };
|
|
839
|
-
}
|
|
840
|
-
const reviewCheckpointsNode = child(executionNode, "review_checkpoints");
|
|
841
|
-
if (!reviewCheckpointsNode) {
|
|
842
|
-
return { ok: false, errors: [`${file}: delegated execution declares no review_checkpoints`] };
|
|
843
|
-
}
|
|
844
|
-
const checkpoints = [];
|
|
845
|
-
for (const checkpoint of reviewCheckpointsNode.children) {
|
|
846
|
-
if (!IDENTITY_PATTERNS.checkpoint.test(checkpoint.name))
|
|
847
|
-
continue;
|
|
848
|
-
const fileTexts = (container) => {
|
|
849
|
-
const node = child(checkpoint, container);
|
|
850
|
-
return node ? children(node, "file").map((entry) => textOf(entry)) : [];
|
|
851
|
-
};
|
|
852
|
-
const childTexts = (container, entry) => {
|
|
853
|
-
const node = child(checkpoint, container);
|
|
854
|
-
return node ? children(node, entry).map((item) => textOf(item)) : [];
|
|
855
|
-
};
|
|
856
|
-
checkpoints.push({
|
|
857
|
-
checkpointId: checkpoint.name,
|
|
858
|
-
kind: textOf(child(checkpoint, "kind")) === "final" ? "final" : "milestone",
|
|
859
|
-
afterWave: textOf(child(checkpoint, "after_wave")),
|
|
860
|
-
covers: childTexts("covers", "task_id"),
|
|
861
|
-
scope: fileTexts("scope").map((raw) => {
|
|
862
|
-
const normalized = normalizeDeclaredScopePath(raw);
|
|
863
|
-
return normalized.ok ? normalized.path : raw;
|
|
864
|
-
}),
|
|
865
|
-
reviewers: childTexts("reviewers", "reviewer").map((value) => {
|
|
866
|
-
return value === "spec" ? "spec" : "code";
|
|
867
|
-
}),
|
|
868
|
-
acceptance: childTexts("acceptance", "criterion"),
|
|
869
|
-
verification: childTexts("verification", "command"),
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
return {
|
|
873
|
-
ok: true,
|
|
874
|
-
definition: {
|
|
875
|
-
mode: "delegated",
|
|
876
|
-
waves: facts.waveOrder,
|
|
877
|
-
tasks: facts.tasks.map((task) => ({
|
|
878
|
-
taskId: task.taskId,
|
|
879
|
-
taskElement: task.taskElement,
|
|
880
|
-
wave: task.wave,
|
|
881
|
-
writeScope: [...task.writeScope],
|
|
882
|
-
...(task.title ? { title: task.title } : {}),
|
|
883
|
-
...(task.acceptance.length > 0 ? { acceptance: [...task.acceptance] } : {}),
|
|
884
|
-
...(task.verification.length > 0 ? { verification: [...task.verification] } : {}),
|
|
885
|
-
...(task.dependsOn.length > 0 ? { dependsOn: [...task.dependsOn] } : {}),
|
|
886
|
-
})),
|
|
887
|
-
checkpoints,
|
|
888
|
-
},
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
// END_BLOCK_DELEGATED_EXTRACTION
|
|
892
|
-
// START_BLOCK_CONTRACT_CHECKS
|
|
893
|
-
/** Report children that are neither allowed fixed names nor valid identity elements. */
|
|
894
|
-
function checkChildren(node, rule, file, findings, extraAllowed = []) {
|
|
895
|
-
const allowed = new Set([...(rule?.names ?? []), ...extraAllowed]);
|
|
896
|
-
for (const c of node.children) {
|
|
897
|
-
if (allowed.has(c.name))
|
|
898
|
-
continue;
|
|
899
|
-
if (rule?.identity && IDENTITY_PATTERNS[rule.identity].test(c.name))
|
|
900
|
-
continue;
|
|
901
|
-
const expected = rule?.identity
|
|
902
|
-
? `${(rule.names ?? []).join(", ") || "(none)"}, or ${rule.identity} identity elements`
|
|
903
|
-
: (rule?.names ?? []).join(", ") || "(none)";
|
|
904
|
-
findings.push({
|
|
905
|
-
severity: "error",
|
|
906
|
-
rule: "element.unknown",
|
|
907
|
-
message: `element <${c.name}> is not part of the template contract under <${node.name}>; allowed: ${expected}`,
|
|
908
|
-
file,
|
|
909
|
-
line: c.line,
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
// END_BLOCK_CONTRACT_CHECKS
|
|
914
|
-
// START_BLOCK_LAYOUT_CHECKS
|
|
915
|
-
/** True when a file label sits under an archive/ directory. */
|
|
916
|
-
export function isSpecArchivePath(file) {
|
|
917
|
-
return /(^|\/)archive\//.test(file.replace(/\\/g, "/"));
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Package layout checks applied when the label is a specs-package artifact
|
|
921
|
-
* (.vvoc/specs/<id>/<artifact>.xml or an equivalent specs/ segment). Foreign
|
|
922
|
-
* paths (plain filenames, temp fixtures without a specs segment) are skipped.
|
|
923
|
-
*/
|
|
924
|
-
function checkPackageLayout(file, findings) {
|
|
925
|
-
const segments = file.replace(/\\/g, "/").split("/");
|
|
926
|
-
const fileName = segments[segments.length - 1];
|
|
927
|
-
if (!["spec.xml", "plan.xml", "design-context.xml"].includes(fileName))
|
|
928
|
-
return;
|
|
929
|
-
const specsIndex = segments.lastIndexOf("specs");
|
|
930
|
-
if (specsIndex === -1)
|
|
931
|
-
return;
|
|
932
|
-
const dirSegments = segments.slice(specsIndex + 1, -1).filter((s) => s !== "archive");
|
|
933
|
-
if (dirSegments.length === 0)
|
|
934
|
-
return;
|
|
935
|
-
const packageId = dirSegments[dirSegments.length - 1];
|
|
936
|
-
if (dirSegments.length > 1)
|
|
937
|
-
return; // nested dirs inside a package: skip id checks
|
|
938
|
-
if (!PACKAGE_ID_PATTERN.test(packageId)) {
|
|
939
|
-
findings.push({
|
|
940
|
-
severity: "error",
|
|
941
|
-
rule: "layout.package_id",
|
|
942
|
-
message: `package directory "${packageId}" does not match the required date-prefixed id YYYY-MM-DD-<slug> with a lowercase slug`,
|
|
943
|
-
file,
|
|
944
|
-
line: 1,
|
|
945
|
-
});
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
const slug = packageId.slice(11);
|
|
949
|
-
if (RESERVED_PACKAGE_SLUGS.has(slug)) {
|
|
950
|
-
findings.push({
|
|
951
|
-
severity: "error",
|
|
952
|
-
rule: "layout.reserved_slug",
|
|
953
|
-
message: `package slug "${slug}" is reserved`,
|
|
954
|
-
file,
|
|
955
|
-
line: 1,
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
// END_BLOCK_LAYOUT_CHECKS
|
|
960
|
-
// START_BLOCK_KIND_DETECTION
|
|
961
|
-
export function detectSpecArtifactKind(rootName) {
|
|
962
|
-
switch (rootName) {
|
|
963
|
-
case "spec":
|
|
964
|
-
return "spec";
|
|
965
|
-
case "plan":
|
|
966
|
-
return "plan";
|
|
967
|
-
case "design-context":
|
|
968
|
-
return "design-context";
|
|
969
|
-
default:
|
|
970
|
-
return "unknown";
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
function lintSpec(root, file, parseFindings) {
|
|
974
|
-
const findings = [...parseFindings];
|
|
975
|
-
checkChildren(root, SPEC_CONTRACT.spec, file, findings);
|
|
976
|
-
const completeness = requiresCompleteness(root);
|
|
977
|
-
const statusNode = child(root, "status");
|
|
978
|
-
const status = textOf(statusNode);
|
|
979
|
-
if (!DOC_STATUSES.has(status)) {
|
|
980
|
-
findings.push({
|
|
981
|
-
severity: "error",
|
|
982
|
-
rule: "lifecycle.status",
|
|
983
|
-
message: `spec status "${status}" is not one of draft, approved, applied`,
|
|
984
|
-
file,
|
|
985
|
-
line: statusNode?.line ?? root.line,
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
const componentsNode = child(root, "components");
|
|
989
|
-
if (componentsNode) {
|
|
990
|
-
checkChildren(componentsNode, SPEC_CONTRACT.components, file, findings);
|
|
991
|
-
const seen = new Set();
|
|
992
|
-
const slugs = [];
|
|
993
|
-
for (const c of componentsNode.children) {
|
|
994
|
-
if (!IDENTITY_PATTERNS.component.test(c.name)) {
|
|
995
|
-
findings.push({
|
|
996
|
-
severity: "error",
|
|
997
|
-
rule: "identity.pattern",
|
|
998
|
-
message: `component element <${c.name}> does not match the COMPONENT-UPPER-SLUG pattern (uppercase alphanumeric and hyphens, derived from the display name)`,
|
|
999
|
-
file,
|
|
1000
|
-
line: c.line,
|
|
1001
|
-
});
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
if (seen.has(c.name)) {
|
|
1005
|
-
findings.push({
|
|
1006
|
-
severity: "error",
|
|
1007
|
-
rule: "identity.duplicate",
|
|
1008
|
-
message: `component ${c.name} is declared more than once`,
|
|
1009
|
-
file,
|
|
1010
|
-
line: c.line,
|
|
1011
|
-
});
|
|
1012
|
-
continue;
|
|
1013
|
-
}
|
|
1014
|
-
seen.add(c.name);
|
|
1015
|
-
slugs.push(c.name);
|
|
1016
|
-
checkChildren(c, { names: COMPONENT_CHILDREN }, file, findings);
|
|
1017
|
-
}
|
|
1018
|
-
for (const c of componentsNode.children) {
|
|
1019
|
-
for (const dep of children(c, "depends_on")) {
|
|
1020
|
-
const ref = textOf(dep);
|
|
1021
|
-
if (!ref)
|
|
1022
|
-
continue;
|
|
1023
|
-
const target = `COMPONENT-${ref.startsWith("COMPONENT-") ? ref.slice("COMPONENT-".length) : ref}`;
|
|
1024
|
-
if (!seen.has(target)) {
|
|
1025
|
-
findings.push({
|
|
1026
|
-
severity: "error",
|
|
1027
|
-
rule: "ref.dangling",
|
|
1028
|
-
message: `depends_on references "${ref}" which is not a declared component in this document`,
|
|
1029
|
-
file,
|
|
1030
|
-
line: dep.line,
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
if (completeness) {
|
|
1036
|
-
for (const c of componentsNode.children) {
|
|
1037
|
-
if (!nonEmpty(child(c, "name"))) {
|
|
1038
|
-
findings.push({
|
|
1039
|
-
severity: "error",
|
|
1040
|
-
rule: "lifecycle.required",
|
|
1041
|
-
message: `component ${c.name} has an empty <name>; the spec is ${status} and must be complete`,
|
|
1042
|
-
file,
|
|
1043
|
-
line: c.line,
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
if (!nonEmpty(child(c, "responsibility"))) {
|
|
1047
|
-
findings.push({
|
|
1048
|
-
severity: "error",
|
|
1049
|
-
rule: "lifecycle.required",
|
|
1050
|
-
message: `component ${c.name} has an empty <responsibility>; the spec is ${status} and must be complete`,
|
|
1051
|
-
file,
|
|
1052
|
-
line: c.line,
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
const testingNode = child(root, "testing");
|
|
1059
|
-
if (testingNode)
|
|
1060
|
-
checkChildren(testingNode, SPEC_CONTRACT.testing, file, findings);
|
|
1061
|
-
const nonGoalsNode = child(root, "non_goals");
|
|
1062
|
-
if (nonGoalsNode)
|
|
1063
|
-
checkChildren(nonGoalsNode, SPEC_CONTRACT.non_goals, file, findings);
|
|
1064
|
-
if (completeness) {
|
|
1065
|
-
const required = [
|
|
1066
|
-
["goal", child(root, "goal")],
|
|
1067
|
-
["architecture", child(root, "architecture")],
|
|
1068
|
-
["tech_stack", child(root, "tech_stack")],
|
|
1069
|
-
["data_flow", child(root, "data_flow")],
|
|
1070
|
-
["error_handling", child(root, "error_handling")],
|
|
1071
|
-
["testing.strategy", testingNode ? child(testingNode, "strategy") : undefined],
|
|
1072
|
-
["testing.coverage", testingNode ? child(testingNode, "coverage") : undefined],
|
|
1073
|
-
];
|
|
1074
|
-
for (const [label, node] of required) {
|
|
1075
|
-
if (!nonEmpty(node)) {
|
|
1076
|
-
findings.push({
|
|
1077
|
-
severity: "error",
|
|
1078
|
-
rule: "lifecycle.required",
|
|
1079
|
-
message: `<${label}> is empty; the spec is ${status} and must be complete`,
|
|
1080
|
-
file,
|
|
1081
|
-
line: node?.line ?? root.line,
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
if (componentsNode && componentsNode.children.length === 0) {
|
|
1086
|
-
findings.push({
|
|
1087
|
-
severity: "error",
|
|
1088
|
-
rule: "lifecycle.required",
|
|
1089
|
-
message: `<components> declares no components; the spec is ${status} and must be complete`,
|
|
1090
|
-
file,
|
|
1091
|
-
line: componentsNode.line,
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
checkPackageLayout(file, findings);
|
|
1096
|
-
return {
|
|
1097
|
-
version: LINT_VERSION,
|
|
1098
|
-
file,
|
|
1099
|
-
kind: "spec",
|
|
1100
|
-
ok: !findings.some((f) => f.severity === "error"),
|
|
1101
|
-
findings,
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
// END_BLOCK_SPEC_RULES
|
|
1105
|
-
// START_BLOCK_PLAN_RULES
|
|
1106
|
-
function lintPlan(root, file, parseFindings, specByLabel, options) {
|
|
1107
|
-
const findings = [...parseFindings];
|
|
1108
|
-
checkChildren(root, PLAN_CONTRACT.plan, file, findings);
|
|
1109
|
-
const statusNode = child(root, "status");
|
|
1110
|
-
const status = textOf(statusNode);
|
|
1111
|
-
if (!DOC_STATUSES.has(status)) {
|
|
1112
|
-
findings.push({
|
|
1113
|
-
severity: "error",
|
|
1114
|
-
rule: "lifecycle.status",
|
|
1115
|
-
message: `plan status "${status}" is not one of draft, approved, applied`,
|
|
1116
|
-
file,
|
|
1117
|
-
line: statusNode?.line ?? root.line,
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
const completeness = requiresCompleteness(root);
|
|
1121
|
-
// Cross-file facts are resolved early so architecture depends_on may reference
|
|
1122
|
-
// spec components that the plan does not touch.
|
|
1123
|
-
const specPathNodeEarly = child(root, "spec");
|
|
1124
|
-
const specFacts = options.skipCrossFile
|
|
1125
|
-
? undefined
|
|
1126
|
-
: resolveSpecFacts(textOf(specPathNodeEarly), specByLabel);
|
|
1127
|
-
// Architecture: component identity elements mirroring the spec.
|
|
1128
|
-
const architectureNode = child(root, "architecture");
|
|
1129
|
-
const componentSlugs = [];
|
|
1130
|
-
if (architectureNode) {
|
|
1131
|
-
checkChildren(architectureNode, PLAN_CONTRACT.architecture, file, findings);
|
|
1132
|
-
const seen = new Set();
|
|
1133
|
-
for (const c of architectureNode.children) {
|
|
1134
|
-
if (!IDENTITY_PATTERNS.component.test(c.name)) {
|
|
1135
|
-
findings.push({
|
|
1136
|
-
severity: "error",
|
|
1137
|
-
rule: "identity.pattern",
|
|
1138
|
-
message: `architecture element <${c.name}> does not match the COMPONENT-UPPER-SLUG pattern mirrored from spec.xml`,
|
|
1139
|
-
file,
|
|
1140
|
-
line: c.line,
|
|
1141
|
-
});
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
if (seen.has(c.name)) {
|
|
1145
|
-
findings.push({
|
|
1146
|
-
severity: "error",
|
|
1147
|
-
rule: "identity.duplicate",
|
|
1148
|
-
message: `architecture component ${c.name} is declared more than once`,
|
|
1149
|
-
file,
|
|
1150
|
-
line: c.line,
|
|
1151
|
-
});
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
seen.add(c.name);
|
|
1155
|
-
componentSlugs.push(c.name);
|
|
1156
|
-
checkChildren(c, { names: ["name", "purpose", "file", "contract", "depends_on"] }, file, findings);
|
|
1157
|
-
const fileNode = child(c, "file");
|
|
1158
|
-
if (fileNode)
|
|
1159
|
-
checkChildren(fileNode, { names: PLAN_FILE_CHILDREN }, file, findings);
|
|
1160
|
-
}
|
|
1161
|
-
for (const c of architectureNode.children) {
|
|
1162
|
-
for (const dep of children(c, "depends_on")) {
|
|
1163
|
-
const ref = textOf(dep);
|
|
1164
|
-
if (!ref)
|
|
1165
|
-
continue;
|
|
1166
|
-
const target = ref.startsWith("COMPONENT-") ? ref : `COMPONENT-${ref}`;
|
|
1167
|
-
const knownLocally = seen.has(target);
|
|
1168
|
-
const knownInSpec = specFacts?.componentSlugs.includes(target) ?? false;
|
|
1169
|
-
// Without a resolved spec, non-local references stay unverifiable: the
|
|
1170
|
-
// crossfile.spec_missing warning already reports the skipped checks.
|
|
1171
|
-
if (specFacts && !knownLocally && !knownInSpec) {
|
|
1172
|
-
findings.push({
|
|
1173
|
-
severity: "error",
|
|
1174
|
-
rule: "ref.dangling",
|
|
1175
|
-
message: `architecture depends_on references "${ref}" which is declared neither in this plan's architecture nor in the linked spec`,
|
|
1176
|
-
file,
|
|
1177
|
-
line: dep.line,
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
// Tasks: waves with TASK-T-NNN identity elements.
|
|
1184
|
-
const tasksNode = child(root, "tasks");
|
|
1185
|
-
const taskIds = new Set();
|
|
1186
|
-
if (tasksNode) {
|
|
1187
|
-
checkChildren(tasksNode, PLAN_CONTRACT.tasks, file, findings);
|
|
1188
|
-
for (const wave of tasksNode.children) {
|
|
1189
|
-
if (!IDENTITY_PATTERNS.wave.test(wave.name)) {
|
|
1190
|
-
findings.push({
|
|
1191
|
-
severity: "error",
|
|
1192
|
-
rule: "identity.pattern",
|
|
1193
|
-
message: `wave element <${wave.name}> does not match the WAVE-N pattern`,
|
|
1194
|
-
file,
|
|
1195
|
-
line: wave.line,
|
|
1196
|
-
});
|
|
1197
|
-
continue;
|
|
1198
|
-
}
|
|
1199
|
-
checkChildren(wave, { names: WAVE_CHILDREN, identity: "task" }, file, findings);
|
|
1200
|
-
}
|
|
1201
|
-
for (const wave of tasksNode.children) {
|
|
1202
|
-
for (const task of wave.children) {
|
|
1203
|
-
if (!IDENTITY_PATTERNS.task.test(task.name)) {
|
|
1204
|
-
if (!WAVE_CHILDREN.includes(task.name)) {
|
|
1205
|
-
findings.push({
|
|
1206
|
-
severity: "error",
|
|
1207
|
-
rule: "identity.pattern",
|
|
1208
|
-
message: `task element <${task.name}> does not match the TASK-T-NNN pattern`,
|
|
1209
|
-
file,
|
|
1210
|
-
line: task.line,
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
if (taskIds.has(task.name)) {
|
|
1216
|
-
findings.push({
|
|
1217
|
-
severity: "error",
|
|
1218
|
-
rule: "identity.duplicate",
|
|
1219
|
-
message: `task ${task.name} is declared more than once`,
|
|
1220
|
-
file,
|
|
1221
|
-
line: task.line,
|
|
1222
|
-
});
|
|
1223
|
-
continue;
|
|
1224
|
-
}
|
|
1225
|
-
taskIds.add(task.name);
|
|
1226
|
-
checkChildren(task, { names: TASK_CHILDREN }, file, findings);
|
|
1227
|
-
const dependsNode = child(task, "depends_on");
|
|
1228
|
-
if (dependsNode)
|
|
1229
|
-
checkChildren(dependsNode, { names: TASK_DEPENDS_CHILDREN }, file, findings);
|
|
1230
|
-
const acceptanceNode = child(task, "acceptance");
|
|
1231
|
-
if (acceptanceNode)
|
|
1232
|
-
checkChildren(acceptanceNode, { names: ACCEPTANCE_CHILDREN }, file, findings);
|
|
1233
|
-
const verificationNode = child(task, "verification");
|
|
1234
|
-
if (verificationNode)
|
|
1235
|
-
checkChildren(verificationNode, { names: VERIFICATION_CHILDREN }, file, findings);
|
|
1236
|
-
const writeScopeNode = child(task, "write_scope");
|
|
1237
|
-
if (writeScopeNode)
|
|
1238
|
-
checkChildren(writeScopeNode, { names: TASK_WRITE_SCOPE_CHILDREN }, file, findings);
|
|
1239
|
-
const taskStatusNode = child(task, "status");
|
|
1240
|
-
const taskStatus = textOf(taskStatusNode);
|
|
1241
|
-
if (!TASK_STATUSES.has(taskStatus)) {
|
|
1242
|
-
findings.push({
|
|
1243
|
-
severity: "error",
|
|
1244
|
-
rule: "lifecycle.task_status",
|
|
1245
|
-
message: `task ${task.name} status "${taskStatus}" is not one of pending, in_progress, done, skipped`,
|
|
1246
|
-
file,
|
|
1247
|
-
line: taskStatusNode?.line ?? task.line,
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
const snippetNode = child(task, "snippet");
|
|
1251
|
-
if (snippetNode && snippetNode.text.trim() !== "" && snippetNode.cdataCount === 0) {
|
|
1252
|
-
findings.push({
|
|
1253
|
-
severity: "error",
|
|
1254
|
-
rule: "snippet.cdata",
|
|
1255
|
-
message: `task ${task.name} has a non-empty <snippet> that is not wrapped in CDATA`,
|
|
1256
|
-
file,
|
|
1257
|
-
line: snippetNode.line,
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
if (completeness) {
|
|
1261
|
-
if (!nonEmpty(child(task, "title"))) {
|
|
1262
|
-
findings.push({
|
|
1263
|
-
severity: "error",
|
|
1264
|
-
rule: "lifecycle.required",
|
|
1265
|
-
message: `task ${task.name} has an empty <title>; the plan is ${status} and must be complete`,
|
|
1266
|
-
file,
|
|
1267
|
-
line: task.line,
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
if (!nonEmpty(child(task, "file"))) {
|
|
1271
|
-
findings.push({
|
|
1272
|
-
severity: "error",
|
|
1273
|
-
rule: "lifecycle.required",
|
|
1274
|
-
message: `task ${task.name} has an empty <file>; the plan is ${status} and must be complete`,
|
|
1275
|
-
file,
|
|
1276
|
-
line: task.line,
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
if (!nonEmpty(child(task, "description"))) {
|
|
1280
|
-
findings.push({
|
|
1281
|
-
severity: "error",
|
|
1282
|
-
rule: "lifecycle.required",
|
|
1283
|
-
message: `task ${task.name} has an empty <description>; the plan is ${status} and must be complete`,
|
|
1284
|
-
file,
|
|
1285
|
-
line: task.line,
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
const acceptance = child(task, "acceptance");
|
|
1289
|
-
if (!acceptance || children(acceptance, "criterion").length === 0) {
|
|
1290
|
-
findings.push({
|
|
1291
|
-
severity: "error",
|
|
1292
|
-
rule: "lifecycle.required",
|
|
1293
|
-
message: `task ${task.name} has no acceptance criteria; the plan is ${status} and must be complete`,
|
|
1294
|
-
file,
|
|
1295
|
-
line: acceptance?.line ?? task.line,
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
const verification = child(task, "verification");
|
|
1299
|
-
if (!verification || children(verification, "command").length === 0) {
|
|
1300
|
-
findings.push({
|
|
1301
|
-
severity: "error",
|
|
1302
|
-
rule: "lifecycle.required",
|
|
1303
|
-
message: `task ${task.name} has no verification command; the plan is ${status} and must be complete`,
|
|
1304
|
-
file,
|
|
1305
|
-
line: verification?.line ?? task.line,
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
for (const wave of tasksNode.children) {
|
|
1312
|
-
for (const task of wave.children) {
|
|
1313
|
-
const dependsNode = child(task, "depends_on");
|
|
1314
|
-
if (!dependsNode)
|
|
1315
|
-
continue;
|
|
1316
|
-
for (const ref of children(dependsNode, "task_id")) {
|
|
1317
|
-
const value = textOf(ref);
|
|
1318
|
-
if (!value)
|
|
1319
|
-
continue;
|
|
1320
|
-
if (!TASK_ID_REF.test(value) || !taskIds.has(`TASK-${value}`)) {
|
|
1321
|
-
findings.push({
|
|
1322
|
-
severity: "error",
|
|
1323
|
-
rule: "ref.dangling",
|
|
1324
|
-
message: `task_id references "${value}" which is not a declared task in this plan`,
|
|
1325
|
-
file,
|
|
1326
|
-
line: ref.line,
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
if (completeness) {
|
|
1334
|
-
const metaNode = child(root, "meta");
|
|
1335
|
-
const required = metaNode
|
|
1336
|
-
? [
|
|
1337
|
-
["meta.summary", child(metaNode, "summary")],
|
|
1338
|
-
["meta.waves", child(metaNode, "waves")],
|
|
1339
|
-
["meta.affected_modules", child(metaNode, "affected_modules")],
|
|
1340
|
-
["meta.complexity", child(metaNode, "complexity")],
|
|
1341
|
-
]
|
|
1342
|
-
: [];
|
|
1343
|
-
for (const [label, node] of required) {
|
|
1344
|
-
if (!nonEmpty(node)) {
|
|
1345
|
-
findings.push({
|
|
1346
|
-
severity: "error",
|
|
1347
|
-
rule: "lifecycle.required",
|
|
1348
|
-
message: `<${label}> is empty; the plan is ${status} and must be complete`,
|
|
1349
|
-
file,
|
|
1350
|
-
line: node?.line ?? root.line,
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
if (tasksNode && tasksNode.children.length === 0) {
|
|
1355
|
-
findings.push({
|
|
1356
|
-
severity: "error",
|
|
1357
|
-
rule: "lifecycle.required",
|
|
1358
|
-
message: `<tasks> declares no waves; the plan is ${status} and must be complete`,
|
|
1359
|
-
file,
|
|
1360
|
-
line: tasksNode.line,
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
// Optional execution section: delegated/classic/inline intent and checkpoints.
|
|
1365
|
-
lintExecutionSection(root, file, collectDelegatedTaskFacts(root), completeness, findings);
|
|
1366
|
-
// Cross-file: plan components are a subset of spec components.
|
|
1367
|
-
if (!options.skipCrossFile) {
|
|
1368
|
-
const specPath = textOf(specPathNodeEarly);
|
|
1369
|
-
if (!specFacts) {
|
|
1370
|
-
findings.push({
|
|
1371
|
-
severity: "warning",
|
|
1372
|
-
rule: "crossfile.spec_missing",
|
|
1373
|
-
message: `linked spec "${specPath || "(empty)"}" was not part of this lint run; plan-subset-of-spec checks were skipped`,
|
|
1374
|
-
file,
|
|
1375
|
-
line: specPathNodeEarly?.line ?? root.line,
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
else {
|
|
1379
|
-
const specSlugs = new Set(specFacts.componentSlugs);
|
|
1380
|
-
for (const slug of componentSlugs) {
|
|
1381
|
-
if (!specSlugs.has(slug)) {
|
|
1382
|
-
const archNode = architectureNode?.children.find((c) => c.name === slug);
|
|
1383
|
-
findings.push({
|
|
1384
|
-
severity: "error",
|
|
1385
|
-
rule: "crossfile.plan_component",
|
|
1386
|
-
message: `plan architecture component ${slug} does not exist in the linked spec; plan components must be a subset of spec components`,
|
|
1387
|
-
file,
|
|
1388
|
-
line: archNode?.line ?? root.line,
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
checkPackageLayout(file, findings);
|
|
1395
|
-
return {
|
|
1396
|
-
version: LINT_VERSION,
|
|
1397
|
-
file,
|
|
1398
|
-
kind: "plan",
|
|
1399
|
-
ok: !findings.some((f) => f.severity === "error"),
|
|
1400
|
-
findings,
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
|
-
// END_BLOCK_PLAN_RULES
|
|
1404
|
-
// START_BLOCK_SPEC_FACTS
|
|
1405
|
-
function collectSpecFacts(root) {
|
|
1406
|
-
const componentsNode = child(root, "components");
|
|
1407
|
-
const componentSlugs = componentsNode
|
|
1408
|
-
? componentsNode.children
|
|
1409
|
-
.filter((c) => IDENTITY_PATTERNS.component.test(c.name))
|
|
1410
|
-
.map((c) => c.name)
|
|
1411
|
-
: [];
|
|
1412
|
-
return { componentSlugs, status: textOf(child(root, "status")) };
|
|
1413
|
-
}
|
|
1414
|
-
function resolveSpecFacts(specPath, specByLabel) {
|
|
1415
|
-
if (!specPath)
|
|
1416
|
-
return undefined;
|
|
1417
|
-
const normalized = specPath.replace(/\\/g, "/");
|
|
1418
|
-
if (specByLabel.has(normalized))
|
|
1419
|
-
return specByLabel.get(normalized);
|
|
1420
|
-
const base = normalized.split("/").pop() ?? normalized;
|
|
1421
|
-
for (const [label, facts] of specByLabel) {
|
|
1422
|
-
const labelBase = label.replace(/\\/g, "/").split("/").pop() ?? label;
|
|
1423
|
-
if (labelBase === base)
|
|
1424
|
-
return facts;
|
|
1425
|
-
}
|
|
1426
|
-
return undefined;
|
|
1427
|
-
}
|
|
1428
|
-
// END_BLOCK_SPEC_FACTS
|
|
1429
|
-
// START_BLOCK_DESIGN_CONTEXT_RULES
|
|
1430
|
-
function lintDesignContext(root, file, parseFindings) {
|
|
1431
|
-
const findings = [...parseFindings];
|
|
1432
|
-
const contract = DESIGN_CONTEXT_CONTRACT;
|
|
1433
|
-
checkChildren(root, contract["design-context"], file, findings);
|
|
1434
|
-
const walk = (node) => {
|
|
1435
|
-
const rule = contract[node.name];
|
|
1436
|
-
if (rule)
|
|
1437
|
-
checkChildren(node, rule, file, findings);
|
|
1438
|
-
for (const c of node.children)
|
|
1439
|
-
walk(c);
|
|
1440
|
-
};
|
|
1441
|
-
for (const c of root.children)
|
|
1442
|
-
walk(c);
|
|
1443
|
-
checkPackageLayout(file, findings);
|
|
1444
|
-
return {
|
|
1445
|
-
version: LINT_VERSION,
|
|
1446
|
-
file,
|
|
1447
|
-
kind: "design-context",
|
|
1448
|
-
ok: !findings.some((f) => f.severity === "error"),
|
|
1449
|
-
findings,
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
// END_BLOCK_DESIGN_CONTEXT_RULES
|
|
1453
|
-
// START_BLOCK_LINT_ENTRY
|
|
1454
|
-
/**
|
|
1455
|
-
* Lint a set of spec-package artifacts together. Plans resolve their linked
|
|
1456
|
-
* spec among the provided inputs by path or basename for cross-file subset
|
|
1457
|
-
* checks; a plan without its spec yields a warning, not an error.
|
|
1458
|
-
*/
|
|
1459
|
-
export function lintSpecArtifacts(inputs, options = {}) {
|
|
1460
|
-
// Parse everything first so spec facts exist for plan cross-file rules.
|
|
1461
|
-
const parsed = inputs.map((input) => ({ input, ...parseSpecXml(input.content, input.file) }));
|
|
1462
|
-
const specByLabel = new Map();
|
|
1463
|
-
for (const p of parsed) {
|
|
1464
|
-
if (p.root && detectSpecArtifactKind(p.root.name) === "spec") {
|
|
1465
|
-
specByLabel.set(p.input.file.replace(/\\/g, "/"), collectSpecFacts(p.root));
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
const verdicts = [];
|
|
1469
|
-
for (const p of parsed) {
|
|
1470
|
-
if (!p.root) {
|
|
1471
|
-
verdicts.push({
|
|
1472
|
-
version: LINT_VERSION,
|
|
1473
|
-
file: p.input.file,
|
|
1474
|
-
kind: "unknown",
|
|
1475
|
-
ok: false,
|
|
1476
|
-
findings: p.findings.length
|
|
1477
|
-
? p.findings
|
|
1478
|
-
: [
|
|
1479
|
-
{
|
|
1480
|
-
severity: "error",
|
|
1481
|
-
rule: "xml.empty",
|
|
1482
|
-
message: "document has no root element",
|
|
1483
|
-
file: p.input.file,
|
|
1484
|
-
line: 1,
|
|
1485
|
-
},
|
|
1486
|
-
],
|
|
1487
|
-
});
|
|
1488
|
-
continue;
|
|
1489
|
-
}
|
|
1490
|
-
const kind = detectSpecArtifactKind(p.root.name);
|
|
1491
|
-
switch (kind) {
|
|
1492
|
-
case "spec":
|
|
1493
|
-
verdicts.push(lintSpec(p.root, p.input.file, p.findings));
|
|
1494
|
-
break;
|
|
1495
|
-
case "plan":
|
|
1496
|
-
verdicts.push(lintPlan(p.root, p.input.file, p.findings, specByLabel, options));
|
|
1497
|
-
break;
|
|
1498
|
-
case "design-context":
|
|
1499
|
-
verdicts.push(lintDesignContext(p.root, p.input.file, p.findings));
|
|
1500
|
-
break;
|
|
1501
|
-
default:
|
|
1502
|
-
verdicts.push({
|
|
1503
|
-
version: LINT_VERSION,
|
|
1504
|
-
file: p.input.file,
|
|
1505
|
-
kind: "unknown",
|
|
1506
|
-
ok: false,
|
|
1507
|
-
findings: [
|
|
1508
|
-
...p.findings,
|
|
1509
|
-
{
|
|
1510
|
-
severity: "error",
|
|
1511
|
-
rule: "element.root",
|
|
1512
|
-
message: `root element <${p.root.name}> is not one of spec, plan, design-context`,
|
|
1513
|
-
file: p.input.file,
|
|
1514
|
-
line: p.root.line,
|
|
1515
|
-
},
|
|
1516
|
-
],
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
return verdicts;
|
|
1521
|
-
}
|
|
1522
|
-
// END_BLOCK_LINT_ENTRY
|
|
27
|
+
// END_BLOCK_DELEGATED_TYPES
|
|
28
|
+
export { LINT_VERSION, parseSpecXml, isSpecArchivePath, detectSpecArtifactKind, } from "./spec-lint/parser.js";
|
|
29
|
+
export { extractDelegatedPlanDefinition } from "./spec-lint/delegated-extraction.js";
|
|
30
|
+
export { lintSpecArtifacts } from "./spec-lint/lint.js";
|
|
1523
31
|
//# sourceMappingURL=spec-lint.js.map
|