@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +47 -16
  3. package/dist/commands/patch-provider.d.ts +5 -5
  4. package/dist/commands/patch-provider.js +10 -6
  5. package/dist/commands/patch-provider.js.map +1 -1
  6. package/dist/lib/agent-tool-catalog.d.ts +144 -0
  7. package/dist/lib/agent-tool-catalog.js +2551 -0
  8. package/dist/lib/agent-tool-catalog.js.map +1 -0
  9. package/dist/lib/agent-tool-contract.d.ts +200 -0
  10. package/dist/lib/agent-tool-contract.js +407 -0
  11. package/dist/lib/agent-tool-contract.js.map +1 -0
  12. package/dist/lib/opencode/agent-registrations.d.ts +33 -0
  13. package/dist/lib/opencode/agent-registrations.js +469 -0
  14. package/dist/lib/opencode/agent-registrations.js.map +1 -0
  15. package/dist/lib/opencode/inspection.d.ts +84 -0
  16. package/dist/lib/opencode/inspection.js +367 -0
  17. package/dist/lib/opencode/inspection.js.map +1 -0
  18. package/dist/lib/opencode/model-overrides.d.ts +19 -0
  19. package/dist/lib/opencode/model-overrides.js +221 -0
  20. package/dist/lib/opencode/model-overrides.js.map +1 -0
  21. package/dist/lib/opencode/paths.d.ts +21 -0
  22. package/dist/lib/opencode/paths.js +62 -0
  23. package/dist/lib/opencode/paths.js.map +1 -0
  24. package/dist/lib/opencode/plugin-registration.d.ts +17 -0
  25. package/dist/lib/opencode/plugin-registration.js +210 -0
  26. package/dist/lib/opencode/plugin-registration.js.map +1 -0
  27. package/dist/lib/opencode/shared-utils.d.ts +30 -0
  28. package/dist/lib/opencode/shared-utils.js +183 -0
  29. package/dist/lib/opencode/shared-utils.js.map +1 -0
  30. package/dist/lib/opencode/vvoc-config-io.d.ts +9 -0
  31. package/dist/lib/opencode/vvoc-config-io.js +58 -0
  32. package/dist/lib/opencode/vvoc-config-io.js.map +1 -0
  33. package/dist/lib/opencode.d.ts +15 -174
  34. package/dist/lib/opencode.js +19 -1387
  35. package/dist/lib/opencode.js.map +1 -1
  36. package/dist/lib/spec-lint/cross-file-rules.d.ts +2 -0
  37. package/dist/lib/spec-lint/cross-file-rules.js +33 -0
  38. package/dist/lib/spec-lint/cross-file-rules.js.map +1 -0
  39. package/dist/lib/spec-lint/delegated-extraction.d.ts +8 -0
  40. package/dist/lib/spec-lint/delegated-extraction.js +112 -0
  41. package/dist/lib/spec-lint/delegated-extraction.js.map +1 -0
  42. package/dist/lib/spec-lint/lint.d.ts +7 -0
  43. package/dist/lib/spec-lint/lint.js +92 -0
  44. package/dist/lib/spec-lint/lint.js.map +1 -0
  45. package/dist/lib/spec-lint/parser.d.ts +101 -0
  46. package/dist/lib/spec-lint/parser.js +482 -0
  47. package/dist/lib/spec-lint/parser.js.map +1 -0
  48. package/dist/lib/spec-lint/plan-rules.d.ts +38 -0
  49. package/dist/lib/spec-lint/plan-rules.js +745 -0
  50. package/dist/lib/spec-lint/plan-rules.js.map +1 -0
  51. package/dist/lib/spec-lint/spec-rules.d.ts +8 -0
  52. package/dist/lib/spec-lint/spec-rules.js +188 -0
  53. package/dist/lib/spec-lint/spec-rules.js.map +1 -0
  54. package/dist/lib/spec-lint.d.ts +4 -63
  55. package/dist/lib/spec-lint.js +13 -1505
  56. package/dist/lib/spec-lint.js.map +1 -1
  57. package/dist/lib/vvoc-preset-registry.d.ts +7 -7
  58. package/dist/lib/vvoc-preset-registry.js +8 -8
  59. package/dist/lib/vvoc-preset-registry.js.map +1 -1
  60. package/dist/lib/workflow-contract.d.ts +12 -0
  61. package/dist/lib/workflow-contract.js +71 -18
  62. package/dist/lib/workflow-contract.js.map +1 -1
  63. package/dist/plugins/hashline-edit/index.js +102 -97
  64. package/dist/plugins/hashline-edit/index.js.map +1 -1
  65. package/dist/plugins/hashline-edit/normalize-edits.d.ts +2 -8
  66. package/dist/plugins/hashline-edit/normalize-edits.js +32 -17
  67. package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
  68. package/dist/plugins/hashline-edit/schemas.d.ts +246 -0
  69. package/dist/plugins/hashline-edit/schemas.js +659 -0
  70. package/dist/plugins/hashline-edit/schemas.js.map +1 -0
  71. package/dist/plugins/hashline-edit/str-replace-editor.d.ts +6 -17
  72. package/dist/plugins/hashline-edit/str-replace-editor.js +19 -21
  73. package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -1
  74. package/dist/plugins/hashline-edit/tool-description.d.ts +1 -0
  75. package/dist/plugins/hashline-edit/tool-description.js +20 -4
  76. package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
  77. package/dist/plugins/web-tools/fetch-service.d.ts +8 -5
  78. package/dist/plugins/web-tools/fetch-service.js +30 -45
  79. package/dist/plugins/web-tools/fetch-service.js.map +1 -1
  80. package/dist/plugins/web-tools/index.js +23 -5
  81. package/dist/plugins/web-tools/index.js.map +1 -1
  82. package/dist/plugins/web-tools/schemas.d.ts +469 -0
  83. package/dist/plugins/web-tools/schemas.js +523 -0
  84. package/dist/plugins/web-tools/schemas.js.map +1 -0
  85. package/dist/plugins/web-tools/search-service.d.ts +6 -3
  86. package/dist/plugins/web-tools/search-service.js +24 -29
  87. package/dist/plugins/web-tools/search-service.js.map +1 -1
  88. package/dist/plugins/workflow/authority.d.ts +6 -0
  89. package/dist/plugins/workflow/authority.js +45 -10
  90. package/dist/plugins/workflow/authority.js.map +1 -1
  91. package/dist/plugins/workflow/authorization.d.ts +30 -0
  92. package/dist/plugins/workflow/authorization.js +147 -0
  93. package/dist/plugins/workflow/authorization.js.map +1 -0
  94. package/dist/plugins/workflow/checkpoints.d.ts +41 -2
  95. package/dist/plugins/workflow/checkpoints.js +176 -47
  96. package/dist/plugins/workflow/checkpoints.js.map +1 -1
  97. package/dist/plugins/workflow/delegated.d.ts +15 -1
  98. package/dist/plugins/workflow/delegated.js +65 -35
  99. package/dist/plugins/workflow/delegated.js.map +1 -1
  100. package/dist/plugins/workflow/execution.d.ts +15 -30
  101. package/dist/plugins/workflow/execution.js +133 -33
  102. package/dist/plugins/workflow/execution.js.map +1 -1
  103. package/dist/plugins/workflow/index.js +197 -435
  104. package/dist/plugins/workflow/index.js.map +1 -1
  105. package/dist/plugins/workflow/input-validation.d.ts +48 -0
  106. package/dist/plugins/workflow/input-validation.js +536 -0
  107. package/dist/plugins/workflow/input-validation.js.map +1 -0
  108. package/dist/plugins/workflow/inspection.d.ts +60 -0
  109. package/dist/plugins/workflow/inspection.js +316 -0
  110. package/dist/plugins/workflow/inspection.js.map +1 -0
  111. package/dist/plugins/workflow/protocol.d.ts +2 -0
  112. package/dist/plugins/workflow/protocol.js +26 -4
  113. package/dist/plugins/workflow/protocol.js.map +1 -1
  114. package/dist/plugins/workflow/recovery.d.ts +18 -0
  115. package/dist/plugins/workflow/recovery.js +217 -0
  116. package/dist/plugins/workflow/recovery.js.map +1 -0
  117. package/dist/plugins/workflow/repair.d.ts +1 -1
  118. package/dist/plugins/workflow/repair.js +9 -10
  119. package/dist/plugins/workflow/repair.js.map +1 -1
  120. package/dist/plugins/workflow/results.d.ts +2566 -0
  121. package/dist/plugins/workflow/results.js +1111 -0
  122. package/dist/plugins/workflow/results.js.map +1 -0
  123. package/dist/plugins/workflow/schemas.d.ts +398 -0
  124. package/dist/plugins/workflow/schemas.js +399 -0
  125. package/dist/plugins/workflow/schemas.js.map +1 -0
  126. package/dist/plugins/workflow/state.d.ts +3 -2
  127. package/dist/plugins/workflow/state.js +15 -8
  128. package/dist/plugins/workflow/state.js.map +1 -1
  129. package/dist/plugins/workflow/system-instruction.md +17 -1
  130. package/dist/plugins/workflow/tooling.d.ts +11 -82
  131. package/dist/plugins/workflow/tooling.js +478 -461
  132. package/dist/plugins/workflow/tooling.js.map +1 -1
  133. package/dist/plugins/workflow/transactions.d.ts +13 -0
  134. package/dist/plugins/workflow/transactions.js +13 -3
  135. package/dist/plugins/workflow/transactions.js.map +1 -1
  136. package/package.json +6 -3
  137. package/schemas/vvoc/v3.json +1 -1
  138. package/templates/agents/vv-code-reviewer.md +13 -4
  139. package/templates/agents/vv-implementer.md +16 -9
  140. package/templates/agents/vv-spec-reviewer.md +13 -4
  141. package/templates/skills/vv-execute/SKILL.md +3 -3
  142. package/templates/skills/vv-execute/references/tool-contracts.md +1750 -0
  143. package/templates/skills/vv-review/SKILL.md +1 -1
