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,422 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// check-spec-references — resolve every explicitly-relative reference a project spec's .md files
|
|
3
|
-
// carry, and report the ones that resolve to nothing.
|
|
4
|
-
//
|
|
5
|
-
// The recurrence this closes was not a typo but a CONSISTENT OFF-BY-ONE: every `../../../src/…`
|
|
6
|
-
// reference in a spec corpus was one directory level short, and every one of them read as entirely
|
|
7
|
-
// plausible — right filename, right-looking depth, wrong level. Re-reading a reference by eye is
|
|
8
|
-
// not a check on it; only resolving it is. So the finding names BOTH the reference as written and
|
|
9
|
-
// the path it actually resolved to — the second is the half review cannot supply.
|
|
10
|
-
//
|
|
11
|
-
// Scope is deliberately narrow. Only an EXPLICITLY-RELATIVE path (`./` or `../`) is a reference; a
|
|
12
|
-
// bare path in inline code is prose. A spec corpus is full of bare paths that are illustrative or
|
|
13
|
-
// relative to somewhere other than the repo (`cli/`, `skills/doctor/`, `.claude/skills`,
|
|
14
|
-
// `~/.codex/config.toml`), and resolving those would reject nearly all of them. The prefix rule is
|
|
15
|
-
// what makes the repo-root-relative case (`.research/agentic-configuration-standards/`) pass by
|
|
16
|
-
// construction rather than by exception.
|
|
17
|
-
//
|
|
18
|
-
// There is exactly ONE escape hatch, deliberately. A second, implicit one was built and then cut:
|
|
19
|
-
// excluding whatever a doubled-backtick span holds, on the theory that such a span exhibits markup
|
|
20
|
-
// rather than citing it. It was unconditional, so a genuinely broken reference written that way
|
|
21
|
-
// escaped silently — reproducing, inside the exclusion, the exact "looks anchored, isn't" failure
|
|
22
|
-
// this engine exists to close. An escape must be explicit, reasoned, and visible where it applies.
|
|
23
|
-
//
|
|
24
|
-
// One false-positive class is genuine and recurs: prose QUOTING a path relative to something other
|
|
25
|
-
// than the file it sits in — the text held inside a bridge file, a symlink target relative to
|
|
26
|
-
// `.cursor/`. That is structurally indistinguishable from a real anchor, so an inline
|
|
27
|
-
// `<!-- spec-ref-ignore: why -->` marker suppresses its own line, keeping the justification beside
|
|
28
|
-
// the prose it excuses instead of in a registry that drifts away from what it covers.
|
|
29
|
-
//
|
|
30
|
-
// Pure functions are exported for node:test; running the file directly drives the CLI. No
|
|
31
|
-
// dependencies (the repo's node-≥23.6 / no-deps convention).
|
|
32
|
-
|
|
33
|
-
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
34
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
35
|
-
import { pathToFileURL } from 'node:url'
|
|
36
|
-
|
|
37
|
-
// ── Extraction ──
|
|
38
|
-
|
|
39
|
-
/** A reference as written, and the 1-based line it sits on. */
|
|
40
|
-
export interface Reference {
|
|
41
|
-
line: number
|
|
42
|
-
ref: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Suppresses every reference on the line it appears on. Matched as a COMPLETE html comment, not
|
|
46
|
-
* as a substring: a marker whose name merely starts with this one's (a typo, a future sibling)
|
|
47
|
-
* must not silently inherit its power to hide a reference. The optional `: reason` is convention,
|
|
48
|
-
* not syntax — the marker is recognized with or without one. */
|
|
49
|
-
const IGNORE_MARKER_RE = /<!--\s*spec-ref-ignore\s*(?::[^>]*)?-->/
|
|
50
|
-
|
|
51
|
-
/** A markdown inline link whose target is explicitly relative — covering the plain form, an
|
|
52
|
-
* angle-bracket-wrapped target, and an optional title in any of markdown's three quotings. Run only
|
|
53
|
-
* over the parts of a line that are NOT inside a code span: inside one, markup is literal text on
|
|
54
|
-
* display, not a link. */
|
|
55
|
-
const LINK_RE = /\]\(\s*(?:<([^>\n]*)>|(\.{1,2}\/[^)\s]*))(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)/g
|
|
56
|
-
|
|
57
|
-
/** A reference-style link definition (`[label]: ../path`) whose target is explicitly relative. It
|
|
58
|
-
* is a link target like any other — the label form changes where the path is written, not what it
|
|
59
|
-
* points at. */
|
|
60
|
-
const LINK_DEF_RE = /^\s{0,3}\[.+\]:\s*(?:<([^>\n]*)>|(\S+))/
|
|
61
|
-
|
|
62
|
-
/** An explicitly-relative path, and nothing else. The trailing class excludes whitespace and
|
|
63
|
-
* backticks so that "the span's WHOLE content is the path" means what it says: a span holding
|
|
64
|
-
* `../a` plus commentary is prose that starts with a path, not a citation. */
|
|
65
|
-
const RELATIVE_RE = /^\.{1,2}\/[^\s`]*$/
|
|
66
|
-
|
|
67
|
-
/** The same, minus the no-whitespace rule — for an ANGLE-BRACKET link target, whose whole reason
|
|
68
|
-
* for existing is to carry a path with a space in it. */
|
|
69
|
-
const RELATIVE_IN_ANGLES_RE = /^\.{1,2}\/[^`]*$/
|
|
70
|
-
|
|
71
|
-
/** An open fence, remembered so the close can be matched against it. */
|
|
72
|
-
interface Fence {
|
|
73
|
-
char: '`' | '~'
|
|
74
|
-
length: number
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The fence a line opens, or `undefined`. CommonMark: three or more backticks or tildes, indented
|
|
79
|
-
* fewer than four spaces.
|
|
80
|
-
*/
|
|
81
|
-
function fenceOpenedBy(line: string): Fence | undefined {
|
|
82
|
-
const m = /^ {0,3}(`{3,}|~{3,})/.exec(line)
|
|
83
|
-
if (m === null) return undefined
|
|
84
|
-
const run = m[1] as string
|
|
85
|
-
return { char: run[0] as '`' | '~', length: run.length }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Whether `line` CLOSES `fence`: the same character, at least as long, and nothing but whitespace
|
|
90
|
-
* after it.
|
|
91
|
-
*
|
|
92
|
-
* Matching the delimiter rather than toggling a boolean on any fence-shaped line is what keeps the
|
|
93
|
-
* exclusion honest. A bare toggle flips on a `~~~` line inside a backtick fence — so a reference
|
|
94
|
-
* genuinely inside a fence gets extracted, and, worse, a fence-shaped line inside another fence
|
|
95
|
-
* leaves the parity inverted and every genuinely broken reference AFTER the block is silently
|
|
96
|
-
* swallowed. That is this engine's own failure class, reached through the one rule meant to
|
|
97
|
-
* suppress noise.
|
|
98
|
-
*/
|
|
99
|
-
function fenceClosedBy(line: string, fence: Fence): boolean {
|
|
100
|
-
const m = /^ {0,3}(`{3,}|~{3,})\s*$/.exec(line)
|
|
101
|
-
if (m === null) return false
|
|
102
|
-
const run = m[1] as string
|
|
103
|
-
return run[0] === fence.char && run.length >= fence.length
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* A line that starts a new block: blank, an ATX heading, a blockquote, a list item, or a thematic
|
|
108
|
-
* break. Inline parsing happens WITHIN a block, so a code span cannot reach across one of these —
|
|
109
|
-
* and the boundary matters in the under-reporting direction. An unclosed backtick left by a typo
|
|
110
|
-
* would otherwise pair with the first backtick of the next block, swallowing the span that was
|
|
111
|
-
* about to open and every reference after it in the flow.
|
|
112
|
-
*/
|
|
113
|
-
export function startsNewBlock(line: string): boolean {
|
|
114
|
-
if (/^[ \t]*$/.test(line)) return true
|
|
115
|
-
if (/^ {0,3}#{1,6}(\s|$)/.test(line)) return true // ATX heading
|
|
116
|
-
if (/^ {0,3}>/.test(line)) return true // blockquote
|
|
117
|
-
if (/^ {0,3}([-+*]|\d{1,9}[.)])(\s|$)/.test(line)) return true // list item
|
|
118
|
-
if (/^ {0,3}((\*[ \t]*){3,}|(-[ \t]*){3,}|(_[ \t]*){3,})$/.test(line)) return true // thematic break
|
|
119
|
-
return false
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface CodeSpan {
|
|
123
|
-
/** The span's content, line endings folded to spaces and CommonMark's one-space padding stripped. */
|
|
124
|
-
content: string
|
|
125
|
-
/** Offset into the scanned text where the opening run begins. */
|
|
126
|
-
start: number
|
|
127
|
-
end: number
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Same length, same line breaks, no content — so blanking a region shifts no offset and moves no
|
|
131
|
-
* finding to another line. */
|
|
132
|
-
function blankOut(region: string): string {
|
|
133
|
-
return region.replace(/[^\n]/g, ' ')
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The inline-code spans on one line, scanned the way CommonMark delimits them: a run of N backticks
|
|
138
|
-
* opens, the next run of EXACTLY N closes, and one leading + trailing space is stripped when both
|
|
139
|
-
* are present.
|
|
140
|
-
*
|
|
141
|
-
* Scanning properly rather than matching a single-backtick pair is what makes the reference rule —
|
|
142
|
-
* "a code span is a reference when its WHOLE content is the path" — mean one thing everywhere. A
|
|
143
|
-
* doubled span written around another span (`` `x` ``) has content that still carries backticks, so
|
|
144
|
-
* it is not a path and not a reference; a doubled span written around a bare path has the path as
|
|
145
|
-
* its content, so it IS one. Neither is a special case, and neither leaves a broken reference
|
|
146
|
-
* anywhere to hide.
|
|
147
|
-
*/
|
|
148
|
-
export function scanCodeSpans(text: string, blockStarts: readonly number[] = []): CodeSpan[] {
|
|
149
|
-
const out: CodeSpan[] = []
|
|
150
|
-
let i = 0
|
|
151
|
-
while (i < text.length) {
|
|
152
|
-
if (text[i] !== '`') {
|
|
153
|
-
i++
|
|
154
|
-
continue
|
|
155
|
-
}
|
|
156
|
-
const open = i
|
|
157
|
-
while (text[i] === '`') i++
|
|
158
|
-
const runLength = i - open
|
|
159
|
-
// find the next run of exactly runLength backticks, stopping at the next block boundary — a
|
|
160
|
-
// code span lives inside one block and cannot reach past it
|
|
161
|
-
const limit = blockStarts.find((b) => b > open) ?? text.length
|
|
162
|
-
let j = i
|
|
163
|
-
let closeStart = -1
|
|
164
|
-
while (j < limit) {
|
|
165
|
-
if (text[j] !== '`') {
|
|
166
|
-
j++
|
|
167
|
-
continue
|
|
168
|
-
}
|
|
169
|
-
const runStart = j
|
|
170
|
-
while (text[j] === '`') j++
|
|
171
|
-
if (j - runStart === runLength) {
|
|
172
|
-
closeStart = runStart
|
|
173
|
-
break
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (closeStart === -1) {
|
|
177
|
-
// An unmatched run is literal text, and the scan RESUMES after it — CommonMark's own
|
|
178
|
-
// recovery. Abandoning the rest of the text instead would let one stray backtick
|
|
179
|
-
// silently swallow every reference after it, which is this engine's own failure class
|
|
180
|
-
// wearing a different hat.
|
|
181
|
-
i = open + runLength
|
|
182
|
-
continue
|
|
183
|
-
}
|
|
184
|
-
// Line endings inside a span are spaces, per CommonMark — which is what lets a span that
|
|
185
|
-
// WRAPS still be one span. Prose here hard-wraps, so a long path in backticks landing
|
|
186
|
-
// across a line break is ordinary, not exotic; scanning line by line would have left every
|
|
187
|
-
// wrapped reference unread.
|
|
188
|
-
let content = text.slice(open + runLength, closeStart).replace(/\r?\n/g, ' ')
|
|
189
|
-
if (content.length > 1 && content.startsWith(' ') && content.endsWith(' ') && content.trim() !== '') {
|
|
190
|
-
content = content.slice(1, -1)
|
|
191
|
-
}
|
|
192
|
-
out.push({ content, start: open, end: j })
|
|
193
|
-
i = j
|
|
194
|
-
}
|
|
195
|
-
return out
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Every explicitly-relative reference in `text`, in document order.
|
|
200
|
-
*
|
|
201
|
-
* Skipped: lines inside a fenced code block, and lines carrying the ignore marker. Extraction is
|
|
202
|
-
* line-scoped on purpose — it is what gives each finding a line number, and what lets the marker's
|
|
203
|
-
* scope be exactly the line it appears on rather than the whole file.
|
|
204
|
-
*/
|
|
205
|
-
export function extractReferences(text: string): Reference[] {
|
|
206
|
-
const lines = text.split('\n')
|
|
207
|
-
|
|
208
|
-
// 1. Blank the fenced blocks, keeping every line's length and every line break, so offsets and
|
|
209
|
-
// line numbers below still mean what they say.
|
|
210
|
-
let fence: Fence | undefined
|
|
211
|
-
const unfenced = lines.map((line) => {
|
|
212
|
-
if (fence !== undefined) {
|
|
213
|
-
if (fenceClosedBy(line, fence)) fence = undefined
|
|
214
|
-
return blankOut(line)
|
|
215
|
-
}
|
|
216
|
-
const opened = fenceOpenedBy(line)
|
|
217
|
-
if (opened !== undefined) {
|
|
218
|
-
fence = opened
|
|
219
|
-
return blankOut(line)
|
|
220
|
-
}
|
|
221
|
-
return line
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
// 2. Scan code spans across the WHOLE text, not line by line: a span that wraps is still one
|
|
225
|
-
// span, and reading it as two would leave a wrapped reference unread. Bounded at block
|
|
226
|
-
// starts, so widening the window past a line break does not let a stray backtick reach into
|
|
227
|
-
// the next block.
|
|
228
|
-
const scannable = unfenced.join('\n')
|
|
229
|
-
const lineStarts: number[] = []
|
|
230
|
-
let acc = 0
|
|
231
|
-
for (const line of lines) {
|
|
232
|
-
lineStarts.push(acc)
|
|
233
|
-
acc += line.length + 1
|
|
234
|
-
}
|
|
235
|
-
// A blanked fence line is all spaces, so it reads as a block start here too — the fence bounds
|
|
236
|
-
// a span by the same rule as everything else rather than by a happy accident.
|
|
237
|
-
const blockStarts = lineStarts.filter((_start, i) => i > 0 && startsNewBlock(unfenced[i] as string))
|
|
238
|
-
const lineOf = (offset: number): number => {
|
|
239
|
-
let lo = 0
|
|
240
|
-
let hi = lineStarts.length - 1
|
|
241
|
-
while (lo < hi) {
|
|
242
|
-
const mid = (lo + hi + 1) >> 1
|
|
243
|
-
if ((lineStarts[mid] as number) <= offset) lo = mid
|
|
244
|
-
else hi = mid - 1
|
|
245
|
-
}
|
|
246
|
-
return lo
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const spans = scanCodeSpans(scannable, blockStarts)
|
|
250
|
-
let outsideSpans = scannable
|
|
251
|
-
for (const span of spans) {
|
|
252
|
-
outsideSpans =
|
|
253
|
-
outsideSpans.slice(0, span.start) +
|
|
254
|
-
blankOut(outsideSpans.slice(span.start, span.end)) +
|
|
255
|
-
outsideSpans.slice(span.end)
|
|
256
|
-
}
|
|
257
|
-
const outsideLines = outsideSpans.split('\n')
|
|
258
|
-
|
|
259
|
-
// 3. The marker is read from OUTSIDE the code spans — the same rule, not an exception for the
|
|
260
|
-
// escape hatch. Read from the raw line it would fire on a line that merely QUOTES it, which
|
|
261
|
-
// is how this very node documents it, and that line's real references would vanish: an
|
|
262
|
-
// escape hatch a description of the escape hatch can trigger hides exactly what this engine
|
|
263
|
-
// exists to find. A wrapped span is judged by the line it OPENS on — the line a reader
|
|
264
|
-
// would put the marker beside.
|
|
265
|
-
const marked = outsideLines.map((line) => IGNORE_MARKER_RE.test(line))
|
|
266
|
-
|
|
267
|
-
const perLine = new Map<number, Set<string>>()
|
|
268
|
-
const add = (line: number, ref: string) => {
|
|
269
|
-
if (marked[line] === true) return
|
|
270
|
-
const set = perLine.get(line) ?? new Set<string>()
|
|
271
|
-
set.add(ref)
|
|
272
|
-
perLine.set(line, set)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
for (const span of spans) {
|
|
276
|
-
const content = span.content.trim()
|
|
277
|
-
if (RELATIVE_RE.test(content)) add(lineOf(span.start), content)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
outsideLines.forEach((line, i) => {
|
|
281
|
-
const addTarget = (angled: string | undefined, bare: string | undefined) => {
|
|
282
|
-
if (angled !== undefined) {
|
|
283
|
-
if (RELATIVE_IN_ANGLES_RE.test(angled)) add(i, angled)
|
|
284
|
-
return
|
|
285
|
-
}
|
|
286
|
-
if (bare !== undefined && RELATIVE_RE.test(bare)) add(i, bare)
|
|
287
|
-
}
|
|
288
|
-
for (const m of line.matchAll(LINK_RE)) addTarget(m[1], m[2])
|
|
289
|
-
const def = LINK_DEF_RE.exec(line)
|
|
290
|
-
if (def) addTarget(def[1], def[2])
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
const out: Reference[] = []
|
|
294
|
-
for (const line of [...perLine.keys()].sort((a, b) => a - b)) {
|
|
295
|
-
for (const ref of perLine.get(line) as Set<string>) out.push({ line: line + 1, ref })
|
|
296
|
-
}
|
|
297
|
-
return out
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ── Resolution ──
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Where `ref` points, resolved against `fileDir` — the directory of the file that CARRIES it,
|
|
304
|
-
* never the spec root and never the repo root. A trailing `#fragment` is stripped first; a
|
|
305
|
-
* trailing slash is immaterial (`resolve` drops it), so a directory reference resolves either way.
|
|
306
|
-
*/
|
|
307
|
-
export function resolveReference(fileDir: string, ref: string): string {
|
|
308
|
-
const withoutFragment = ref.replace(/#.*$/, '')
|
|
309
|
-
return resolve(fileDir, withoutFragment)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ── The walk ──
|
|
313
|
-
|
|
314
|
-
/** Every `.md` file under `dir`, at any depth, absolute and sorted so the report is stable. */
|
|
315
|
-
export function listMarkdownFiles(dir: string): string[] {
|
|
316
|
-
const out: string[] = []
|
|
317
|
-
const walk = (d: string) => {
|
|
318
|
-
for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
319
|
-
const p = join(d, e.name)
|
|
320
|
-
if (e.isDirectory()) walk(p)
|
|
321
|
-
else if (e.name.endsWith('.md')) out.push(p)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
walk(dir)
|
|
325
|
-
return out
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// ── Audit ──
|
|
329
|
-
|
|
330
|
-
export interface Finding {
|
|
331
|
-
/** Absolute path of the file carrying the reference. */
|
|
332
|
-
file: string
|
|
333
|
-
line: number
|
|
334
|
-
/** The reference exactly as written. */
|
|
335
|
-
ref: string
|
|
336
|
-
/** The absolute path it resolved to — the half a reader cannot supply by eye. */
|
|
337
|
-
resolved: string
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export interface AuditOptions {
|
|
341
|
-
/** Substitute the markdown walk (tests assert which files are visited). */
|
|
342
|
-
listMarkdownFiles?: (dir: string) => string[]
|
|
343
|
-
/** Substitute the file reader (tests assert nothing outside the walk is read). */
|
|
344
|
-
readFile?: (path: string) => string
|
|
345
|
-
/** Substitute the existence probe. */
|
|
346
|
-
exists?: (path: string) => boolean
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Every unresolved reference under `specDir`, ordered by file, then line, then reference — so two
|
|
351
|
-
* runs over an unchanged tree render byte-identically.
|
|
352
|
-
*
|
|
353
|
-
* EVERY finding, never the first: a single off-by-one lands as a whole family of broken
|
|
354
|
-
* references, and reporting one at a time would take as many runs to clear as there are levels
|
|
355
|
-
* wrong.
|
|
356
|
-
*/
|
|
357
|
-
export function audit(specDir: string, options: AuditOptions = {}): Finding[] {
|
|
358
|
-
const list = options.listMarkdownFiles ?? listMarkdownFiles
|
|
359
|
-
const read = options.readFile ?? ((p: string) => readFileSync(p, 'utf8'))
|
|
360
|
-
const has = options.exists ?? existsSync
|
|
361
|
-
|
|
362
|
-
const findings: Finding[] = []
|
|
363
|
-
for (const file of list(specDir)) {
|
|
364
|
-
const fileDir = dirname(file)
|
|
365
|
-
for (const { line, ref } of extractReferences(read(file))) {
|
|
366
|
-
const resolved = resolveReference(fileDir, ref)
|
|
367
|
-
if (!has(resolved)) findings.push({ file, line, ref, resolved })
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return findings.sort((a, b) =>
|
|
371
|
-
a.file !== b.file ? (a.file < b.file ? -1 : 1) : a.line !== b.line ? a.line - b.line : a.ref < b.ref ? -1 : 1,
|
|
372
|
-
)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ── Report ──
|
|
376
|
-
|
|
377
|
-
/** Renders each finding as `file:line: <ref> -> <resolved>`, both paths relative to `base` so the
|
|
378
|
-
* report reads the same from any checkout. */
|
|
379
|
-
export function formatFindings(findings: Finding[], base: string): string {
|
|
380
|
-
if (findings.length === 0) return 'check-spec-references: every relative reference resolves\n'
|
|
381
|
-
const lines = findings.map(
|
|
382
|
-
(f) =>
|
|
383
|
-
` ${relative(base, f.file)}:${f.line}: \`${f.ref}\` -> ${relative(base, f.resolved)} — no file or directory there\n`,
|
|
384
|
-
)
|
|
385
|
-
lines.push(`check-spec-references: ${findings.length} unresolved reference(s)\n`)
|
|
386
|
-
return lines.join('')
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ── CLI ──
|
|
390
|
-
|
|
391
|
-
// One mode, deliberately. A report-only mode would differ from the guard by exit code alone —
|
|
392
|
-
// the same findings, printed the same way — and no actor wants the list without wanting it fixed.
|
|
393
|
-
// Every finding is a defect, so every finding fails the run.
|
|
394
|
-
export function main(argv: string[], cwd: string = process.cwd()): number {
|
|
395
|
-
const i = argv.indexOf('--spec-dir')
|
|
396
|
-
const specDir = i === -1 ? '' : (argv[i + 1] ?? '')
|
|
397
|
-
if (specDir === '') {
|
|
398
|
-
process.stderr.write('check-spec-references: --spec-dir <dir> is required\n')
|
|
399
|
-
return 1
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const dir = resolve(cwd, specDir)
|
|
403
|
-
// A spec dir that is not there is refused by name, never as a raw stack trace: a mistyped path
|
|
404
|
-
// must not read like a corpus that has no markdown in it.
|
|
405
|
-
if (!existsSync(dir)) {
|
|
406
|
-
process.stderr.write(`check-spec-references: no directory at ${relative(cwd, dir)}\n`)
|
|
407
|
-
return 1
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const findings = audit(dir)
|
|
411
|
-
const report = formatFindings(findings, cwd)
|
|
412
|
-
if (findings.length === 0) {
|
|
413
|
-
process.stdout.write(report)
|
|
414
|
-
return 0
|
|
415
|
-
}
|
|
416
|
-
process.stderr.write(report)
|
|
417
|
-
return 1
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
421
|
-
process.exit(main(process.argv.slice(2)))
|
|
422
|
-
}
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
//#region skills/check-spec-structure/scripts/check-spec-structure.mts
|
|
6
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
7
|
-
"node_modules",
|
|
8
|
-
".git",
|
|
9
|
-
"dist",
|
|
10
|
-
".turbo",
|
|
11
|
-
".next",
|
|
12
|
-
"coverage"
|
|
13
|
-
]);
|
|
14
|
-
const DEFAULT_MAX_SCENARIOS = 40;
|
|
15
|
-
const REQUIRED_BEHAVIORAL_SECTIONS = [
|
|
16
|
-
"What",
|
|
17
|
-
"Use Cases",
|
|
18
|
-
"Control Flow",
|
|
19
|
-
"Scenario map"
|
|
20
|
-
];
|
|
21
|
-
function parseNodeFrontmatter(text) {
|
|
22
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
23
|
-
if (!m) return { concepts: [] };
|
|
24
|
-
const fm = { concepts: [] };
|
|
25
|
-
const lines = m[1].split("\n").map((l) => l.replace(/\r$/, ""));
|
|
26
|
-
for (let i = 0; i < lines.length; i++) {
|
|
27
|
-
const line = lines[i];
|
|
28
|
-
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
29
|
-
if (line.length - line.trimStart().length !== 0) continue;
|
|
30
|
-
const [key, ...rest] = line.trim().split(":");
|
|
31
|
-
const value = rest.join(":").trim();
|
|
32
|
-
if (key === "spec-type") fm.specType = unquote(value);
|
|
33
|
-
else if (key === "concept") {
|
|
34
|
-
if (value === "" || value === "|" || value === ">") for (let j = i + 1; j < lines.length; j++) {
|
|
35
|
-
const item = lines[j];
|
|
36
|
-
if (item.trim() === "") continue;
|
|
37
|
-
if (item.length - item.trimStart().length === 0) break;
|
|
38
|
-
const dash = /^\s*-\s+(.*)$/.exec(item);
|
|
39
|
-
if (dash) fm.concepts.push(unquote(dash[1].trim()));
|
|
40
|
-
}
|
|
41
|
-
else fm.concepts.push(...parseScalarOrFlow(value));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return fm;
|
|
45
|
-
}
|
|
46
|
-
function parseScalarOrFlow(value) {
|
|
47
|
-
const v = value.trim();
|
|
48
|
-
if (v.startsWith("[") && v.endsWith("]")) return v.slice(1, -1).split(",").map((s) => unquote(s.trim())).filter((s) => s.length > 0);
|
|
49
|
-
const single = unquote(v);
|
|
50
|
-
return single.length > 0 ? [single] : [];
|
|
51
|
-
}
|
|
52
|
-
function unquote(v) {
|
|
53
|
-
return v.replace(/^["']|["']$/g, "");
|
|
54
|
-
}
|
|
55
|
-
function parseSectionHeadings(text) {
|
|
56
|
-
const headings = [];
|
|
57
|
-
let inFence = false;
|
|
58
|
-
for (const raw of text.split("\n")) {
|
|
59
|
-
const line = raw.replace(/\r$/, "");
|
|
60
|
-
if (/^\s*(```|~~~)/.test(line)) {
|
|
61
|
-
inFence = !inFence;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (inFence) continue;
|
|
65
|
-
const m = /^##\s+(.+?)\s*$/.exec(line);
|
|
66
|
-
if (m) headings.push(m[1].replace(/`/g, "").trim());
|
|
67
|
-
}
|
|
68
|
-
return headings;
|
|
69
|
-
}
|
|
70
|
-
function countScenarios(featureText) {
|
|
71
|
-
let n = 0;
|
|
72
|
-
for (const line of featureText.split("\n")) if (/^\s*Scenario:/.test(line)) n++;
|
|
73
|
-
return n;
|
|
74
|
-
}
|
|
75
|
-
const SECTION_HEADER = /^\s*#\s*(?:──|-{3,})/;
|
|
76
|
-
function profileFeature(featureText) {
|
|
77
|
-
const lines = featureText.split("\n");
|
|
78
|
-
let scenarioCount = 0;
|
|
79
|
-
let taggedCount = 0;
|
|
80
|
-
let clusterCount = 0;
|
|
81
|
-
let pendingRubric = false;
|
|
82
|
-
for (const line of lines) {
|
|
83
|
-
if (/^\s*Scenario:/.test(line)) {
|
|
84
|
-
scenarioCount++;
|
|
85
|
-
if (pendingRubric) taggedCount++;
|
|
86
|
-
pendingRubric = false;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const trimmed = line.trim();
|
|
90
|
-
if (trimmed === "") continue;
|
|
91
|
-
if (trimmed.startsWith("@")) {
|
|
92
|
-
if (/(^|\s)@rubric(\s|$)/.test(trimmed)) pendingRubric = true;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (SECTION_HEADER.test(line)) {
|
|
96
|
-
clusterCount++;
|
|
97
|
-
pendingRubric = false;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
pendingRubric = false;
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
scenarioCount,
|
|
104
|
-
plainCount: scenarioCount - taggedCount,
|
|
105
|
-
taggedCount,
|
|
106
|
-
clusterCount
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function displayPath(relPath) {
|
|
110
|
-
const p = relPath.replace(/\\/g, "/");
|
|
111
|
-
return p.endsWith("/README.md") ? p.slice(0, -9) : p;
|
|
112
|
-
}
|
|
113
|
-
function scanProjectSpec(specDir) {
|
|
114
|
-
const records = [];
|
|
115
|
-
walk(specDir, specDir, records);
|
|
116
|
-
return records.sort((a, b) => a.display.localeCompare(b.display));
|
|
117
|
-
}
|
|
118
|
-
function walk(dir, specDir, out) {
|
|
119
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
120
|
-
for (const entry of entries) {
|
|
121
|
-
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
122
|
-
const full = join(dir, entry.name);
|
|
123
|
-
if (entry.isDirectory()) walk(full, specDir, out);
|
|
124
|
-
else if (entry.name === "README.md") {
|
|
125
|
-
const readmeText = readFileSync(full, "utf8");
|
|
126
|
-
const fm = parseNodeFrontmatter(readmeText);
|
|
127
|
-
if (fm.specType === void 0 && fm.concepts.length === 0) continue;
|
|
128
|
-
const sectionHeadings = parseSectionHeadings(readmeText);
|
|
129
|
-
const features = entries.filter((e) => e.isFile() && e.name.endsWith(".feature"));
|
|
130
|
-
const hasFeature = features.length > 0;
|
|
131
|
-
const profile = hasFeature ? profileFeature(readFileSync(join(dir, features[0].name), "utf8")) : {
|
|
132
|
-
scenarioCount: 0,
|
|
133
|
-
plainCount: 0,
|
|
134
|
-
taggedCount: 0,
|
|
135
|
-
clusterCount: 0
|
|
136
|
-
};
|
|
137
|
-
const relPath = full.slice(specDir.length + 1).replace(/\\/g, "/");
|
|
138
|
-
out.push({
|
|
139
|
-
relPath,
|
|
140
|
-
capability: relPath.split("/")[0],
|
|
141
|
-
display: displayPath(relPath),
|
|
142
|
-
concepts: fm.concepts,
|
|
143
|
-
specType: fm.specType,
|
|
144
|
-
sectionHeadings,
|
|
145
|
-
hasFeature,
|
|
146
|
-
scenarioCount: profile.scenarioCount,
|
|
147
|
-
plainCount: profile.plainCount,
|
|
148
|
-
taggedCount: profile.taggedCount,
|
|
149
|
-
clusterCount: profile.clusterCount
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function checkUntagged(records) {
|
|
155
|
-
return records.filter((r) => r.specType !== void 0 && r.concepts.length === 0).map((r) => ({
|
|
156
|
-
kind: "untagged-node",
|
|
157
|
-
severity: "blocking",
|
|
158
|
-
node: r.display,
|
|
159
|
-
detail: `spec-type: ${r.specType} but no concept tag — orphaned from the by-concept index`
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
162
|
-
function checkOversized(records, maxScenarios) {
|
|
163
|
-
return records.filter((r) => r.scenarioCount > maxScenarios).map((r) => ({
|
|
164
|
-
kind: "oversized-node",
|
|
165
|
-
severity: "advisory",
|
|
166
|
-
node: r.display,
|
|
167
|
-
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`
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
function checkGlossary(specDir) {
|
|
171
|
-
if (existsSync(join(specDir, "glossary.md"))) return [];
|
|
172
|
-
return [{
|
|
173
|
-
kind: "missing-glossary",
|
|
174
|
-
severity: "advisory",
|
|
175
|
-
node: "glossary.md",
|
|
176
|
-
detail: "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"
|
|
177
|
-
}];
|
|
178
|
-
}
|
|
179
|
-
function checkIncomplete(records) {
|
|
180
|
-
return records.filter((r) => r.specType === "behavioral" && r.hasFeature).map((r) => {
|
|
181
|
-
return {
|
|
182
|
-
record: r,
|
|
183
|
-
missing: REQUIRED_BEHAVIORAL_SECTIONS.filter((s) => !r.sectionHeadings.includes(s))
|
|
184
|
-
};
|
|
185
|
-
}).filter(({ missing }) => missing.length > 0).map(({ record, missing }) => ({
|
|
186
|
-
kind: "incomplete-node",
|
|
187
|
-
severity: "advisory",
|
|
188
|
-
node: record.display,
|
|
189
|
-
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)`
|
|
190
|
-
}));
|
|
191
|
-
}
|
|
192
|
-
function audit(records, maxScenarios, specDir) {
|
|
193
|
-
return [
|
|
194
|
-
...checkUntagged(records),
|
|
195
|
-
...checkOversized(records, maxScenarios),
|
|
196
|
-
...checkIncomplete(records),
|
|
197
|
-
...specDir === void 0 ? [] : checkGlossary(specDir)
|
|
198
|
-
];
|
|
199
|
-
}
|
|
200
|
-
function hasBlocking(findings) {
|
|
201
|
-
return findings.some((f) => f.severity === "blocking");
|
|
202
|
-
}
|
|
203
|
-
function renderFindings(findings, specDir) {
|
|
204
|
-
const blocking = findings.filter((f) => f.severity === "blocking");
|
|
205
|
-
const advisory = findings.filter((f) => f.severity === "advisory");
|
|
206
|
-
const lines = [`check-spec-structure: spec-dir=${specDir}`];
|
|
207
|
-
lines.push(`blocking[${blocking.length}]:`);
|
|
208
|
-
for (const f of blocking) lines.push(` ${f.node} — ${f.kind}: ${f.detail}`);
|
|
209
|
-
lines.push(`advisory[${advisory.length}]:`);
|
|
210
|
-
for (const f of advisory) lines.push(` ${f.node} — ${f.kind}: ${f.detail}`);
|
|
211
|
-
lines.push("note: advisory — findings feed the Warden formation pass; the engine writes nothing");
|
|
212
|
-
return lines.join("\n");
|
|
213
|
-
}
|
|
214
|
-
function main(argv) {
|
|
215
|
-
let specDir = ".";
|
|
216
|
-
let mode = "audit";
|
|
217
|
-
let format = "toon";
|
|
218
|
-
let maxScenarios = 40;
|
|
219
|
-
for (let i = 0; i < argv.length; i++) {
|
|
220
|
-
const a = argv[i];
|
|
221
|
-
if (a === "--spec-dir") specDir = argv[++i] ?? ".";
|
|
222
|
-
else if (a === "--check") mode = "check";
|
|
223
|
-
else if (a === "--max-scenarios") maxScenarios = Number(argv[++i] ?? 40);
|
|
224
|
-
else if (a === "--format") format = argv[++i] ?? "toon";
|
|
225
|
-
}
|
|
226
|
-
const findings = audit(scanProjectSpec(specDir), maxScenarios, specDir);
|
|
227
|
-
if (mode === "check") {
|
|
228
|
-
if (hasBlocking(findings)) {
|
|
229
|
-
process.stderr.write(`check-spec-structure: ${findings.filter((f) => f.severity === "blocking").length} blocking finding(s)\n`);
|
|
230
|
-
return 1;
|
|
231
|
-
}
|
|
232
|
-
process.stdout.write("check-spec-structure: no blocking findings\n");
|
|
233
|
-
return 0;
|
|
234
|
-
}
|
|
235
|
-
if (format === "json") process.stdout.write(`${JSON.stringify(findings)}\n`);
|
|
236
|
-
else process.stdout.write(`${renderFindings(findings, specDir)}\n`);
|
|
237
|
-
return 0;
|
|
238
|
-
}
|
|
239
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
240
|
-
//#endregion
|
|
241
|
-
export { DEFAULT_MAX_SCENARIOS, REQUIRED_BEHAVIORAL_SECTIONS, audit, checkGlossary, checkIncomplete, checkOversized, checkUntagged, countScenarios, hasBlocking, main, parseNodeFrontmatter, parseScalarOrFlow, parseSectionHeadings, profileFeature, renderFindings, scanProjectSpec };
|