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,214 +0,0 @@
|
|
|
1
|
-
// resolve-tracking — resolve one artifact's tracking signal (tracked | ignored).
|
|
2
|
-
// Self-contained, no deps (repo's node-≥23.6 convention). Spec: the intake/resolve-tracking
|
|
3
|
-
// node of the SDD project spec (repo-only).
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
6
|
-
import { join } from 'node:path'
|
|
7
|
-
import { pathToFileURL } from 'node:url'
|
|
8
|
-
|
|
9
|
-
export type Tracking = 'tracked' | 'ignored'
|
|
10
|
-
|
|
11
|
-
// ─── .sddignore (.agents/sdd/.sddignore) — the universal override valve ───────────────
|
|
12
|
-
//
|
|
13
|
-
// gitignore syntax. Each non-blank, non-comment line is a pattern. A leading `!` marks the
|
|
14
|
-
// path tracked (re-include); any other pattern marks it ignored. Matching is
|
|
15
|
-
// last-match-wins: the LAST rule whose pattern matches the path decides.
|
|
16
|
-
|
|
17
|
-
export interface IgnoreRule {
|
|
18
|
-
negated: boolean // leading `!` — re-includes (tracked)
|
|
19
|
-
regex: RegExp
|
|
20
|
-
source: string // the original pattern text (for notes)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Compile the pattern body (no ! / trailing slash) to a regex fragment:
|
|
24
|
-
// ** spans path separators, * and ? stay within a segment.
|
|
25
|
-
function globToRegExpBody(glob: string): string {
|
|
26
|
-
return glob
|
|
27
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex specials (leave * ?)
|
|
28
|
-
.replace(/\*+/g, (m) => (m.length > 1 ? '.*' : '[^/]*')) // ** spans separators, * does not
|
|
29
|
-
.replace(/\?/g, '[^/]')
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Parse one raw line into a rule. Returns:
|
|
33
|
-
// - a rule for a well-formed pattern
|
|
34
|
-
// - 'skip' for a blank line or a comment (nothing loaded)
|
|
35
|
-
// - null for a malformed line (an empty pattern after stripping ! and trailing /)
|
|
36
|
-
export function parseIgnoreLine(raw: string): IgnoreRule | 'skip' | null {
|
|
37
|
-
const line = raw.replace(/\r$/, '')
|
|
38
|
-
const trimmed = line.trim()
|
|
39
|
-
if (!trimmed || trimmed.startsWith('#')) return 'skip'
|
|
40
|
-
|
|
41
|
-
let body = trimmed
|
|
42
|
-
const negated = body.startsWith('!')
|
|
43
|
-
if (negated) body = body.slice(1)
|
|
44
|
-
const dirOnly = body.endsWith('/')
|
|
45
|
-
if (dirOnly) body = body.slice(0, -1)
|
|
46
|
-
|
|
47
|
-
let anchored = false
|
|
48
|
-
if (body.startsWith('/')) {
|
|
49
|
-
anchored = true
|
|
50
|
-
body = body.slice(1)
|
|
51
|
-
} else if (body.includes('/')) {
|
|
52
|
-
anchored = true
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (body === '') return null // nothing to match — malformed
|
|
56
|
-
|
|
57
|
-
// A leading `**/` matches at any depth; otherwise anchored patterns pin to root and
|
|
58
|
-
// unanchored patterns (no interior slash) match the name at any level. A trailing
|
|
59
|
-
// component (or dir-only pattern) also matches everything under it, mirroring git's
|
|
60
|
-
// "ignore a directory ⇒ ignore its contents".
|
|
61
|
-
const prefix = anchored ? '^' : '^(?:.*/)?'
|
|
62
|
-
const core = globToRegExpBody(body)
|
|
63
|
-
const suffix = '(?:/.*)?$'
|
|
64
|
-
try {
|
|
65
|
-
return { negated, regex: new RegExp(prefix + core + suffix), source: trimmed }
|
|
66
|
-
} catch {
|
|
67
|
-
return null // uncompilable — malformed
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function parseIgnoreFile(text: string): IgnoreRule[] {
|
|
72
|
-
const out: IgnoreRule[] = []
|
|
73
|
-
for (const raw of text.split('\n')) {
|
|
74
|
-
const rule = parseIgnoreLine(raw)
|
|
75
|
-
if (rule && rule !== 'skip') out.push(rule)
|
|
76
|
-
}
|
|
77
|
-
return out
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// A missing file is legal — most projects need no overrides.
|
|
81
|
-
export function loadIgnoreRules(root: string): IgnoreRule[] {
|
|
82
|
-
const path = join(root, '.agents', 'sdd', '.sddignore')
|
|
83
|
-
if (!existsSync(path)) return []
|
|
84
|
-
return parseIgnoreFile(readFileSync(path, 'utf8'))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Last-match-wins: iterate rules in file order; the LAST rule that matches decides.
|
|
88
|
-
// `!` => tracked, else => ignored. No match => null (fall through).
|
|
89
|
-
export function resolveFromIgnore(rules: IgnoreRule[], path: string): Tracking | null {
|
|
90
|
-
let value: Tracking | null = null
|
|
91
|
-
for (const rule of rules) {
|
|
92
|
-
if (rule.regex.test(path)) value = rule.negated ? 'tracked' : 'ignored'
|
|
93
|
-
}
|
|
94
|
-
return value
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ─── kind default — the fixed agent-config location convention ───────────────────────
|
|
98
|
-
//
|
|
99
|
-
// Only skill / subagent / command carry a location-varying convention. Project-private
|
|
100
|
-
// paths (`.agents/{skills,agents,commands}/**`) => ignored; project-public / shipped paths
|
|
101
|
-
// (`skills/**`, `plugins/*/skills/**`, `packages/*/skills/**`, and the agents/commands
|
|
102
|
-
// equivalents) => tracked. agents-section has one AGENTS.md (no location convention); code
|
|
103
|
-
// artifact-types have no universal convention — both fall through with no kind default.
|
|
104
|
-
const KIND_DEFAULT_GLOBS: Record<string, { ignored: string[]; tracked: string[] }> = {
|
|
105
|
-
skill: {
|
|
106
|
-
ignored: ['.agents/skills/**'],
|
|
107
|
-
tracked: ['skills/**', 'plugins/*/skills/**', 'packages/*/skills/**'],
|
|
108
|
-
},
|
|
109
|
-
subagent: {
|
|
110
|
-
ignored: ['.agents/agents/**'],
|
|
111
|
-
tracked: ['plugins/*/agents/**', 'packages/*/agents/**'],
|
|
112
|
-
},
|
|
113
|
-
command: {
|
|
114
|
-
ignored: ['.agents/commands/**'],
|
|
115
|
-
tracked: ['plugins/*/commands/**', 'packages/*/commands/**'],
|
|
116
|
-
},
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function globMatch(glob: string, path: string): boolean {
|
|
120
|
-
return new RegExp(`^${globToRegExpBody(glob)}$`).test(path)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function resolveKindDefault(artifactType: string | undefined, path: string): Tracking | null {
|
|
124
|
-
if (!artifactType) return null
|
|
125
|
-
const kind = KIND_DEFAULT_GLOBS[artifactType]
|
|
126
|
-
if (!kind) return null
|
|
127
|
-
if (kind.ignored.some((g) => globMatch(g, path))) return 'ignored'
|
|
128
|
-
if (kind.tracked.some((g) => globMatch(g, path))) return 'tracked'
|
|
129
|
-
return null
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ─── the four-step resolution ─────────────────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
export interface ResolveInput {
|
|
135
|
-
root: string
|
|
136
|
-
path: string
|
|
137
|
-
artifactType?: string
|
|
138
|
-
explicit?: Tracking
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface ResolveResult {
|
|
142
|
-
value: Tracking
|
|
143
|
-
reason: string
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function resolveTracking(input: ResolveInput): ResolveResult {
|
|
147
|
-
if (input.explicit) {
|
|
148
|
-
return { value: input.explicit, reason: 'explicit override' }
|
|
149
|
-
}
|
|
150
|
-
const rules = loadIgnoreRules(input.root)
|
|
151
|
-
const ignoreMatch = resolveFromIgnore(rules, input.path)
|
|
152
|
-
if (ignoreMatch) {
|
|
153
|
-
return { value: ignoreMatch, reason: '.sddignore' }
|
|
154
|
-
}
|
|
155
|
-
const kindDefault = resolveKindDefault(input.artifactType, input.path)
|
|
156
|
-
if (kindDefault) {
|
|
157
|
-
return { value: kindDefault, reason: `kind-default (${input.artifactType})` }
|
|
158
|
-
}
|
|
159
|
-
return { value: 'tracked', reason: 'fail-closed (no signal)' }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ─── validate the ignore file (no --path given) ───────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
export function validateIgnoreFile(root: string): { ok: boolean; message: string } {
|
|
165
|
-
const path = join(root, '.agents', 'sdd', '.sddignore')
|
|
166
|
-
if (!existsSync(path)) return { ok: true, message: '.sddignore OK (absent, fine)' }
|
|
167
|
-
const text = readFileSync(path, 'utf8')
|
|
168
|
-
const notes: string[] = []
|
|
169
|
-
const lines = text.split('\n')
|
|
170
|
-
lines.forEach((raw, i) => {
|
|
171
|
-
if (parseIgnoreLine(raw) === null) {
|
|
172
|
-
notes.push(`line ${i + 1}: not a valid gitignore pattern: ${raw.trim()}`)
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
if (notes.length > 0) {
|
|
176
|
-
return { ok: false, message: notes.join('\n') }
|
|
177
|
-
}
|
|
178
|
-
const loaded = parseIgnoreFile(text).length
|
|
179
|
-
return { ok: true, message: `.sddignore OK (${loaded} rule(s))` }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ─── CLI ────────────────────────────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
function parseArgs(argv: string[]) {
|
|
185
|
-
const out: Record<string, string> = {}
|
|
186
|
-
for (let i = 0; i < argv.length; i++) {
|
|
187
|
-
if (argv[i].startsWith('--')) {
|
|
188
|
-
out[argv[i].slice(2)] = argv[i + 1]
|
|
189
|
-
i++
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return out
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function main(argv: string[]): void {
|
|
196
|
-
const args = parseArgs(argv)
|
|
197
|
-
const root = args.root ?? '.'
|
|
198
|
-
|
|
199
|
-
if (!args.path) {
|
|
200
|
-
const result = validateIgnoreFile(root)
|
|
201
|
-
process.stdout.write(`${result.message}\n`)
|
|
202
|
-
if (!result.ok) process.exitCode = 1
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const explicit = args.explicit === 'tracked' || args.explicit === 'ignored' ? args.explicit : undefined
|
|
207
|
-
const result = resolveTracking({ root, path: args.path, artifactType: args['artifact-type'], explicit })
|
|
208
|
-
process.stdout.write(`${result.value}\n`)
|
|
209
|
-
process.stdout.write(`reason: ${result.reason}\n`)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
213
|
-
main(process.argv.slice(2))
|
|
214
|
-
}
|
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
4
|
-
import { basename, dirname, join } from "node:path";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
//#region skills/spec-gate/scripts/check-spec-state.mts
|
|
7
|
-
const LIFECYCLE_STATUSES = [
|
|
8
|
-
"draft",
|
|
9
|
-
"approved",
|
|
10
|
-
"implemented",
|
|
11
|
-
"deprecated"
|
|
12
|
-
];
|
|
13
|
-
const GATES = ["spec", "impl"];
|
|
14
|
-
const VERDICTS = [
|
|
15
|
-
"approve",
|
|
16
|
-
"pause",
|
|
17
|
-
"reject"
|
|
18
|
-
];
|
|
19
|
-
const SPEC_TYPES = ["reference", "behavioral"];
|
|
20
|
-
const NODE_FORBIDDEN_FIELDS = [
|
|
21
|
-
"status",
|
|
22
|
-
"project-path",
|
|
23
|
-
"approval",
|
|
24
|
-
"produced-by",
|
|
25
|
-
"aligned",
|
|
26
|
-
"spec-layout",
|
|
27
|
-
"leash"
|
|
28
|
-
];
|
|
29
|
-
function frontmatter(text) {
|
|
30
|
-
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
31
|
-
return m ? m[1].split("\n") : [];
|
|
32
|
-
}
|
|
33
|
-
function prose(text) {
|
|
34
|
-
return text.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");
|
|
35
|
-
}
|
|
36
|
-
function parseSpecState(text) {
|
|
37
|
-
const lines = frontmatter(text);
|
|
38
|
-
let status = "";
|
|
39
|
-
let projectPath = null;
|
|
40
|
-
let approval = null;
|
|
41
|
-
for (let i = 0; i < lines.length; i++) {
|
|
42
|
-
const s = /^status:\s*(.+)$/.exec(lines[i]);
|
|
43
|
-
if (s) {
|
|
44
|
-
status = s[1].trim().replace(/^["']|["']$/g, "");
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const p = /^project-path:\s*(.+)$/.exec(lines[i]);
|
|
48
|
-
if (p) {
|
|
49
|
-
projectPath = p[1].trim().replace(/^["']|["']$/g, "");
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const ap = /^approval:\s*(.*)$/.exec(lines[i]);
|
|
53
|
-
if (ap) {
|
|
54
|
-
approval = {};
|
|
55
|
-
if (ap[1].trim() && ap[1].trim() !== "{}") continue;
|
|
56
|
-
let gate = null;
|
|
57
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
58
|
-
if (!/^\s/.test(lines[j])) break;
|
|
59
|
-
const g = /^ {2}(\w+):\s*$/.exec(lines[j]);
|
|
60
|
-
if (g) {
|
|
61
|
-
gate = g[1];
|
|
62
|
-
approval[gate] = { hasWhy: false };
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (!gate) continue;
|
|
66
|
-
const verdict = /^ {4}verdict:\s*(.+)$/.exec(lines[j]);
|
|
67
|
-
if (verdict) approval[gate].verdict = verdict[1].trim().replace(/^["']|["']$/g, "");
|
|
68
|
-
const by = /^ {4}by:\s*(.+)$/.exec(lines[j]);
|
|
69
|
-
if (by) approval[gate].by = by[1].trim().replace(/^["']|["']$/g, "");
|
|
70
|
-
if (/^ {4}why:/.test(lines[j])) approval[gate].hasWhy = true;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const markerCount = (prose(text).match(/<!--\s*open:/g) ?? []).length;
|
|
75
|
-
return {
|
|
76
|
-
status,
|
|
77
|
-
projectPath,
|
|
78
|
-
markerCount,
|
|
79
|
-
approval
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
function checkSpec(slug, state) {
|
|
83
|
-
const { status, markerCount, approval } = state;
|
|
84
|
-
const v = [];
|
|
85
|
-
const tag = (msg) => v.push(`${slug}: ${msg}`);
|
|
86
|
-
if (!LIFECYCLE_STATUSES.includes(status)) tag(status === "" ? `no lifecycle status in frontmatter — a spec.md must declare status (${LIFECYCLE_STATUSES.join(" | ")}), and one that does not is checked by nothing` : `status "${status}" is not in the lifecycle enum (${LIFECYCLE_STATUSES.join(" | ")}) — discovery drops it, so it is checked by nothing`);
|
|
87
|
-
if ((status === "approved" || status === "implemented") && markerCount > 0) tag(`illegal state — ${markerCount} open marker(s) but status is ${status} (markers block the gate)`);
|
|
88
|
-
if (approval) for (const [gate, entry] of Object.entries(approval)) {
|
|
89
|
-
if (!GATES.includes(gate)) tag(`approval has unknown gate "${gate}" (expected spec | impl)`);
|
|
90
|
-
if (entry.verdict && !VERDICTS.includes(entry.verdict)) tag(`approval.${gate} has unknown verdict "${entry.verdict}" (expected approve | pause | reject)`);
|
|
91
|
-
if (entry.verdict === "pause" && entry.by) tag(`approval.${gate} is a pause but carries by — a pause is always the agent's act and omits by`);
|
|
92
|
-
if (entry.verdict === "approve" && !entry.by) tag(`approval.${gate} is an approve with no by — an approve must record its approver`);
|
|
93
|
-
if (entry.by === "agent" && !entry.hasWhy) tag(`approval.${gate} is by:agent but has no why block (a self-assertion must record its derivation)`);
|
|
94
|
-
const passed = gate === "spec" && (status === "approved" || status === "implemented") || gate === "impl" && status === "implemented";
|
|
95
|
-
if (entry.verdict === "pause" && passed) tag(`approval.${gate} is a pause but the ${gate} gate is already passed (status ${status})`);
|
|
96
|
-
}
|
|
97
|
-
if ((status === "approved" || status === "implemented") && !(approval?.spec?.verdict === "approve" && approval?.spec?.by)) tag(`status is ${status} but approval.spec has no approve verdict with an approver — the spec gate has no recorded ratification`);
|
|
98
|
-
if (status === "implemented" && !(approval?.impl?.verdict === "approve" && approval?.impl?.by)) tag("status is implemented but approval.impl has no approve verdict with an approver — the impl gate has no recorded ratification");
|
|
99
|
-
return v;
|
|
100
|
-
}
|
|
101
|
-
const PATH_PREFIXES = [
|
|
102
|
-
".agents/",
|
|
103
|
-
"plugins/",
|
|
104
|
-
"packages/",
|
|
105
|
-
"apps/",
|
|
106
|
-
"docs/",
|
|
107
|
-
".claude/"
|
|
108
|
-
];
|
|
109
|
-
function extractPathRefs(text) {
|
|
110
|
-
const out = [];
|
|
111
|
-
const re = /`([^`\n]+)`/g;
|
|
112
|
-
let m;
|
|
113
|
-
while (m = re.exec(text)) {
|
|
114
|
-
const token = m[1].trim();
|
|
115
|
-
if (/[<*]/.test(token)) continue;
|
|
116
|
-
const isRelative = token.startsWith("./") || token.startsWith("../");
|
|
117
|
-
const isRooted = PATH_PREFIXES.some((p) => token.startsWith(p));
|
|
118
|
-
if (isRelative || isRooted) out.push(token);
|
|
119
|
-
}
|
|
120
|
-
return out;
|
|
121
|
-
}
|
|
122
|
-
function introducedPathRefs(baselineText, currentText) {
|
|
123
|
-
const baseRefs = new Set(extractPathRefs(baselineText));
|
|
124
|
-
return extractPathRefs(currentText).filter((ref) => !baseRefs.has(ref));
|
|
125
|
-
}
|
|
126
|
-
function checkReferencedArtifacts(slug, dir, text, baselineText = "") {
|
|
127
|
-
const v = [];
|
|
128
|
-
const tag = (msg) => v.push(`${slug}: introduces unresolved reference \`${msg}\` — surfaced for judgment`);
|
|
129
|
-
for (const ref of introducedPathRefs(baselineText, text)) {
|
|
130
|
-
const clean = ref.replace(/#.*$/, "");
|
|
131
|
-
const resolved = clean.startsWith("./") || clean.startsWith("../") ? join(dir, clean) : clean;
|
|
132
|
-
if (!existsSync(resolved)) tag(clean);
|
|
133
|
-
}
|
|
134
|
-
return v;
|
|
135
|
-
}
|
|
136
|
-
function parseNode(text) {
|
|
137
|
-
let type = null;
|
|
138
|
-
const lifecycleFields = [];
|
|
139
|
-
for (const l of frontmatter(text)) {
|
|
140
|
-
const key = /^([\w-]+):/.exec(l);
|
|
141
|
-
if (!key) continue;
|
|
142
|
-
if (key[1] === "spec-type") {
|
|
143
|
-
const m = /^spec-type:\s*(.+)$/.exec(l);
|
|
144
|
-
if (m) type = m[1].trim().replace(/^["']|["']$/g, "");
|
|
145
|
-
} else if (NODE_FORBIDDEN_FIELDS.includes(key[1])) lifecycleFields.push(key[1]);
|
|
146
|
-
}
|
|
147
|
-
const body = prose(text);
|
|
148
|
-
return {
|
|
149
|
-
type,
|
|
150
|
-
hasSubject: /^##\s+Subject\b/m.test(body),
|
|
151
|
-
hasUseCases: /^##\s+Use Cases\b/m.test(body),
|
|
152
|
-
lifecycleFields
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
function checkNode(slug, node, hasFeature) {
|
|
156
|
-
const { type, hasSubject, hasUseCases, lifecycleFields } = node;
|
|
157
|
-
const v = [];
|
|
158
|
-
const tag = (msg) => v.push(`${slug}: ${msg}`);
|
|
159
|
-
for (const f of lifecycleFields) tag(`carries lifecycle field "${f}" — lifecycle frontmatter is root-spec.md-only (a node README carries only spec-type)`);
|
|
160
|
-
if (type === null) return v;
|
|
161
|
-
if (!SPEC_TYPES.includes(type)) {
|
|
162
|
-
tag(`unknown spec-type "${type}" (expected reference | behavioral)`);
|
|
163
|
-
return v;
|
|
164
|
-
}
|
|
165
|
-
if (type === "reference") {
|
|
166
|
-
if (hasFeature) tag("spec-type: reference but a sibling .feature exists — a reference artifact is suite-less by design");
|
|
167
|
-
if (!hasSubject) tag("spec-type: reference but no ## Subject section");
|
|
168
|
-
}
|
|
169
|
-
if (type === "behavioral" && !hasUseCases) tag("spec-type: behavioral but no ## Use Cases section");
|
|
170
|
-
return v;
|
|
171
|
-
}
|
|
172
|
-
function parseLedgerGates(text) {
|
|
173
|
-
const gates = [];
|
|
174
|
-
for (const line of text.split("\n")) {
|
|
175
|
-
const s = line.trim();
|
|
176
|
-
if (!s) continue;
|
|
177
|
-
let obj;
|
|
178
|
-
try {
|
|
179
|
-
obj = JSON.parse(s);
|
|
180
|
-
} catch {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (obj.kind === "gate" && typeof obj.gate === "string" && typeof obj.verdict === "string") gates.push({
|
|
184
|
-
gate: obj.gate,
|
|
185
|
-
verdict: obj.verdict
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
return gates;
|
|
189
|
-
}
|
|
190
|
-
function readLedgerText(root, slug) {
|
|
191
|
-
const parts = [];
|
|
192
|
-
const legacy = join(root, slug, "ledger.jsonl");
|
|
193
|
-
if (existsSync(legacy)) parts.push(readFileSync(legacy, "utf8"));
|
|
194
|
-
const dir = join(root, slug, "ledger");
|
|
195
|
-
if (existsSync(dir)) {
|
|
196
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) if (e.isFile() && e.name.endsWith(".jsonl")) parts.push(readFileSync(join(dir, e.name), "utf8"));
|
|
197
|
-
}
|
|
198
|
-
return parts.join("\n");
|
|
199
|
-
}
|
|
200
|
-
function checkGateFloor(slug, status, gates) {
|
|
201
|
-
const v = [];
|
|
202
|
-
const tag = (msg) => v.push(`${slug}: ${msg}`);
|
|
203
|
-
const approved = (gate) => gates.some((g) => g.gate === gate && g.verdict === "approve");
|
|
204
|
-
if ((status === "approved" || status === "implemented") && !approved("spec")) tag(`status is ${status} but the ledger has no spec gate approve line — the durable gate floor is missing`);
|
|
205
|
-
if (status === "implemented" && !approved("impl")) tag("status is implemented but the ledger has no impl gate approve line — the durable gate floor is missing");
|
|
206
|
-
return v;
|
|
207
|
-
}
|
|
208
|
-
function hasFeatureFile(dir) {
|
|
209
|
-
try {
|
|
210
|
-
return readdirSync(dir).some((f) => f.endsWith(".feature"));
|
|
211
|
-
} catch {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function discoverDirsWith(root, name) {
|
|
216
|
-
const out = [];
|
|
217
|
-
const walk = (dir, rel) => {
|
|
218
|
-
let entries;
|
|
219
|
-
try {
|
|
220
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
221
|
-
} catch {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (entries.some((e) => e.isFile() && e.name === name)) out.push(rel);
|
|
225
|
-
for (const e of entries) {
|
|
226
|
-
if (!e.isDirectory() || e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
227
|
-
walk(join(dir, e.name), rel ? join(rel, e.name) : e.name);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
walk(root, "");
|
|
231
|
-
return out;
|
|
232
|
-
}
|
|
233
|
-
const discoverSpecDirs = (root) => discoverDirsWith(root, "spec.md");
|
|
234
|
-
const discoverNodeDirs = (root) => discoverDirsWith(root, "README.md");
|
|
235
|
-
function parseFilesArg(argv) {
|
|
236
|
-
const idx = argv.indexOf("--files");
|
|
237
|
-
if (idx === -1) return [];
|
|
238
|
-
const paths = [];
|
|
239
|
-
for (let i = idx + 1; i < argv.length; i++) {
|
|
240
|
-
if (argv[i].startsWith("--")) break;
|
|
241
|
-
paths.push(argv[i]);
|
|
242
|
-
}
|
|
243
|
-
return paths;
|
|
244
|
-
}
|
|
245
|
-
function readBaselineFromGit(base, path) {
|
|
246
|
-
try {
|
|
247
|
-
return execFileSync("git", ["show", `${base}:${path}`], {
|
|
248
|
-
encoding: "utf8",
|
|
249
|
-
stdio: [
|
|
250
|
-
"ignore",
|
|
251
|
-
"pipe",
|
|
252
|
-
"ignore"
|
|
253
|
-
]
|
|
254
|
-
});
|
|
255
|
-
} catch {
|
|
256
|
-
return "";
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
function checkReferencedArtifactsInFiles(paths, baseline = () => "") {
|
|
260
|
-
const findings = [];
|
|
261
|
-
const violations = [];
|
|
262
|
-
for (const p of paths) {
|
|
263
|
-
let text;
|
|
264
|
-
try {
|
|
265
|
-
text = readFileSync(p, "utf8");
|
|
266
|
-
} catch {
|
|
267
|
-
violations.push(`${p}: cannot read file`);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
findings.push(...checkReferencedArtifacts(p, dirname(p), text, baseline(p)));
|
|
271
|
-
}
|
|
272
|
-
return {
|
|
273
|
-
findings,
|
|
274
|
-
violations
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
function isUnderSpecTree(path) {
|
|
278
|
-
return /(^|\/)\.agents\/specs?(\/|$)/.test(path);
|
|
279
|
-
}
|
|
280
|
-
function filterProseMdInSpecTree(paths) {
|
|
281
|
-
return paths.filter((p) => p.endsWith(".md") && isUnderSpecTree(p));
|
|
282
|
-
}
|
|
283
|
-
function splitTableRow(line) {
|
|
284
|
-
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
285
|
-
}
|
|
286
|
-
function extractSection(body, heading) {
|
|
287
|
-
const start = new RegExp(`(^|\\n)##\\s+${heading}\\b`).exec(body);
|
|
288
|
-
if (!start) return null;
|
|
289
|
-
const rest = body.slice(start.index + start[0].length);
|
|
290
|
-
const end = /\n##\s/.exec(rest);
|
|
291
|
-
return end ? rest.slice(0, end.index) : rest;
|
|
292
|
-
}
|
|
293
|
-
function extractUseCaseScenarioRefs(text) {
|
|
294
|
-
const section = extractSection(text.replace(/```[\s\S]*?```/g, ""), "Use Cases");
|
|
295
|
-
if (section === null) return {
|
|
296
|
-
hasSection: false,
|
|
297
|
-
refs: [],
|
|
298
|
-
unparseable: []
|
|
299
|
-
};
|
|
300
|
-
const lines = section.split("\n");
|
|
301
|
-
const headerIdx = lines.findIndex((l) => l.trim().startsWith("|"));
|
|
302
|
-
if (headerIdx === -1) return {
|
|
303
|
-
hasSection: true,
|
|
304
|
-
refs: [],
|
|
305
|
-
unparseable: []
|
|
306
|
-
};
|
|
307
|
-
const scenarioIdx = splitTableRow(lines[headerIdx]).findIndex((c) => /^scenario$/i.test(c));
|
|
308
|
-
if (scenarioIdx === -1) return {
|
|
309
|
-
hasSection: true,
|
|
310
|
-
refs: [],
|
|
311
|
-
unparseable: []
|
|
312
|
-
};
|
|
313
|
-
const refs = [];
|
|
314
|
-
const unparseable = [];
|
|
315
|
-
for (let i = headerIdx + 2; i < lines.length; i++) {
|
|
316
|
-
if (!lines[i].trim().startsWith("|")) break;
|
|
317
|
-
const cell = splitTableRow(lines[i])[scenarioIdx] ?? "";
|
|
318
|
-
const ref = /`([^`\n]+)`/.exec(cell);
|
|
319
|
-
if (ref) refs.push(ref[1].trim());
|
|
320
|
-
else unparseable.push(lines[i].trim());
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
hasSection: true,
|
|
324
|
-
refs,
|
|
325
|
-
unparseable
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
function escapeRegExp(s) {
|
|
329
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
330
|
-
}
|
|
331
|
-
function resolveScenarioRef(ref, featureText) {
|
|
332
|
-
if (ref.startsWith("@")) return new RegExp(`(^|\\s)${escapeRegExp(ref)}(\\s|$)`, "m").test(featureText);
|
|
333
|
-
const m = /^Scenario:\s*(.+)$/.exec(ref);
|
|
334
|
-
if (!m) return false;
|
|
335
|
-
const title = m[1].trim();
|
|
336
|
-
return new RegExp(`^\\s*Scenario:\\s*${escapeRegExp(title)}\\s*$`, "m").test(featureText);
|
|
337
|
-
}
|
|
338
|
-
function findSiblingFeature(dir) {
|
|
339
|
-
let entries;
|
|
340
|
-
try {
|
|
341
|
-
entries = readdirSync(dir).filter((f) => f.endsWith(".feature"));
|
|
342
|
-
} catch {
|
|
343
|
-
return null;
|
|
344
|
-
}
|
|
345
|
-
if (entries.length === 0) return null;
|
|
346
|
-
const named = `${basename(dir)}.feature`;
|
|
347
|
-
if (entries.includes(named)) return join(dir, named);
|
|
348
|
-
return join(dir, entries[0]);
|
|
349
|
-
}
|
|
350
|
-
function checkUseCaseCoverage(slug, dir, text) {
|
|
351
|
-
const v = [];
|
|
352
|
-
const tag = (msg) => v.push(`${slug}: ${msg}`);
|
|
353
|
-
const { hasSection, refs, unparseable } = extractUseCaseScenarioRefs(text);
|
|
354
|
-
if (!hasSection) return v;
|
|
355
|
-
for (const raw of unparseable) tag(`Use Cases data row has no backtick-wrapped Scenario cell — ${raw}`);
|
|
356
|
-
if (refs.length === 0) return v;
|
|
357
|
-
const featurePath = findSiblingFeature(dir);
|
|
358
|
-
const featureText = featurePath ? readFileSync(featurePath, "utf8") : "";
|
|
359
|
-
for (const ref of refs) if (!resolveScenarioRef(ref, featureText)) tag(`Use Cases table names scenario \`${ref}\` that does not resolve in the sibling .feature`);
|
|
360
|
-
return v;
|
|
361
|
-
}
|
|
362
|
-
function checkUseCaseCoverageInFiles(paths) {
|
|
363
|
-
const violations = [];
|
|
364
|
-
for (const p of paths) {
|
|
365
|
-
let text;
|
|
366
|
-
try {
|
|
367
|
-
text = readFileSync(p, "utf8");
|
|
368
|
-
} catch {
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
violations.push(...checkUseCaseCoverage(p, dirname(p), text));
|
|
372
|
-
}
|
|
373
|
-
return violations;
|
|
374
|
-
}
|
|
375
|
-
function main(argv) {
|
|
376
|
-
if (argv.includes("--files")) {
|
|
377
|
-
const paths = parseFilesArg(argv);
|
|
378
|
-
if (paths.length === 0) {
|
|
379
|
-
console.error("✗ --files requires at least one .md path under the spec tree");
|
|
380
|
-
return 1;
|
|
381
|
-
}
|
|
382
|
-
const proseFiles = filterProseMdInSpecTree(paths);
|
|
383
|
-
const base = argv.includes("--base") ? argv[argv.indexOf("--base") + 1] : void 0;
|
|
384
|
-
const { findings, violations: refReadErrors } = checkReferencedArtifactsInFiles(proseFiles, base ? (p) => readBaselineFromGit(base, p) : () => "");
|
|
385
|
-
const ucViolations = checkUseCaseCoverageInFiles(proseFiles);
|
|
386
|
-
const violations = [...refReadErrors, ...ucViolations];
|
|
387
|
-
for (const f of findings) process.stdout.write(`⚠ ${f}\n`);
|
|
388
|
-
if (violations.length) {
|
|
389
|
-
for (const line of violations) console.error(`✗ ${line}`);
|
|
390
|
-
return 1;
|
|
391
|
-
}
|
|
392
|
-
process.stdout.write(findings.length ? `referenced-artifact findings surfaced for judgment (${findings.length}); use-case-coverage OK\n` : "referenced-artifact and use-case-coverage checks OK\n");
|
|
393
|
-
return 0;
|
|
394
|
-
}
|
|
395
|
-
const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] : ".agents/specs";
|
|
396
|
-
let violations = [];
|
|
397
|
-
for (const slug of discoverSpecDirs(root)) {
|
|
398
|
-
const specPath = join(root, slug, "spec.md");
|
|
399
|
-
if (!existsSync(specPath)) continue;
|
|
400
|
-
const state = parseSpecState(readFileSync(specPath, "utf8"));
|
|
401
|
-
violations = violations.concat(checkSpec(slug, state));
|
|
402
|
-
const gates = parseLedgerGates(readLedgerText(root, slug));
|
|
403
|
-
violations = violations.concat(checkGateFloor(slug, state.status, gates));
|
|
404
|
-
}
|
|
405
|
-
for (const slug of discoverNodeDirs(root)) {
|
|
406
|
-
const dir = join(root, slug);
|
|
407
|
-
const readme = join(dir, "README.md");
|
|
408
|
-
if (!existsSync(readme)) continue;
|
|
409
|
-
violations = violations.concat(checkNode(slug, parseNode(readFileSync(readme, "utf8")), hasFeatureFile(dir)));
|
|
410
|
-
}
|
|
411
|
-
if (violations.length) {
|
|
412
|
-
for (const line of violations) console.error(`✗ ${line}`);
|
|
413
|
-
return 1;
|
|
414
|
-
}
|
|
415
|
-
process.stdout.write("spec states OK\n");
|
|
416
|
-
return 0;
|
|
417
|
-
}
|
|
418
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
419
|
-
//#endregion
|
|
420
|
-
export { checkGateFloor, checkNode, checkReferencedArtifacts, checkReferencedArtifactsInFiles, checkSpec, checkUseCaseCoverage, checkUseCaseCoverageInFiles, discoverNodeDirs, discoverSpecDirs, extractPathRefs, extractUseCaseScenarioRefs, filterProseMdInSpecTree, findSiblingFeature, introducedPathRefs, isUnderSpecTree, main, parseFilesArg, parseLedgerGates, parseNode, parseSpecState, readLedgerText, resolveScenarioRef };
|