@@ -1,1523 +1,31 @@
1
1
  // FILE: src/lib/spec-lint.ts
2
- // VERSION: 1.1.0
2
+ // VERSION: 1.2.0
3
3
  // START_MODULE_CONTRACT
4
- // PURPOSE: Pure strict linter for .vvoc spec-package XML artifacts (spec.xml, plan.xml, design-context.xml) enforcing the element-name identity format and delegated execution/checkpoint contracts.
5
- // SCOPE: Strict well-formedness validation over the htmlparser2 xmlMode event stream, template-contract checks per artifact kind, component/task/wave/checkpoint identity rules, reference integrity, lifecycle-aware severity, plan-subset-of-spec cross-file checking, delegated execution and review-checkpoint validation with typed extraction, and package layout checks. No filesystem access — callers pass artifact contents.
6
- // DEPENDS: [htmlparser2, src/lib/workflow-contract.ts]
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: RUNTIME
9
- // MAP_MODE: EXPORTS
8
+ // ROLE: BARREL
9
+ // MAP_MODE: SUMMARY
10
10
  // END_MODULE_CONTRACT
11
11
  //
12
12
  // START_MODULE_MAP
13
- // LINT_VERSION - Rule-set version constant; cache keys include it so rule changes invalidate cached verdicts.
14
- // SpecLintArtifactKind - Artifact kinds detected by root element name.
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-WORKFLOW-PLAN-INDEPENDENCE - Delegated obligation types and declared-path normalization moved to the dependency-free M-WORKFLOW-CONTRACT and re-exported here; native extraction now lifts task title, acceptance, verification, and dependency text for the common adapter.]
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
- * Collect wave order and per-task delegated facts with a standalone walk so
385
- * lintPlan's main task validation and extractDelegatedPlanDefinition share one
386
- * interpretation of the declared task/write-scope vocabulary.
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