thoth-agents 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +21 -0
  3. package/README.md +301 -311
  4. package/dist/agents/configured-role-prompt.d.ts +9 -0
  5. package/dist/agents/prompt-sections.d.ts +8 -4
  6. package/dist/agents/prompt-utils.d.ts +0 -4
  7. package/dist/{chunk-ASKXWU6E.js → chunk-2MZ5C2RN.js} +1078 -1725
  8. package/dist/chunk-XO74DXBZ.js +1781 -0
  9. package/dist/{chunk-6BNAV534.js → chunk-YUCGIJPP.js} +12 -30
  10. package/dist/cli/claude-code-install.d.ts +24 -26
  11. package/dist/cli/claude-code-paths.d.ts +0 -28
  12. package/dist/cli/codex-install.d.ts +2 -2
  13. package/dist/cli/codex-paths.d.ts +0 -2
  14. package/dist/cli/index.js +279 -119
  15. package/dist/cli/install.d.ts +5 -1
  16. package/dist/cli/model-catalog/cache.d.ts +1 -1
  17. package/dist/cli/operations/claude-code.d.ts +6 -3
  18. package/dist/cli/operations/codex.d.ts +2 -1
  19. package/dist/cli/operations/opencode.d.ts +2 -1
  20. package/dist/cli/operations/types.d.ts +9 -1
  21. package/dist/cli/package-root.d.ts +1 -0
  22. package/dist/cli/providers.d.ts +6 -116
  23. package/dist/cli/skills.d.ts +19 -32
  24. package/dist/cli/thoth-mem-install.d.ts +45 -0
  25. package/dist/cli/tui/components/StatusView.d.ts +12 -1
  26. package/dist/cli/tui/index.js +256 -142
  27. package/dist/cli/tui/operations.d.ts +4 -3
  28. package/dist/cli/types.d.ts +0 -10
  29. package/dist/config/constants.d.ts +20 -16
  30. package/dist/config/index.d.ts +0 -1
  31. package/dist/config/loader.d.ts +1 -1
  32. package/dist/config/schema.d.ts +2 -38
  33. package/dist/config/utils.d.ts +1 -2
  34. package/dist/harness/adapters/codex-surfaces.d.ts +3 -3
  35. package/dist/harness/core/agent-pack.d.ts +37 -29
  36. package/dist/harness/core/memory-governance.d.ts +24 -48
  37. package/dist/harness/core/sdd.d.ts +364 -131
  38. package/dist/harness/generate-integration-packages.d.ts +20 -0
  39. package/dist/harness/opencode-init-command.d.ts +11 -0
  40. package/dist/harness/types.d.ts +14 -1
  41. package/dist/harness/writers/claude-code-subagent.d.ts +3 -1
  42. package/dist/hooks/index.d.ts +0 -5
  43. package/dist/index.js +562 -1314
  44. package/dist/mcp/index.d.ts +1 -2
  45. package/package.json +17 -12
  46. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +50 -0
  47. package/plugin/.claude-plugin/plugin.json +8 -0
  48. package/plugin/.codex-plugin/.thoth-agents-plugin-assets.json +16 -0
  49. package/plugin/.codex-plugin/plugin.json +7 -0
  50. package/plugin/.mcp.json +19 -0
  51. package/plugin/agents/deep.md +89 -0
  52. package/plugin/agents/designer.md +89 -0
  53. package/plugin/agents/explorer.md +90 -0
  54. package/plugin/agents/librarian.md +90 -0
  55. package/plugin/agents/oracle.md +92 -0
  56. package/plugin/agents/orchestrator.md +128 -0
  57. package/plugin/agents/quick.md +89 -0
  58. package/plugin/codex.mcp.json +17 -0
  59. package/plugin/settings.json +3 -0
  60. package/plugin/skills/thoth-archive/SKILL.md +40 -0
  61. package/plugin/skills/thoth-archive/scripts/archive.mjs +748 -0
  62. package/plugin/skills/thoth-constitution/SKILL.md +45 -0
  63. package/plugin/skills/thoth-constitution/scripts/validate.mjs +167 -0
  64. package/plugin/skills/thoth-constitution/templates/constitution.md +54 -0
  65. package/plugin/skills/thoth-init/SKILL.md +30 -0
  66. package/plugin/skills/thoth-init/scripts/init.mjs +141 -0
  67. package/plugin/skills/thoth-sdd/SKILL.md +83 -0
  68. package/plugin/skills/thoth-sdd/references/phases/analyze.md +21 -0
  69. package/plugin/skills/thoth-sdd/references/phases/checklist.md +20 -0
  70. package/plugin/skills/thoth-sdd/references/phases/clarify.md +15 -0
  71. package/plugin/skills/thoth-sdd/references/phases/converge.md +14 -0
  72. package/plugin/skills/thoth-sdd/references/phases/explore.md +10 -0
  73. package/plugin/skills/thoth-sdd/references/phases/implement.md +19 -0
  74. package/plugin/skills/thoth-sdd/references/phases/plan.md +15 -0
  75. package/plugin/skills/thoth-sdd/references/phases/specify.md +21 -0
  76. package/plugin/skills/thoth-sdd/references/phases/tasks.md +20 -0
  77. package/plugin/skills/thoth-sdd/references/phases/verify.md +22 -0
  78. package/plugin/skills/thoth-sdd/scripts/validate.mjs +1117 -0
  79. package/plugin/skills/thoth-sdd/templates/archive-report.md +25 -0
  80. package/plugin/skills/thoth-sdd/templates/checklist.md +27 -0
  81. package/plugin/skills/thoth-sdd/templates/plan.md +38 -0
  82. package/plugin/skills/thoth-sdd/templates/spec.md +54 -0
  83. package/plugin/skills/thoth-sdd/templates/tasks.md +29 -0
  84. package/plugin/skills/thoth-sdd/templates/verify-report.md +28 -0
  85. package/skills/README.md +12 -0
  86. package/skills/thoth-archive/SKILL.md +40 -0
  87. package/skills/thoth-archive/scripts/archive.mjs +748 -0
  88. package/skills/thoth-constitution/SKILL.md +45 -0
  89. package/skills/thoth-constitution/scripts/validate.mjs +167 -0
  90. package/skills/thoth-constitution/templates/constitution.md +54 -0
  91. package/skills/thoth-init/SKILL.md +30 -0
  92. package/skills/thoth-init/scripts/init.mjs +141 -0
  93. package/skills/thoth-sdd/SKILL.md +83 -0
  94. package/skills/thoth-sdd/references/phases/analyze.md +21 -0
  95. package/skills/thoth-sdd/references/phases/checklist.md +20 -0
  96. package/skills/thoth-sdd/references/phases/clarify.md +15 -0
  97. package/skills/thoth-sdd/references/phases/converge.md +14 -0
  98. package/skills/thoth-sdd/references/phases/explore.md +10 -0
  99. package/skills/thoth-sdd/references/phases/implement.md +19 -0
  100. package/skills/thoth-sdd/references/phases/plan.md +15 -0
  101. package/skills/thoth-sdd/references/phases/specify.md +21 -0
  102. package/skills/thoth-sdd/references/phases/tasks.md +20 -0
  103. package/skills/thoth-sdd/references/phases/verify.md +22 -0
  104. package/skills/thoth-sdd/scripts/validate.mjs +1117 -0
  105. package/skills/thoth-sdd/templates/archive-report.md +25 -0
  106. package/skills/thoth-sdd/templates/checklist.md +27 -0
  107. package/skills/thoth-sdd/templates/plan.md +38 -0
  108. package/skills/thoth-sdd/templates/spec.md +54 -0
  109. package/skills/thoth-sdd/templates/tasks.md +29 -0
  110. package/skills/thoth-sdd/templates/verify-report.md +28 -0
  111. package/thoth-agents.schema.json +0 -39
  112. package/dist/chunk-OZE7JVGS.js +0 -2344
  113. package/dist/cli/custom-skills.d.ts +0 -49
  114. package/dist/cli/skill-manifest.d.ts +0 -32
  115. package/dist/harness/core/skills.d.ts +0 -211
  116. package/dist/harness/generate-codex-plugin.d.ts +0 -9
  117. package/dist/harness/writers/claude-code-skill-layout.d.ts +0 -16
  118. package/dist/harness/writers/fs-skill-collect.d.ts +0 -7
  119. package/dist/harness/writers/skill-layout.d.ts +0 -16
  120. package/dist/hooks/chat-headers.d.ts +0 -16
  121. package/dist/hooks/phase-reminder/index.d.ts +0 -28
  122. package/dist/hooks/post-read-nudge/index.d.ts +0 -18
  123. package/dist/hooks/skill-sync.d.ts +0 -10
  124. package/dist/hooks/thoth-mem/index.d.ts +0 -46
  125. package/dist/hooks/thoth-mem/protocol.d.ts +0 -7
  126. package/dist/mcp/thoth.d.ts +0 -3
  127. package/dist/sdd/artifact-governance/artifact-loader.d.ts +0 -130
  128. package/dist/sdd/artifact-governance/index.d.ts +0 -6
  129. package/dist/sdd/artifact-governance/tasks-validator.d.ts +0 -17
  130. package/dist/sdd/artifact-governance/types.d.ts +0 -52
  131. package/dist/thoth/client.d.ts +0 -14
  132. package/dist/thoth/index.d.ts +0 -2
  133. package/src/skills/_shared/openspec-convention.md +0 -350
  134. package/src/skills/_shared/persistence-contract.md +0 -163
  135. package/src/skills/_shared/thoth-mem-convention.md +0 -123
  136. package/src/skills/executing-plans/SKILL.md +0 -288
  137. package/src/skills/plan-reviewer/SKILL.md +0 -219
  138. package/src/skills/requirements-interview/SKILL.md +0 -233
  139. package/src/skills/sdd-apply/SKILL.md +0 -89
  140. package/src/skills/sdd-archive/SKILL.md +0 -92
  141. package/src/skills/sdd-clarify/SKILL.md +0 -105
  142. package/src/skills/sdd-constitution/SKILL.md +0 -107
  143. package/src/skills/sdd-design/SKILL.md +0 -155
  144. package/src/skills/sdd-init/SKILL.md +0 -233
  145. package/src/skills/sdd-propose/SKILL.md +0 -101
  146. package/src/skills/sdd-spec/SKILL.md +0 -136
  147. package/src/skills/sdd-tasks/SKILL.md +0 -232
  148. package/src/skills/sdd-verify/SKILL.md +0 -131
  149. package/src/skills/thoth-mem-agents/SKILL.md +0 -210
