cyber-sdd 0.7.0 → 0.7.2

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 (150) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/CHANGELOG.md +320 -0
  4. package/agents/sdd-scanner.md +1 -1
  5. package/package.json +11 -12
  6. package/plugin.json +55 -0
  7. package/skills/align-spec/SKILL.md +1 -1
  8. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  9. package/skills/blast-estimate/README.md +3 -3
  10. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  11. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  12. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  13. package/skills/check-plan-safety/SKILL.md +2 -3
  14. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  15. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  16. package/skills/check-project-specs/SKILL.md +1 -1
  17. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  18. package/skills/check-retired-terms/SKILL.md +2 -3
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  20. package/skills/check-scenario-overlap/SKILL.md +1 -2
  21. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  22. package/skills/check-spec-references/SKILL.md +2 -2
  23. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  24. package/skills/check-spec-structure/SKILL.md +1 -2
  25. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  26. package/skills/collision-ladder/README.md +2 -2
  27. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  28. package/skills/combat-log-governance/SKILL.md +1 -1
  29. package/skills/concept-index/SKILL.md +1 -1
  30. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  31. package/skills/discover-plans/README.md +3 -3
  32. package/skills/discover-plans/SKILL.md +1 -1
  33. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  34. package/skills/discover-specs/README.md +3 -3
  35. package/skills/discover-specs/SKILL.md +1 -1
  36. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  37. package/skills/doctrine-loop/SKILL.md +1 -1
  38. package/skills/gate-validation-governance/SKILL.md +1 -1
  39. package/skills/init/README.md +2 -2
  40. package/skills/init/scripts/wire-statusline.mjs +168 -12
  41. package/skills/manage-ignore/README.md +3 -3
  42. package/skills/manage-ignore/SKILL.md +1 -2
  43. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  44. package/skills/manage-scenario-bridge/README.md +3 -3
  45. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  46. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  47. package/skills/manage-spec-anchors/README.md +3 -3
  48. package/skills/manage-spec-anchors/SKILL.md +1 -1
  49. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  50. package/skills/mission-graph/README.md +2 -2
  51. package/skills/mission-graph/SKILL.md +1 -1
  52. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  53. package/skills/pause-mission/SKILL.md +1 -1
  54. package/skills/place-node/SKILL.md +2 -2
  55. package/skills/place-node/scripts/place-node.mjs +117 -12
  56. package/skills/plan-retirement/README.md +2 -2
  57. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  58. package/skills/resolve-governances/README.md +3 -3
  59. package/skills/resolve-governances/SKILL.md +1 -1
  60. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  61. package/skills/resolve-tracking/SKILL.md +1 -1
  62. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  63. package/skills/scaffold-project-spec/SKILL.md +1 -1
  64. package/skills/spec-gate/README.md +12 -9
  65. package/skills/spec-gate/SKILL.md +2 -2
  66. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  67. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  68. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  69. package/skills/spec-producer-governance/SKILL.md +2 -2
  70. package/skills/start-mission/SKILL.md +1 -1
  71. package/skills/suite-format-governance/README.md +1 -1
  72. package/skills/suite-format-governance/SKILL.md +1 -1
  73. package/skills/touch-set-correction/README.md +5 -5
  74. package/skills/touch-set-correction/SKILL.md +1 -1
  75. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  76. package/skills/verify-scenarios/README.md +6 -6
  77. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  78. package/.plugin/pins.json +0 -3
  79. package/.plugin/plugin.json +0 -19
  80. package/dist/align-spec/scripts/align-spec.js +0 -102
  81. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  82. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  83. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  84. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  85. package/dist/concept-index/scripts/concept-index.js +0 -181
  86. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  87. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  88. package/dist/spec-gate/scripts/check-suite.js +0 -317
  89. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  90. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  91. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  92. package/skills/align-spec/scripts/align-spec.mts +0 -188
  93. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  94. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  95. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  96. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  97. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  98. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  99. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  100. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  102. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  103. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  104. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  105. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  106. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  108. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  109. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  110. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  111. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  112. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  113. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  114. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  115. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  116. package/skills/concept-index/scripts/concept-index.mts +0 -246
  117. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  118. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  119. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  120. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  121. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  124. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  126. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  128. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  129. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  130. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  131. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  132. package/skills/place-node/scripts/place-node.mts +0 -158
  133. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  134. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  135. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  136. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  137. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  138. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  139. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  140. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  141. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  142. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  143. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  144. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  145. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  146. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  147. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  148. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  149. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  150. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,347 +0,0 @@
