cyber-sdd 0.7.0 → 0.7.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/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/agents/sdd-scanner.md +1 -1
  4. package/package.json +11 -13
  5. package/plugin.json +55 -0
  6. package/skills/align-spec/SKILL.md +1 -1
  7. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  8. package/skills/blast-estimate/README.md +3 -3
  9. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  10. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  11. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  12. package/skills/check-plan-safety/SKILL.md +2 -3
  13. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  14. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  15. package/skills/check-project-specs/SKILL.md +1 -1
  16. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  17. package/skills/check-retired-terms/SKILL.md +2 -3
  18. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  19. package/skills/check-scenario-overlap/SKILL.md +1 -2
  20. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  21. package/skills/check-spec-references/SKILL.md +2 -2
  22. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  23. package/skills/check-spec-structure/SKILL.md +1 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  25. package/skills/collision-ladder/README.md +2 -2
  26. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  27. package/skills/combat-log-governance/SKILL.md +1 -1
  28. package/skills/concept-index/SKILL.md +1 -1
  29. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  30. package/skills/discover-plans/README.md +3 -3
  31. package/skills/discover-plans/SKILL.md +1 -1
  32. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  33. package/skills/discover-specs/README.md +3 -3
  34. package/skills/discover-specs/SKILL.md +1 -1
  35. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  36. package/skills/doctrine-loop/SKILL.md +1 -1
  37. package/skills/gate-validation-governance/SKILL.md +1 -1
  38. package/skills/init/README.md +2 -2
  39. package/skills/init/scripts/wire-statusline.mjs +168 -12
  40. package/skills/manage-ignore/README.md +3 -3
  41. package/skills/manage-ignore/SKILL.md +1 -2
  42. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  43. package/skills/manage-scenario-bridge/README.md +3 -3
  44. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  45. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  46. package/skills/manage-spec-anchors/README.md +3 -3
  47. package/skills/manage-spec-anchors/SKILL.md +1 -1
  48. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  49. package/skills/mission-graph/README.md +2 -2
  50. package/skills/mission-graph/SKILL.md +1 -1
  51. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  52. package/skills/pause-mission/SKILL.md +1 -1
  53. package/skills/place-node/SKILL.md +2 -2
  54. package/skills/place-node/scripts/place-node.mjs +117 -12
  55. package/skills/plan-retirement/README.md +2 -2
  56. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  57. package/skills/resolve-governances/README.md +3 -3
  58. package/skills/resolve-governances/SKILL.md +1 -1
  59. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  60. package/skills/resolve-tracking/SKILL.md +1 -1
  61. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  62. package/skills/scaffold-project-spec/SKILL.md +1 -1
  63. package/skills/spec-gate/README.md +12 -9
  64. package/skills/spec-gate/SKILL.md +2 -2
  65. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  66. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  67. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  68. package/skills/spec-producer-governance/SKILL.md +2 -2
  69. package/skills/start-mission/SKILL.md +1 -1
  70. package/skills/suite-format-governance/README.md +1 -1
  71. package/skills/suite-format-governance/SKILL.md +1 -1
  72. package/skills/touch-set-correction/README.md +5 -5
  73. package/skills/touch-set-correction/SKILL.md +1 -1
  74. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  75. package/skills/verify-scenarios/README.md +6 -6
  76. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  77. package/.plugin/pins.json +0 -3
  78. package/.plugin/plugin.json +0 -19
  79. package/dist/align-spec/scripts/align-spec.js +0 -102
  80. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  81. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  82. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  83. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  84. package/dist/concept-index/scripts/concept-index.js +0 -181
  85. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  86. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  87. package/dist/spec-gate/scripts/check-suite.js +0 -317
  88. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  89. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  90. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  91. package/skills/align-spec/scripts/align-spec.mts +0 -188
  92. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  93. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  94. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  95. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  96. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  97. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  98. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  99. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  100. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  102. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  103. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  104. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  105. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  106. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  108. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  109. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  110. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  111. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  112. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  113. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  114. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  115. package/skills/concept-index/scripts/concept-index.mts +0 -246
  116. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  117. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  118. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  119. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  120. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  121. package/skills/init/scripts/wire-statusline.mts +0 -279
  122. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  123. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  124. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  126. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  128. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  129. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  130. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  131. package/skills/place-node/scripts/place-node.mts +0 -158
  132. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  133. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  134. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  135. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  136. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  137. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  138. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  139. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  140. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  141. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  142. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  143. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  144. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  145. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  146. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  147. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  148. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  149. 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
- }