@@ -0,0 +1,1117 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join, posix, resolve } from 'node:path';
5
+
6
+ function parseArgs(argv) {
7
+ const values = { route: 'full', through: 'ready', json: false };
8
+ for (let index = 0; index < argv.length; index += 1) {
9
+ const argument = argv[index];
10
+ if (argument === '--json') values.json = true;
11
+ else if (argument === '--change') values.change = argv[++index];
12
+ else if (argument === '--route') values.route = argv[++index];
13
+ else if (argument === '--through') values.through = argv[++index];
14
+ else throw new Error(`Unknown argument: ${argument}`);
15
+ }
16
+ if (!values.change) throw new Error('--change is required');
17
+ if (!['direct', 'accelerated', 'full'].includes(values.route)) {
18
+ throw new Error('--route must be direct, accelerated, or full');
19
+ }
20
+ if (
21
+ !['specify', 'plan', 'tasks', 'checklist', 'ready', 'closeout'].includes(
22
+ values.through,
23
+ )
24
+ ) {
25
+ throw new Error(
26
+ '--through must be specify, plan, tasks, checklist, ready, or closeout',
27
+ );
28
+ }
29
+ return values;
30
+ }
31
+
32
+ function issue(code, artifact, message) {
33
+ return { code, artifact, message };
34
+ }
35
+
36
+ function readArtifact(root, relativePath, errors, required = true) {
37
+ const path = join(root, relativePath);
38
+ if (!existsSync(path)) {
39
+ if (required) {
40
+ errors.push(
41
+ issue(
42
+ 'SDD-ARTIFACT-MISSING',
43
+ relativePath,
44
+ `Required artifact is missing: ${relativePath}`,
45
+ ),
46
+ );
47
+ }
48
+ return undefined;
49
+ }
50
+ return readFileSync(path, 'utf8');
51
+ }
52
+
53
+ function valuesFor(pattern, content) {
54
+ return [...content.matchAll(pattern)].map((match) => match[1]);
55
+ }
56
+
57
+ function sectionContent(content, heading) {
58
+ const expression = new RegExp(`^##\\s+${heading}\\s*$`, 'im');
59
+ const match = expression.exec(content);
60
+ if (!match || match.index === undefined) return undefined;
61
+ const start = match.index + match[0].length;
62
+ const remainder = content.slice(start);
63
+ const nextHeading = /^##\s+/m.exec(remainder);
64
+ return remainder.slice(0, nextHeading?.index ?? remainder.length).trim();
65
+ }
66
+
67
+ function checkUniqueSequential(ids, prefix, artifact, errors) {
68
+ if (new Set(ids).size !== ids.length) {
69
+ errors.push(
70
+ issue(
71
+ `SDD-${prefix}-UNIQUE`,
72
+ artifact,
73
+ `${prefix} identifiers must be unique.`,
74
+ ),
75
+ );
76
+ }
77
+ const numbers = ids.map((id) => Number.parseInt(id, 10));
78
+ if (numbers.some((value, index) => value !== index + 1)) {
79
+ errors.push(
80
+ issue(
81
+ `SDD-${prefix}-SEQUENCE`,
82
+ artifact,
83
+ `${prefix} identifiers must start at 001 and remain sequential.`,
84
+ ),
85
+ );
86
+ }
87
+ }
88
+
89
+ const CAPABILITY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
90
+
91
+ function parseRequirementDelta(metadata) {
92
+ if (metadata === 'INTERNAL') {
93
+ return { operation: 'INTERNAL' };
94
+ }
95
+
96
+ const change = /^(ADDED|MODIFIED|REMOVED)\s+(\S+)$/.exec(metadata);
97
+ if (change && CAPABILITY_PATTERN.test(change[2])) {
98
+ return { operation: change[1], capability: change[2] };
99
+ }
100
+
101
+ const rename = /^RENAMED\s+(\S+)\s+FROM\s+(.+)$/.exec(metadata);
102
+ if (
103
+ rename &&
104
+ CAPABILITY_PATTERN.test(rename[1]) &&
105
+ rename[2].trim().length > 0
106
+ ) {
107
+ return {
108
+ operation: 'RENAMED',
109
+ capability: rename[1],
110
+ previousTitle: rename[2].trim(),
111
+ };
112
+ }
113
+
114
+ return undefined;
115
+ }
116
+
117
+ function parseAffectedCapabilities(intentSection) {
118
+ const match = /^\*\*Affected capabilities\*\*:\s*(.+)$/im.exec(
119
+ intentSection ?? '',
120
+ );
121
+ if (!match) return undefined;
122
+ const value = match[1].replace(/<br>\s*$/i, '').trim();
123
+ if (/^None\.?$/i.test(value)) return [];
124
+ const capabilities = valuesFor(/`([^`]+)`/g, value);
125
+ return capabilities.length > 0 &&
126
+ capabilities.every((item) => CAPABILITY_PATTERN.test(item))
127
+ ? capabilities
128
+ : undefined;
129
+ }
130
+
131
+ function contractCandidateLines(content, prefix) {
132
+ const pattern = new RegExp(`^\\s*-\\s+(?:\\*\\*)?${prefix}\\b`, 'i');
133
+ return content.split(/\r?\n/).filter((line) => pattern.test(line));
134
+ }
135
+
136
+ function validateSpec(content, errors) {
137
+ const artifact = 'spec.md';
138
+ const intentSection = sectionContent(content, 'Intent and scope');
139
+ const affectedCapabilities = parseAffectedCapabilities(intentSection);
140
+ if (
141
+ intentSection === undefined ||
142
+ !/^\*\*Why\*\*:\s*\S.+$/im.test(intentSection) ||
143
+ !/^\*\*Impact\*\*:\s*\S.+$/im.test(intentSection) ||
144
+ affectedCapabilities === undefined
145
+ ) {
146
+ errors.push(
147
+ issue(
148
+ 'SDD-SPEC-INTENT',
149
+ artifact,
150
+ 'Intent and scope must define Why, Impact, and valid affected capabilities or None.',
151
+ ),
152
+ );
153
+ }
154
+
155
+ const userStoriesSection = sectionContent(content, 'User stories');
156
+ const edgeCasesSection = sectionContent(content, 'Edge cases');
157
+ const functionalRequirementsSection = sectionContent(
158
+ content,
159
+ 'Functional requirements',
160
+ );
161
+ const successCriteriaSection = sectionContent(content, 'Success criteria');
162
+ const dependenciesSection = sectionContent(content, 'Dependencies');
163
+ const missingSections = [
164
+ ['User stories', userStoriesSection],
165
+ ['Edge cases', edgeCasesSection],
166
+ ['Functional requirements', functionalRequirementsSection],
167
+ ['Success criteria', successCriteriaSection],
168
+ ['Dependencies', dependenciesSection],
169
+ ]
170
+ .filter(([, section]) => section === undefined)
171
+ .map(([heading]) => heading);
172
+ if (missingSections.length > 0) {
173
+ errors.push(
174
+ issue(
175
+ 'SDD-SPEC-SECTIONS',
176
+ artifact,
177
+ `Canonical specification sections are missing: ${missingSections.join(', ')}.`,
178
+ ),
179
+ );
180
+ }
181
+
182
+ const storyMatches = [
183
+ ...(userStoriesSection ?? '').matchAll(
184
+ /^###\s+US(\d+)\s+-.*\(Priority:\s*P\d+\)/gim,
185
+ ),
186
+ ];
187
+ const storyIds = storyMatches.map((match) => match[1]);
188
+ const storyCoverage = new Set();
189
+ if (storyIds.length === 0) {
190
+ errors.push(
191
+ issue(
192
+ 'SDD-SPEC-STORIES',
193
+ artifact,
194
+ 'Add prioritized US# stories with an independent test and acceptance scenarios.',
195
+ ),
196
+ );
197
+ } else {
198
+ checkUniqueSequential(storyIds, 'STORY', artifact, errors);
199
+ for (const [index, match] of storyMatches.entries()) {
200
+ const start = match.index ?? 0;
201
+ const next = storyMatches[index + 1]?.index;
202
+ const end = next ?? userStoriesSection.length;
203
+ const story = userStoriesSection.slice(start, end);
204
+ if (!/\*\*Independent test\*\*:\s*\S.+/i.test(story)) {
205
+ errors.push(
206
+ issue(
207
+ 'SDD-SPEC-INDEPENDENCE',
208
+ artifact,
209
+ `US${match[1]} must describe an independent test.`,
210
+ ),
211
+ );
212
+ }
213
+ const covers = /^\*\*Covers\*\*:\s*(\S.+)$/im.exec(story);
214
+ const coveredIds = covers?.[1].match(/\b(?:FR|SC)-\d{3}\b/g) ?? [];
215
+ if (
216
+ !coveredIds.some((id) => id.startsWith('FR-')) ||
217
+ !coveredIds.some((id) => id.startsWith('SC-'))
218
+ ) {
219
+ errors.push(
220
+ issue(
221
+ 'SDD-SPEC-STORY-COVERAGE',
222
+ artifact,
223
+ `US${match[1]} must map to its FR-### and SC-### contracts.`,
224
+ ),
225
+ );
226
+ }
227
+ for (const id of coveredIds) storyCoverage.add(id);
228
+ if (
229
+ !/\*\*Given\*\*[\s\S]+?\*\*When\*\*[\s\S]+?\*\*Then\*\*\s+\S/i.test(
230
+ story,
231
+ )
232
+ ) {
233
+ errors.push(
234
+ issue(
235
+ 'SDD-SPEC-ACCEPTANCE',
236
+ artifact,
237
+ `US${match[1]} must include a Given/When/Then acceptance scenario.`,
238
+ ),
239
+ );
240
+ }
241
+ }
242
+ }
243
+
244
+ const functionalRequirements = [
245
+ ...(functionalRequirementsSection ?? '').matchAll(
246
+ /^- \*\*FR-(\d{3})\s+—\s+(.+?)\*\*:\s*`\[(.+?)\]`\s+(\S.+)$/gim,
247
+ ),
248
+ ];
249
+ const successCriteria = [
250
+ ...(successCriteriaSection ?? '').matchAll(
251
+ /^- \*\*SC-(\d{3})\*\*\s+`\[(buildable|outcome)\]`:\s*(\S.+)$/gim,
252
+ ),
253
+ ];
254
+ const frIds = functionalRequirements.map((match) => match[1]);
255
+ const scIds = successCriteria.map((match) => match[1]);
256
+ const rawFrCandidates = contractCandidateLines(content, 'FR');
257
+ const rawScCandidates = contractCandidateLines(content, 'SC');
258
+ if (rawFrCandidates.length !== functionalRequirements.length) {
259
+ errors.push(
260
+ issue(
261
+ 'SDD-SPEC-FR-FORMAT',
262
+ artifact,
263
+ 'Every FR-### must use the canonical named requirement, delta metadata, and normative statement format.',
264
+ ),
265
+ );
266
+ }
267
+ if (frIds.length === 0) {
268
+ errors.push(issue('SDD-SPEC-FR', artifact, 'Add FR-### requirements.'));
269
+ } else {
270
+ checkUniqueSequential(frIds, 'FR', artifact, errors);
271
+ for (const match of functionalRequirements) {
272
+ if (!/\b(?:MUST|SHALL)\b/.test(match[4])) {
273
+ errors.push(
274
+ issue(
275
+ 'SDD-SPEC-FR-NORMATIVE',
276
+ artifact,
277
+ `FR-${match[1]} must contain an observable MUST or SHALL statement.`,
278
+ ),
279
+ );
280
+ }
281
+ const delta = parseRequirementDelta(match[3]);
282
+ if (!delta) {
283
+ errors.push(
284
+ issue(
285
+ 'SDD-SPEC-FR-DELTA',
286
+ artifact,
287
+ `FR-${match[1]} must declare INTERNAL, ADDED, MODIFIED, REMOVED, or RENAMED metadata.`,
288
+ ),
289
+ );
290
+ } else if (
291
+ delta.capability &&
292
+ !affectedCapabilities?.includes(delta.capability)
293
+ ) {
294
+ errors.push(
295
+ issue(
296
+ 'SDD-SPEC-CAPABILITY-COVERAGE',
297
+ artifact,
298
+ `FR-${match[1]} targets undeclared capability ${delta.capability}.`,
299
+ ),
300
+ );
301
+ }
302
+ }
303
+ }
304
+ if (rawScCandidates.length !== successCriteria.length) {
305
+ errors.push(
306
+ issue(
307
+ 'SDD-SPEC-SC-TYPE',
308
+ artifact,
309
+ 'Every SC-### must be classified as buildable or outcome.',
310
+ ),
311
+ );
312
+ }
313
+ if (scIds.length === 0) {
314
+ errors.push(
315
+ issue('SDD-SPEC-SC', artifact, 'Add measurable SC-### criteria.'),
316
+ );
317
+ } else {
318
+ checkUniqueSequential(scIds, 'SC', artifact, errors);
319
+ const measurable =
320
+ /(?:\b\d+(?:\.\d+)?\b|%|\b(?:all|every|none|zero|under|over|within|at least|at most|no more|fewer|pass(?:es)?|fail(?:s)?|accept(?:s)?|reject(?:s)?|preserve(?:s)?)\b)/i;
321
+ if (successCriteria.some((match) => !measurable.test(match[3]))) {
322
+ errors.push(
323
+ issue(
324
+ 'SDD-SPEC-SC-MEASURABLE',
325
+ artifact,
326
+ 'Every SC-### definition must state a structurally measurable outcome.',
327
+ ),
328
+ );
329
+ }
330
+ }
331
+ if (!/^##\s+Assumptions\s*$/im.test(content)) {
332
+ errors.push(
333
+ issue('SDD-SPEC-ASSUMPTIONS', artifact, 'Add an Assumptions section.'),
334
+ );
335
+ }
336
+ if (!/^##\s+Out of scope\s*$/im.test(content)) {
337
+ errors.push(
338
+ issue('SDD-SPEC-OUT-OF-SCOPE', artifact, 'Add an Out of scope section.'),
339
+ );
340
+ }
341
+
342
+ const knownContracts = new Set([
343
+ ...frIds.map((id) => `FR-${id}`),
344
+ ...scIds.map((id) => `SC-${id}`),
345
+ ]);
346
+ const unknownCoverage = [...storyCoverage].filter(
347
+ (id) => !knownContracts.has(id),
348
+ );
349
+ const uncoveredContracts = [...knownContracts].filter(
350
+ (id) => !storyCoverage.has(id),
351
+ );
352
+ if (unknownCoverage.length > 0 || uncoveredContracts.length > 0) {
353
+ errors.push(
354
+ issue(
355
+ 'SDD-SPEC-STORY-COVERAGE',
356
+ artifact,
357
+ `Story traceability is incomplete. Unknown: ${unknownCoverage.join(', ') || 'none'}; uncovered: ${uncoveredContracts.join(', ') || 'none'}.`,
358
+ ),
359
+ );
360
+ }
361
+
362
+ return {
363
+ storyIds,
364
+ frIds,
365
+ scIds,
366
+ buildableScIds: successCriteria
367
+ .filter((match) => match[2].toLowerCase() === 'buildable')
368
+ .map((match) => match[1]),
369
+ outcomeScIds: successCriteria
370
+ .filter((match) => match[2].toLowerCase() === 'outcome')
371
+ .map((match) => match[1]),
372
+ };
373
+ }
374
+
375
+ function constitutionEntries(content) {
376
+ if (content === undefined) return [];
377
+ return content
378
+ .split(/\r?\n/)
379
+ .filter((line) => /^- /.test(line))
380
+ .map((line) => {
381
+ const match =
382
+ /^- (?:\*\*)?(.+?)(?:\*\*)?:\s*(PASS|JUSTIFIED EXCEPTION|FAIL)\s+[—-]\s+(\S.+)$/i.exec(
383
+ line,
384
+ );
385
+ return match
386
+ ? {
387
+ name: match[1].trim(),
388
+ status: match[2].toUpperCase(),
389
+ evidence: match[3],
390
+ }
391
+ : undefined;
392
+ });
393
+ }
394
+
395
+ function validatePlan(content, errors, constitution) {
396
+ const pre = sectionContent(content, 'Constitution Check \\(pre-design\\)');
397
+ const post = sectionContent(content, 'Constitution Check \\(post-design\\)');
398
+ if (pre === undefined) {
399
+ errors.push(
400
+ issue(
401
+ 'SDD-PLAN-CONSTITUTION-PRE',
402
+ 'plan.md',
403
+ 'Record the pre-design Constitution Check.',
404
+ ),
405
+ );
406
+ }
407
+ if (post === undefined) {
408
+ errors.push(
409
+ issue(
410
+ 'SDD-PLAN-CONSTITUTION-POST',
411
+ 'plan.md',
412
+ 'Record the post-design Constitution Check.',
413
+ ),
414
+ );
415
+ }
416
+
417
+ const preEntries = constitutionEntries(pre);
418
+ const postEntries = constitutionEntries(post);
419
+ if (
420
+ (pre !== undefined &&
421
+ (preEntries.length === 0 ||
422
+ preEntries.some(
423
+ (entry) => !entry || !isConcreteEvidence(entry.evidence),
424
+ ))) ||
425
+ (post !== undefined &&
426
+ (postEntries.length === 0 ||
427
+ postEntries.some(
428
+ (entry) => !entry || !isConcreteEvidence(entry.evidence),
429
+ )))
430
+ ) {
431
+ errors.push(
432
+ issue(
433
+ 'SDD-PLAN-CONSTITUTION-EVIDENCE',
434
+ 'plan.md',
435
+ 'Every Constitution Check entry must include a status and concrete evidence.',
436
+ ),
437
+ );
438
+ }
439
+
440
+ const preNames = preEntries
441
+ .filter(Boolean)
442
+ .map(({ name }) => name.toLowerCase());
443
+ const postNames = postEntries
444
+ .filter(Boolean)
445
+ .map(({ name }) => name.toLowerCase());
446
+ const expectedNames = constitution
447
+ ? valuesFor(/^###\s+(?:\d+|[IVXLCDM]+)\.\s+(.+)$/gim, constitution).map(
448
+ (name) => name.trim().toLowerCase(),
449
+ )
450
+ : [];
451
+ const coverage = expectedNames.length > 0 ? expectedNames : preNames;
452
+ if (
453
+ coverage.some(
454
+ (name) => !preNames.includes(name) || !postNames.includes(name),
455
+ ) ||
456
+ preNames.some((name) => !postNames.includes(name)) ||
457
+ postNames.some((name) => !preNames.includes(name))
458
+ ) {
459
+ errors.push(
460
+ issue(
461
+ 'SDD-PLAN-CONSTITUTION-COVERAGE',
462
+ 'plan.md',
463
+ 'Pre-design and post-design checks must cover the same active Constitution principles.',
464
+ ),
465
+ );
466
+ }
467
+ if (
468
+ [...preEntries, ...postEntries]
469
+ .filter(Boolean)
470
+ .some(({ status }) => status === 'FAIL')
471
+ ) {
472
+ errors.push(
473
+ issue(
474
+ 'SDD-PLAN-CONSTITUTION-FAIL',
475
+ 'plan.md',
476
+ 'A FAIL Constitution status blocks downstream task generation.',
477
+ ),
478
+ );
479
+ }
480
+ }
481
+
482
+ function exactTaskPath(line) {
483
+ const description = line.split(' | Verify:', 1)[0];
484
+ const spans = [...description.matchAll(/`([^`\r\n]+)`/g)];
485
+ if (spans.length !== 1) return undefined;
486
+
487
+ const rawPath = spans[0][1].trim().replaceAll('\\', '/');
488
+ if (
489
+ rawPath.length === 0 ||
490
+ /^\[.*\]$/.test(rawPath) ||
491
+ /[*?[\]{}]/.test(rawPath) ||
492
+ rawPath.startsWith('!') ||
493
+ rawPath === '~' ||
494
+ rawPath.startsWith('~/') ||
495
+ rawPath.startsWith('/') ||
496
+ /^[a-z][a-z0-9+.-]*:/i.test(rawPath)
497
+ ) {
498
+ return undefined;
499
+ }
500
+
501
+ const normalized = posix.normalize(rawPath);
502
+ if (
503
+ normalized === '.' ||
504
+ normalized === '..' ||
505
+ normalized.startsWith('../')
506
+ ) {
507
+ return undefined;
508
+ }
509
+ return normalized.toLowerCase();
510
+ }
511
+
512
+ function validateTasks(content, errors, spec) {
513
+ const artifact = 'tasks.md';
514
+ const taskLines = content
515
+ .split(/\r?\n/)
516
+ .filter((line) => /^- \[[ x~]\]/.test(line));
517
+ const taskShape =
518
+ /^- \[[ x~]\] T(\d{3})(?: \[P\])?(?: \[US(\d+)\])? .+`[^`\r\n]+`(?: \| Verify: \S.+)?\s*$/;
519
+ if (
520
+ taskLines.length === 0 ||
521
+ taskLines.some((line) => !taskShape.test(line) || !exactTaskPath(line))
522
+ ) {
523
+ errors.push(
524
+ issue(
525
+ 'SDD-TASK-FORMAT',
526
+ artifact,
527
+ 'Every task must use T### [P?] [US#?], a description, and exactly one literal repository-relative path.',
528
+ ),
529
+ );
530
+ }
531
+ const parsedTasks = taskLines
532
+ .map((line) => ({ line, match: taskShape.exec(line) }))
533
+ .filter(({ match }) => match !== null);
534
+ if (parsedTasks.length > 0) {
535
+ const taskIds = parsedTasks.map(({ match }) => match[1]);
536
+ checkUniqueSequential(taskIds, 'TASK', artifact, errors);
537
+ }
538
+ if (
539
+ taskLines.length > 0 &&
540
+ taskLines.some((line) => !/ \| Verify: \S.+\s*$/.test(line))
541
+ ) {
542
+ errors.push(
543
+ issue(
544
+ 'SDD-TASK-VERIFICATION',
545
+ artifact,
546
+ 'Every task must state an observable Verify outcome.',
547
+ ),
548
+ );
549
+ }
550
+
551
+ if (spec) {
552
+ const storyTags = new Set(
553
+ parsedTasks
554
+ .map(({ match }) => match[2])
555
+ .filter((value) => value !== undefined),
556
+ );
557
+ const missing = [
558
+ ...spec.storyIds
559
+ .filter((id) => !storyTags.has(String(Number.parseInt(id, 10))))
560
+ .map((id) => `US${Number.parseInt(id, 10)}`),
561
+ ...spec.frIds
562
+ .filter(
563
+ (id) => !new RegExp(`\\bFR-${id}\\b`).test(taskLines.join('\n')),
564
+ )
565
+ .map((id) => `FR-${id}`),
566
+ ...spec.buildableScIds
567
+ .filter(
568
+ (id) => !new RegExp(`\\bSC-${id}\\b`).test(taskLines.join('\n')),
569
+ )
570
+ .map((id) => `SC-${id}`),
571
+ ];
572
+ const unknownStories = [...storyTags]
573
+ .filter(
574
+ (id) =>
575
+ !spec.storyIds.some(
576
+ (storyId) =>
577
+ Number.parseInt(storyId, 10) === Number.parseInt(id, 10),
578
+ ),
579
+ )
580
+ .map((id) => `US${id}`);
581
+ if (missing.length > 0 || unknownStories.length > 0) {
582
+ errors.push(
583
+ issue(
584
+ 'SDD-TASK-COVERAGE',
585
+ artifact,
586
+ `Task coverage is incomplete. Missing: ${missing.join(', ') || 'none'}; unknown stories: ${unknownStories.join(', ') || 'none'}.`,
587
+ ),
588
+ );
589
+ }
590
+ }
591
+ if (!/^##\s+MVP scope\s*$/im.test(content)) {
592
+ errors.push(
593
+ issue(
594
+ 'SDD-TASK-MVP',
595
+ artifact,
596
+ 'Identify the independently testable MVP.',
597
+ ),
598
+ );
599
+ }
600
+ if (!/^##\s+Dependencies\s*$/im.test(content)) {
601
+ errors.push(
602
+ issue('SDD-TASK-DEPENDENCIES', artifact, 'Document task dependencies.'),
603
+ );
604
+ }
605
+ const parallelSection = sectionContent(
606
+ content,
607
+ 'Parallel execution(?: examples)?',
608
+ );
609
+ const taskRecords = parsedTasks.map(({ line, match }) => ({
610
+ id: `T${match[1]}`,
611
+ parallel: /\[P\]/.test(line),
612
+ path: exactTaskPath(line),
613
+ }));
614
+ const tasksById = new Map(taskRecords.map((task) => [task.id, task]));
615
+ const hasParallelTask = taskRecords.some((task) => task.parallel);
616
+ const parallelLines = (parallelSection ?? '')
617
+ .split(/\r?\n/)
618
+ .map((line) => [line, [...new Set(line.match(/\bT\d{3}\b/g) ?? [])]])
619
+ .filter(([, ids]) => ids.length >= 2);
620
+ const referencedTaskIds = new Set(parallelLines.flatMap(([, ids]) => ids));
621
+ const hasParallelExample = parallelLines.length > 0;
622
+ const noParallelMatch = /^-\s+None:\s*(\S.*)$/im.exec(parallelSection ?? '');
623
+ const explainsNoParallelWork =
624
+ noParallelMatch !== null && isConcreteEvidence(noParallelMatch[1]);
625
+ const pathsOverlap = (left, right) =>
626
+ left === right ||
627
+ left.startsWith(`${right}/`) ||
628
+ right.startsWith(`${left}/`);
629
+ const unknownReferences = [...referencedTaskIds].filter(
630
+ (id) => !tasksById.has(id),
631
+ );
632
+ const omittedParallelTasks = taskRecords
633
+ .filter((task) => task.parallel && !referencedTaskIds.has(task.id))
634
+ .map((task) => task.id);
635
+ const examplesWithoutParallelTask = parallelLines.some(
636
+ ([, ids]) => !ids.some((id) => tasksById.get(id)?.parallel),
637
+ );
638
+ const overlappingExamples = parallelLines.some(([, ids]) => {
639
+ const paths = ids.map((id) => tasksById.get(id)?.path);
640
+ if (paths.some((path) => path === undefined)) return true;
641
+ return paths.some((path, index) =>
642
+ paths.slice(index + 1).some((other) => pathsOverlap(path, other)),
643
+ );
644
+ });
645
+ if (
646
+ parallelSection === undefined ||
647
+ (explainsNoParallelWork && hasParallelExample) ||
648
+ (hasParallelTask &&
649
+ (!hasParallelExample ||
650
+ explainsNoParallelWork ||
651
+ unknownReferences.length > 0 ||
652
+ omittedParallelTasks.length > 0 ||
653
+ examplesWithoutParallelTask ||
654
+ overlappingExamples)) ||
655
+ (!hasParallelTask && !explainsNoParallelWork)
656
+ ) {
657
+ errors.push(
658
+ issue(
659
+ 'SDD-TASK-PARALLEL',
660
+ artifact,
661
+ 'Document existing [P] tasks with non-overlapping paths, or explain why no safe parallel work exists.',
662
+ ),
663
+ );
664
+ }
665
+ }
666
+
667
+ function validateChecklist(content, errors, spec, requireComplete) {
668
+ const artifact = 'checklists/requirements.md';
669
+ const activationMatch = /^\*\*Activation reason\*\*:\s*(\S.*)$/im.exec(
670
+ content,
671
+ );
672
+ if (activationMatch === null || !isConcreteEvidence(activationMatch[1])) {
673
+ errors.push(
674
+ issue(
675
+ 'SDD-CHECKLIST-ACTIVATION',
676
+ artifact,
677
+ 'Record the concrete risk or ambiguity that activated the checklist.',
678
+ ),
679
+ );
680
+ }
681
+ const checklistLines = content
682
+ .split(/\r?\n/)
683
+ .filter((line) => /^- \[[ x~]\]/.test(line));
684
+ const checklistShape = /^- \[([ x~])\] CHK(\d{3})\b.+$/i;
685
+ const parsed = checklistLines
686
+ .map((line) => checklistShape.exec(line))
687
+ .filter((match) => match !== null);
688
+ if (
689
+ checklistLines.length === 0 ||
690
+ checklistLines.some((line) => !checklistShape.test(line))
691
+ ) {
692
+ errors.push(
693
+ issue(
694
+ 'SDD-CHECKLIST-ID',
695
+ artifact,
696
+ 'Checklist items must use CHK### IDs.',
697
+ ),
698
+ );
699
+ }
700
+ if (parsed.length > 0) {
701
+ checkUniqueSequential(
702
+ parsed.map((match) => match[2]),
703
+ 'CHECKLIST',
704
+ artifact,
705
+ errors,
706
+ );
707
+ }
708
+ if (requireComplete && parsed.some((match) => match[1] !== 'x')) {
709
+ errors.push(
710
+ issue(
711
+ 'SDD-CHECKLIST-INCOMPLETE',
712
+ artifact,
713
+ 'Final validation requires every checklist item to be complete.',
714
+ ),
715
+ );
716
+ }
717
+ const initial = sectionContent(content, 'Initial validation');
718
+ const domainLenses = sectionContent(content, 'Domain lenses');
719
+ const revalidation = sectionContent(content, 'Revalidation');
720
+ const noopRevalidationMatch =
721
+ revalidation === undefined
722
+ ? null
723
+ : /^-\s+Not required:\s*(\S.*)$/im.exec(revalidation);
724
+ const explicitNoopRevalidation =
725
+ noopRevalidationMatch !== null &&
726
+ isConcreteEvidence(noopRevalidationMatch[1]);
727
+ const hasRevalidationItems =
728
+ revalidation !== undefined &&
729
+ /^- \[[ x~]\] CHK\d{3}\b/im.test(revalidation);
730
+ const hasDomainItems =
731
+ domainLenses !== undefined &&
732
+ /^- \[[ x~]\] CHK\d{3}\b/im.test(domainLenses);
733
+ const noDomainLensesMatch =
734
+ domainLenses === undefined
735
+ ? null
736
+ : /^-\s+None:\s*(\S.*)$/im.exec(domainLenses);
737
+ const explicitNoDomainLenses =
738
+ noDomainLensesMatch !== null && isConcreteEvidence(noDomainLensesMatch[1]);
739
+ const missingDimensions = [
740
+ 'Completeness',
741
+ 'Clarity',
742
+ 'Consistency',
743
+ 'Measurability',
744
+ 'Coverage',
745
+ ].filter(
746
+ (dimension) => !new RegExp(`\\[${dimension}\\]`, 'i').test(initial ?? ''),
747
+ );
748
+ if (missingDimensions.length > 0) {
749
+ errors.push(
750
+ issue(
751
+ 'SDD-CHECKLIST-TAXONOMY',
752
+ artifact,
753
+ `Missing requirement-quality dimensions: ${missingDimensions.join(', ')}.`,
754
+ ),
755
+ );
756
+ }
757
+ if (
758
+ domainLenses === undefined ||
759
+ (!hasDomainItems && !explicitNoDomainLenses) ||
760
+ (hasDomainItems && explicitNoDomainLenses)
761
+ ) {
762
+ errors.push(
763
+ issue(
764
+ 'SDD-CHECKLIST-DOMAIN-LENSES',
765
+ artifact,
766
+ 'Add applicable domain-lens checks or an evidence-backed None decision.',
767
+ ),
768
+ );
769
+ }
770
+ if (
771
+ initial === undefined ||
772
+ revalidation === undefined ||
773
+ (!hasRevalidationItems && !explicitNoopRevalidation) ||
774
+ (hasRevalidationItems && explicitNoopRevalidation)
775
+ ) {
776
+ errors.push(
777
+ issue(
778
+ 'SDD-CHECKLIST-REVALIDATION',
779
+ artifact,
780
+ 'Record initial validation plus checked revalidation or an evidence-backed no-op.',
781
+ ),
782
+ );
783
+ }
784
+ if (spec) {
785
+ const missing = [
786
+ ...spec.storyIds
787
+ .filter(
788
+ (id) =>
789
+ !new RegExp(`\\bUS${Number.parseInt(id, 10)}\\b`).test(content),
790
+ )
791
+ .map((id) => `US${Number.parseInt(id, 10)}`),
792
+ ...spec.frIds
793
+ .filter((id) => !new RegExp(`\\bFR-${id}\\b`).test(content))
794
+ .map((id) => `FR-${id}`),
795
+ ...spec.scIds
796
+ .filter((id) => !new RegExp(`\\bSC-${id}\\b`).test(content))
797
+ .map((id) => `SC-${id}`),
798
+ ];
799
+ if (missing.length > 0) {
800
+ errors.push(
801
+ issue(
802
+ 'SDD-CHECKLIST-COVERAGE',
803
+ artifact,
804
+ `Checklist coverage is missing: ${missing.join(', ')}.`,
805
+ ),
806
+ );
807
+ }
808
+ }
809
+ }
810
+
811
+ function validateCloseoutTasks(content, errors) {
812
+ if (/^- \[(?: |~)\] T\d{3}\b/m.test(content)) {
813
+ errors.push(
814
+ issue(
815
+ 'SDD-CLOSEOUT-TASKS',
816
+ 'tasks.md',
817
+ 'Closeout requires every task to be complete.',
818
+ ),
819
+ );
820
+ }
821
+ }
822
+
823
+ function parseComplianceMatrix(content) {
824
+ const matrix = sectionContent(content, 'Compliance matrix') ?? '';
825
+ const candidates = matrix
826
+ .split(/\r?\n/)
827
+ .filter((line) => /^\s*\|\s*(?:FR|SC)-/i.test(line));
828
+ const entries = [
829
+ ...matrix.matchAll(
830
+ /^\|\s*((?:FR|SC)-\d{3})(?:\s+`?\[(?:buildable|outcome)\]`?)?\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(PASS|RISK|FAIL)\s*\|\s*$/gim,
831
+ ),
832
+ ].map((match) => ({
833
+ id: match[1].toUpperCase(),
834
+ evidence: match[2],
835
+ check: match[3],
836
+ result: match[4].toUpperCase(),
837
+ }));
838
+ return { entries, malformed: candidates.length !== entries.length };
839
+ }
840
+
841
+ function isConcreteEvidence(value) {
842
+ const normalized = value.replaceAll('`', '').trim();
843
+ return (
844
+ normalized.length > 0 &&
845
+ !/^\[.*\]$/.test(normalized) &&
846
+ !/^(?:N\/?A|NONE|PENDING|TBD|NOT YET|-)[.!]?$/i.test(normalized)
847
+ );
848
+ }
849
+
850
+ function hasExplicitResidualRisk(content, id) {
851
+ const residualRisks = sectionContent(content, 'Residual risks') ?? '';
852
+ const match = new RegExp(`^-\\s+${id}\\s*:\\s*(\\S.*)$`, 'im').exec(
853
+ residualRisks,
854
+ );
855
+ return match !== null && isConcreteEvidence(match[1]);
856
+ }
857
+
858
+ function hasBlockingCriticalFinding(content) {
859
+ const findings = sectionContent(content, 'Findings') ?? '';
860
+ return findings.split(/\r?\n/).some((line) => {
861
+ if (!/\bCRITICAL\b/i.test(line)) return false;
862
+ if (/\bRESOLVED\b/i.test(line)) return false;
863
+ return !/\b(?:NO|ZERO)\s+(?:OPEN\s+)?CRITICAL\b/i.test(line);
864
+ });
865
+ }
866
+
867
+ function hasConcreteReviewDimensions(content) {
868
+ const dimensions = sectionContent(content, 'Review dimensions') ?? '';
869
+ return ['Completeness', 'Correctness', 'Coherence'].every((dimension) => {
870
+ const match = new RegExp(
871
+ `^-\\s*\\*\\*${dimension}\\*\\*:\\s*(\\S.*)$`,
872
+ 'im',
873
+ ).exec(dimensions);
874
+ return match !== null && isConcreteEvidence(match[1]);
875
+ });
876
+ }
877
+
878
+ function validateVerifyReport(content, errors, spec) {
879
+ const artifact = 'verify-report.md';
880
+ if (
881
+ !/^\*\*Reviewer\*\*:\s*oracle(?:<br>)?\s*$/im.test(content) ||
882
+ !/^\*\*Independent from implementer\*\*:\s*Yes(?:<br>)?\s*$/im.test(content)
883
+ ) {
884
+ errors.push(
885
+ issue(
886
+ 'SDD-VERIFY-INDEPENDENCE',
887
+ artifact,
888
+ 'Closeout requires an oracle reviewer independent from the implementer.',
889
+ ),
890
+ );
891
+ }
892
+ if (!/^\*\*Verdict\*\*:\s*PASS\s*$/im.test(content)) {
893
+ errors.push(
894
+ issue(
895
+ 'SDD-VERIFY-VERDICT',
896
+ artifact,
897
+ 'Closeout requires an explicit PASS verdict.',
898
+ ),
899
+ );
900
+ }
901
+ if (hasBlockingCriticalFinding(content)) {
902
+ errors.push(
903
+ issue(
904
+ 'SDD-VERIFY-CRITICAL',
905
+ artifact,
906
+ 'Unresolved CRITICAL verification findings block closeout.',
907
+ ),
908
+ );
909
+ }
910
+
911
+ if (!hasConcreteReviewDimensions(content)) {
912
+ errors.push(
913
+ issue(
914
+ 'SDD-VERIFY-DIMENSIONS',
915
+ artifact,
916
+ 'Oracle must judge completeness, correctness, and coherence separately.',
917
+ ),
918
+ );
919
+ }
920
+
921
+ const matrix = parseComplianceMatrix(content);
922
+ const knownIds = new Set([
923
+ ...spec.frIds.map((id) => `FR-${id}`),
924
+ ...spec.scIds.map((id) => `SC-${id}`),
925
+ ]);
926
+ const rowIds = matrix.entries.map(({ id }) => id);
927
+ const duplicateIds = rowIds.filter(
928
+ (id, index) => rowIds.indexOf(id) !== index,
929
+ );
930
+ const unknownIds = rowIds.filter((id) => !knownIds.has(id));
931
+ const failedIds = matrix.entries
932
+ .filter(({ result }) => result === 'FAIL')
933
+ .map(({ id }) => id);
934
+ if (
935
+ matrix.malformed ||
936
+ duplicateIds.length > 0 ||
937
+ unknownIds.length > 0 ||
938
+ failedIds.length > 0
939
+ ) {
940
+ errors.push(
941
+ issue(
942
+ 'SDD-VERIFY-MATRIX',
943
+ artifact,
944
+ `Compliance matrix rows must be canonical, unique, known, and non-FAIL. Duplicates: ${[...new Set(duplicateIds)].join(', ') || 'none'}; unknown: ${[...new Set(unknownIds)].join(', ') || 'none'}; failed: ${[...new Set(failedIds)].join(', ') || 'none'}.`,
945
+ ),
946
+ );
947
+ }
948
+ const rows = new Map(matrix.entries.map((entry) => [entry.id, entry]));
949
+ const required = [
950
+ ...spec.frIds.map((id) => `FR-${id}`),
951
+ ...spec.buildableScIds.map((id) => `SC-${id}`),
952
+ ];
953
+ const missing = required.filter((id) => {
954
+ const row = rows.get(id);
955
+ return (
956
+ row?.result !== 'PASS' ||
957
+ !isConcreteEvidence(row.evidence) ||
958
+ !isConcreteEvidence(row.check)
959
+ );
960
+ });
961
+ if (missing.length > 0) {
962
+ errors.push(
963
+ issue(
964
+ 'SDD-VERIFY-COVERAGE',
965
+ artifact,
966
+ `Verification evidence is missing for: ${missing.join(', ')}.`,
967
+ ),
968
+ );
969
+ }
970
+
971
+ const unresolvedOutcomes = spec.outcomeScIds
972
+ .map((id) => `SC-${id}`)
973
+ .filter((id) => {
974
+ const row = rows.get(id);
975
+ if (!row) return true;
976
+ if (row.result === 'PASS') {
977
+ return (
978
+ !isConcreteEvidence(row.evidence) || !isConcreteEvidence(row.check)
979
+ );
980
+ }
981
+ return row.result !== 'RISK' || !hasExplicitResidualRisk(content, id);
982
+ });
983
+ if (unresolvedOutcomes.length > 0) {
984
+ errors.push(
985
+ issue(
986
+ 'SDD-VERIFY-OUTCOME',
987
+ artifact,
988
+ `Outcome criteria require observed PASS evidence or an explicit residual RISK: ${unresolvedOutcomes.join(', ')}.`,
989
+ ),
990
+ );
991
+ }
992
+ }
993
+
994
+ function validateArchiveReport(content, errors) {
995
+ const artifact = 'archive-report.md';
996
+ const valid =
997
+ /^\*\*Status\*\*:\s*READY(?:<br>)?\s*$/im.test(content) &&
998
+ /^\*\*Oracle verdict\*\*:\s*PASS(?:<br>)?\s*$/im.test(content) &&
999
+ /^\*\*Archive path\*\*:\s*`openspec\/changes\/archive\/YYYY-MM-DD-\[feature\]\/`(?:<br>)?\s*$/im.test(
1000
+ content,
1001
+ ) &&
1002
+ /verify-report\.md/i.test(
1003
+ sectionContent(content, 'Verification lineage') ?? '',
1004
+ ) &&
1005
+ /^- Pending: archive applies declared durable deltas transactionally\.\s*$/im.test(
1006
+ sectionContent(content, 'Canonical specification sync') ?? '',
1007
+ );
1008
+ if (!valid) {
1009
+ errors.push(
1010
+ issue(
1011
+ 'SDD-ARCHIVE-REPORT',
1012
+ artifact,
1013
+ 'Archive report must be READY with oracle PASS, lineage, sync status, and the dated target placeholder.',
1014
+ ),
1015
+ );
1016
+ }
1017
+ }
1018
+
1019
+ function validate({ change, route, through }) {
1020
+ const root = resolve(change);
1021
+ const errors = [];
1022
+ const warnings = [];
1023
+
1024
+ if (route === 'direct') {
1025
+ return { valid: true, route, through, changeRoot: root, errors, warnings };
1026
+ }
1027
+
1028
+ const spec = readArtifact(root, 'spec.md', errors);
1029
+ const requiresPlan = [
1030
+ 'plan',
1031
+ 'checklist',
1032
+ 'tasks',
1033
+ 'ready',
1034
+ 'closeout',
1035
+ ].includes(through);
1036
+ const requiresTasks = ['tasks', 'ready', 'closeout'].includes(through);
1037
+ const plan = requiresPlan ? readArtifact(root, 'plan.md', errors) : undefined;
1038
+ const tasks = requiresTasks
1039
+ ? readArtifact(root, 'tasks.md', errors)
1040
+ : undefined;
1041
+ const checklist = readArtifact(
1042
+ root,
1043
+ 'checklists/requirements.md',
1044
+ errors,
1045
+ through === 'checklist',
1046
+ );
1047
+
1048
+ const specContract =
1049
+ spec === undefined ? undefined : validateSpec(spec, errors);
1050
+ const constitution = readArtifact(
1051
+ join(root, '..', '..'),
1052
+ 'memory/constitution.md',
1053
+ errors,
1054
+ requiresPlan,
1055
+ );
1056
+ if (plan !== undefined) validatePlan(plan, errors, constitution);
1057
+ if (tasks !== undefined) validateTasks(tasks, errors, specContract);
1058
+ if (checklist !== undefined) {
1059
+ validateChecklist(
1060
+ checklist,
1061
+ errors,
1062
+ specContract,
1063
+ ['checklist', 'ready', 'closeout'].includes(through),
1064
+ );
1065
+ } else if (route === 'full' && ['ready', 'closeout'].includes(through)) {
1066
+ warnings.push({
1067
+ code: 'SDD-CHECKLIST-CONDITIONAL',
1068
+ artifact: 'checklists/requirements.md',
1069
+ message:
1070
+ 'No requirements checklist exists; confirm that risk did not activate the conditional checklist gate.',
1071
+ });
1072
+ }
1073
+
1074
+ if (
1075
+ through === 'closeout' &&
1076
+ tasks !== undefined &&
1077
+ specContract !== undefined
1078
+ ) {
1079
+ validateCloseoutTasks(tasks, errors);
1080
+ const verifyReport = readArtifact(root, 'verify-report.md', errors);
1081
+ const archiveReport = readArtifact(root, 'archive-report.md', errors);
1082
+ if (verifyReport !== undefined) {
1083
+ validateVerifyReport(verifyReport, errors, specContract);
1084
+ }
1085
+ if (archiveReport !== undefined) {
1086
+ validateArchiveReport(archiveReport, errors);
1087
+ }
1088
+ }
1089
+
1090
+ return {
1091
+ valid: errors.length === 0,
1092
+ route,
1093
+ through,
1094
+ changeRoot: root,
1095
+ errors,
1096
+ warnings,
1097
+ };
1098
+ }
1099
+
1100
+ try {
1101
+ const options = parseArgs(process.argv.slice(2));
1102
+ const report = validate(options);
1103
+ const output = options.json
1104
+ ? JSON.stringify(report)
1105
+ : report.valid
1106
+ ? 'SDD artifacts are structurally valid.'
1107
+ : report.errors
1108
+ .map((error) => `${error.code} (${error.artifact}): ${error.message}`)
1109
+ .join('\n');
1110
+ process.stdout.write(`${output}\n`);
1111
+ process.exitCode = report.valid ? 0 : 1;
1112
+ } catch (error) {
1113
+ process.stderr.write(
1114
+ `${error instanceof Error ? error.message : String(error)}\n`,
1115
+ );
1116
+ process.exitCode = 2;
1117
+ }