1
- #!/usr/bin/env node
2
- // check-spec-structure — project-spec/check-spec-structure's concrete engine. Audits the internal
3
- // node-shape of one project spec and emits a finding set for the formation Warden: the intra-spec
4
- // successor to the retired cross-spec dedupe/split tools, now that one project is one spec
5
- // (see this skill's README.md).
6
- //
7
- // Two deterministic checks, each with a severity:
8
- // - untagged-node (blocking) — a spec-typed node README with no `concept:` tag, so it never
9
- // appears in the by-concept index (../concept-index/). `--check` fails on it.
10
- // - oversized-node (advisory) — a node whose sibling `.feature` scenario count exceeds the
11
- // granularity threshold; carries a deterministic shape profile (plain/tagged counts + section-
12
- // cluster count) but prescribes no route. Never fails `--check`.
13
- // - missing-glossary (advisory) — the project spec has no root `glossary.md`, so its ubiquitous
14
- // language has no home and a term can be used without ever being defined. A root FILE, not a
15
- // folder: every mandated folder is an exception to screaming architecture. Never fails `--check`.
16
- // - incomplete-node (advisory) — a behavioral leaf spec (a README with `spec-type: behavioral` and
17
- // a colocated `.feature`) missing one of the four required `spec.md` sections (`## What`,
18
- // `## Use Cases`, `## Control Flow`, `## Scenario map` — `sdd:spec-format-governance`). A spec
19
- // that stops at `## Use Cases` never draws its CFG or scenario map. Advisory while a corpus is
20
- // brought up to the four-section shape; a follow-up flips it to blocking once clean.
21
- // Breadth-vs-depth routing and intra-spec contradiction are Warden judgment (the @rubric scenarios)
22
- // — no engine code here.
23
- //
24
- // Pure derivation from frontmatter + scenario counts + the README's level-2 section headings (the
25
- // four-section-shape signal): no node *prose* reaches a finding, and the engine writes nothing. No
26
- // dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions are exported for
27
- // node:test; running the file directly drives the CLI.
28
-
29
- import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
30
- import { join } from 'node:path'
31
- import { pathToFileURL } from 'node:url'
32
-
33
- const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
34
- export const DEFAULT_MAX_SCENARIOS = 40
35
-
36
- // The four `spec.md` sections a behavioral leaf spec must carry, in order
37
- // (`sdd:spec-format-governance`). `## References` is optional and not required here.
38
- export const REQUIRED_BEHAVIORAL_SECTIONS = ['What', 'Use Cases', 'Control Flow', 'Scenario map']
39
-
40
- export type FindingKind = 'untagged-node' | 'oversized-node' | 'missing-glossary' | 'incomplete-node'
41
- export type Severity = 'blocking' | 'advisory'
42
-
43
- export interface NodeRecord {
44
- /** Path relative to the spec directory (POSIX). */
45
- relPath: string
46
- /** The top-level folder — the capability. */
47
- capability: string
48
- /** Display form — a README.md node shows its folder. */
49
- display: string
50
- concepts: string[]
51
- specType?: string
52
- hasFeature: boolean
53
- /** Level-2 `## ` headings in the README body, in document order. */
54
- sectionHeadings: string[]
55
- scenarioCount: number
56
- plainCount: number
57
- taggedCount: number
58
- clusterCount: number
59
- }
60
-
61
- export interface Finding {
62
- kind: FindingKind
63
- severity: Severity
64
- node: string
65
- detail: string
66
- }
67
-
68
- // ── Frontmatter parse (concept + spec-type only — the classification signal) ──
69
- export interface NodeFrontmatter {
70
- concepts: string[]
71
- specType?: string
72
- }
73
-
74
- export function parseNodeFrontmatter(text: string): NodeFrontmatter {
75
- const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text)
76
- if (!m) return { concepts: [] }
77
- const fm: NodeFrontmatter = { concepts: [] }
78
- const lines = m[1].split('\n').map((l) => l.replace(/\r$/, ''))
79
- for (let i = 0; i < lines.length; i++) {
80
- const line = lines[i]
81
- if (line.trim() === '' || line.trim().startsWith('#')) continue
82
- if (line.length - line.trimStart().length !== 0) continue // top-level keys only
83
- const [key, ...rest] = line.trim().split(':')
84
- const value = rest.join(':').trim()
85
- if (key === 'spec-type') fm.specType = unquote(value)
86
- else if (key === 'concept') {
87
- if (value === '' || value === '|' || value === '>') {
88
- for (let j = i + 1; j < lines.length; j++) {
89
- const item = lines[j]
90
- if (item.trim() === '') continue
91
- if (item.length - item.trimStart().length === 0) break
92
- const dash = /^\s*-\s+(.*)$/.exec(item)
93
- if (dash) fm.concepts.push(unquote(dash[1].trim()))
94
- }
95
- } else {
96
- fm.concepts.push(...parseScalarOrFlow(value))
97
- }
98
- }
99
- }
100
- return fm
101
- }
102
-
103
- export function parseScalarOrFlow(value: string): string[] {
104
- const v = value.trim()
105
- if (v.startsWith('[') && v.endsWith(']')) {
106
- return v
107
- .slice(1, -1)
108
- .split(',')
109
- .map((s) => unquote(s.trim()))
110
- .filter((s) => s.length > 0)
111
- }
112
- const single = unquote(v)
113
- return single.length > 0 ? [single] : []
114
- }
115
-
116
- function unquote(v: string): string {
117
- return v.replace(/^["']|["']$/g, '')
118
- }
119
-
120
- // ── Section headings — the four-section shape signal (level-2 `## ` headings, fence-aware) ──
121
- export function parseSectionHeadings(text: string): string[] {
122
- const headings: string[] = []
123
- let inFence = false
124
- for (const raw of text.split('\n')) {
125
- const line = raw.replace(/\r$/, '')
126
- if (/^\s*(```|~~~)/.test(line)) {
127
- inFence = !inFence
128
- continue
129
- }
130
- if (inFence) continue
131
- const m = /^##\s+(.+?)\s*$/.exec(line)
132
- if (m) headings.push(m[1].replace(/`/g, '').trim())
133
- }
134
- return headings
135
- }
136
-
137
- // ── Scenario count — the granularity signal (frontmatter-free, count `Scenario:` lines) ──
138
- export function countScenarios(featureText: string): number {
139
- let n = 0
140
- for (const line of featureText.split('\n')) {
141
- if (/^\s*Scenario:/.test(line)) n++
142
- }
143
- return n
144
- }
145
-
146
- // ── Shape profile — the deterministic breadth-vs-depth signal for the Warden ──
147
- export interface ShapeProfile {
148
- scenarioCount: number
149
- plainCount: number
150
- taggedCount: number
151
- clusterCount: number
152
- }
153
-
154
- const SECTION_HEADER = /^\s*#\s*(?:──|-{3,})/
155
-
156
- export function profileFeature(featureText: string): ShapeProfile {
157
- const lines = featureText.split('\n')
158
- let scenarioCount = 0
159
- let taggedCount = 0
160
- let clusterCount = 0
161
- let pendingRubric = false
162
- for (const line of lines) {
163
- if (/^\s*Scenario:/.test(line)) {
164
- scenarioCount++
165
- if (pendingRubric) taggedCount++
166
- pendingRubric = false
167
- continue
168
- }
169
- const trimmed = line.trim()
170
- if (trimmed === '') continue
171
- if (trimmed.startsWith('@')) {
172
- if (/(^|\s)@rubric(\s|$)/.test(trimmed)) pendingRubric = true
173
- continue
174
- }
175
- if (SECTION_HEADER.test(line)) {
176
- clusterCount++
177
- pendingRubric = false
178
- continue
179
- }
180
- pendingRubric = false
181
- }
182
- return { scenarioCount, plainCount: scenarioCount - taggedCount, taggedCount, clusterCount }
183
- }
184
-
185
- function displayPath(relPath: string): string {
186
- const p = relPath.replace(/\\/g, '/')
187
- return p.endsWith('/README.md') ? p.slice(0, -'README.md'.length) : p
188
- }
189
-
190
- // ── Scan the project-spec — every node carrying a spec-type or concept tag ──
191
- export function scanProjectSpec(specDir: string): NodeRecord[] {
192
- const records: NodeRecord[] = []
193
- walk(specDir, specDir, records)
194
- return records.sort((a, b) => a.display.localeCompare(b.display))
195
- }
196
-
197
- function walk(dir: string, specDir: string, out: NodeRecord[]): void {
198
- const entries = readdirSync(dir, { withFileTypes: true })
199
- for (const entry of entries) {
200
- if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue
201
- const full = join(dir, entry.name)
202
- if (entry.isDirectory()) {
203
- walk(full, specDir, out)
204
- } else if (entry.name === 'README.md') {
205
- const readmeText = readFileSync(full, 'utf8')
206
- const fm = parseNodeFrontmatter(readmeText)
207
- if (fm.specType === undefined && fm.concepts.length === 0) continue
208
- const sectionHeadings = parseSectionHeadings(readmeText)
209
- const features = entries.filter((e) => e.isFile() && e.name.endsWith('.feature'))
210
- const hasFeature = features.length > 0
211
- const profile = hasFeature
212
- ? profileFeature(readFileSync(join(dir, features[0].name), 'utf8'))
213
- : { scenarioCount: 0, plainCount: 0, taggedCount: 0, clusterCount: 0 }
214
- const relPath = full.slice(specDir.length + 1).replace(/\\/g, '/')
215
- out.push({
216
- relPath,
217
- capability: relPath.split('/')[0],
218
- display: displayPath(relPath),
219
- concepts: fm.concepts,
220
- specType: fm.specType,
221
- sectionHeadings,
222
- hasFeature,
223
- scenarioCount: profile.scenarioCount,
224
- plainCount: profile.plainCount,
225
- taggedCount: profile.taggedCount,
226
- clusterCount: profile.clusterCount,
227
- })
228
- }
229
- }
230
- }
231
-
232
- // ── Checks ──
233
- // A spec-typed node with no concept is orphaned from the by-concept index → blocking.
234
- export function checkUntagged(records: NodeRecord[]): Finding[] {
235
- return records
236
- .filter((r) => r.specType !== undefined && r.concepts.length === 0)
237
- .map((r) => ({
238
- kind: 'untagged-node' as const,
239
- severity: 'blocking' as const,
240
- node: r.display,
241
- detail: `spec-type: ${r.specType} but no concept tag — orphaned from the by-concept index`,
242
- }))
243
- }
244
-
245
- // A node whose suite exceeds the granularity threshold → advisory split candidate.
246
- export function checkOversized(records: NodeRecord[], maxScenarios: number): Finding[] {
247
- return records
248
- .filter((r) => r.scenarioCount > maxScenarios)
249
- .map((r) => ({
250
- kind: 'oversized-node' as const,
251
- severity: 'advisory' as const,
252
- node: r.display,
253
- detail: `${r.scenarioCount} scenarios > ${maxScenarios} — shape profile: plain ${r.plainCount}, tagged ${r.taggedCount}, clusters ${r.clusterCount} (soft breadth hint); the Warden routes breadth-vs-depth`,
254
- }))
255
- }
256
-
257
- // A project spec with no root glossary.md has nowhere to define its ubiquitous language → advisory.
258
- export function checkGlossary(specDir: string): Finding[] {
259
- if (existsSync(join(specDir, 'glossary.md'))) return []
260
- return [
261
- {
262
- kind: 'missing-glossary' as const,
263
- severity: 'advisory' as const,
264
- node: 'glossary.md',
265
- detail:
266
- 'no root glossary.md — the project has no home for its ubiquitous language, so a term can be used without ever being defined; a root file, never a folder',
267
- },
268
- ]
269
- }
270
-
271
- // A behavioral leaf spec (README with `spec-type: behavioral` + a colocated `.feature`) whose
272
- // spec.md stops short of the four required sections never drew its CFG or scenario map → advisory
273
- // until a corpus is brought up to the four-section shape, then flipped to blocking by a follow-up.
274
- export function checkIncomplete(records: NodeRecord[]): Finding[] {
275
- return records
276
- .filter((r) => r.specType === 'behavioral' && r.hasFeature)
277
- .map((r) => {
278
- const missing = REQUIRED_BEHAVIORAL_SECTIONS.filter((s) => !r.sectionHeadings.includes(s))
279
- return { record: r, missing }
280
- })
281
- .filter(({ missing }) => missing.length > 0)
282
- .map(({ record, missing }) => ({
283
- kind: 'incomplete-node' as const,
284
- severity: 'advisory' as const,
285
- node: record.display,
286
- detail: `behavioral leaf spec missing required section(s): ${missing.map((s) => `## ${s}`).join(', ')} — a spec that stops at ## Use Cases never draws its CFG or scenario map (sdd:spec-format-governance)`,
287
- }))
288
- }
289
-
290
- export function audit(records: NodeRecord[], maxScenarios: number, specDir?: string): Finding[] {
291
- return [
292
- ...checkUntagged(records),
293
- ...checkOversized(records, maxScenarios),
294
- ...checkIncomplete(records),
295
- ...(specDir === undefined ? [] : checkGlossary(specDir)),
296
- ]
297
- }
298
-
299
- export function hasBlocking(findings: Finding[]): boolean {
300
- return findings.some((f) => f.severity === 'blocking')
301
- }
302
-
303
- // ── Render ──
304
- export function renderFindings(findings: Finding[], specDir: string): string {
305
- const blocking = findings.filter((f) => f.severity === 'blocking')
306
- const advisory = findings.filter((f) => f.severity === 'advisory')
307
- const lines: string[] = [`check-spec-structure: spec-dir=${specDir}`]
308
- lines.push(`blocking[${blocking.length}]:`)
309
- for (const f of blocking) lines.push(` ${f.node} — ${f.kind}: ${f.detail}`)
310
- lines.push(`advisory[${advisory.length}]:`)
311
- for (const f of advisory) lines.push(` ${f.node} — ${f.kind}: ${f.detail}`)
312
- lines.push('note: advisory — findings feed the Warden formation pass; the engine writes nothing')
313
- return lines.join('\n')
314
- }
315
-
316
- // ── CLI ──
317
- export function main(argv: string[]): number {
318
- let specDir = '.'
319
- let mode: 'audit' | 'check' = 'audit'
320
- let format: 'toon' | 'json' = 'toon'
321
- let maxScenarios = DEFAULT_MAX_SCENARIOS
322
- for (let i = 0; i < argv.length; i++) {
323
- const a = argv[i]
324
- if (a === '--spec-dir') specDir = argv[++i] ?? '.'
325
- else if (a === '--check') mode = 'check'
326
- else if (a === '--max-scenarios') maxScenarios = Number(argv[++i] ?? DEFAULT_MAX_SCENARIOS)
327
- else if (a === '--format') format = (argv[++i] as 'toon' | 'json') ?? 'toon'
328
- }
329
- const findings = audit(scanProjectSpec(specDir), maxScenarios, specDir)
330
- if (mode === 'check') {
331
- if (hasBlocking(findings)) {
332
- process.stderr.write(
333
- `check-spec-structure: ${findings.filter((f) => f.severity === 'blocking').length} blocking finding(s)\n`,
334
- )
335
- return 1
336
- }
337
- process.stdout.write('check-spec-structure: no blocking findings\n')
338
- return 0
339
- }
340
- if (format === 'json') process.stdout.write(`${JSON.stringify(findings)}\n`)
341
- else process.stdout.write(`${renderFindings(findings, specDir)}\n`)
342
- return 0
343
- }
344
-
345
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
346
- process.exit(main(process.argv.slice(2)))
347
- }