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.
- package/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- 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
|
-
}
|