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,293 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// check-retired-terms — corpus-wide sweep for survivors of a retired path, directory, or naming
|
|
3
|
-
// convention. A design decision retires a term (a path, a convention) and records the retirement
|
|
4
|
-
// once in the registry (.agents/sdd/retired-terms.toml); this engine reads that registry, scans
|
|
5
|
-
// every git-tracked file in the repo — not only the node someone happened to touch — and reports
|
|
6
|
-
// every literal, case-sensitive occurrence as a violation, with the replacement to use.
|
|
7
|
-
//
|
|
8
|
-
// It is the corpus-wide, declared-data sibling of check:metaphor-free (packages/cyberlegion/src/
|
|
9
|
-
// metaphor-free.ts): same "banned term, allow-list, exclusion list, scope" shape, but the banned
|
|
10
|
-
// list here is registry data appended by any CR (a changelog), not a hardcoded package charter.
|
|
11
|
-
//
|
|
12
|
-
// The registry is parsed with a hand-rolled minimal TOML subset — the same spirit as
|
|
13
|
-
// discover-specs's parseAnchorsToml (plugins/sdd/skills/discover-specs/scripts/discover-specs.mts)
|
|
14
|
-
// — an array of `[[retired]]` tables with string keys (term/since/replacement) and string-array
|
|
15
|
-
// keys (scope/allow). `allow` entries are flat strings, either a bare path (sanctions the whole
|
|
16
|
-
// file) or `path :: substring` (sanctions only lines carrying that substring) — deliberately, so
|
|
17
|
-
// the parser never needs nested inline tables.
|
|
18
|
-
//
|
|
19
|
-
// A malformed registry is NOT a fail-safe empty list (contrast discover-specs's readAnchors, which
|
|
20
|
-
// warns and falls back for an unrelated scan): here the registry IS the check, so a parse failure
|
|
21
|
-
// must be loud and non-zero, never a false-clean.
|
|
22
|
-
//
|
|
23
|
-
// Pure functions are exported for node:test; running the file directly drives the CLI. No
|
|
24
|
-
// dependencies (the repo's node-≥23.6 / no-deps convention).
|
|
25
|
-
|
|
26
|
-
import { execFileSync } from 'node:child_process'
|
|
27
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
28
|
-
import { join } from 'node:path'
|
|
29
|
-
import { pathToFileURL } from 'node:url'
|
|
30
|
-
|
|
31
|
-
// ── Registry ──
|
|
32
|
-
|
|
33
|
-
export interface RetiredEntry {
|
|
34
|
-
/** The literal, case-sensitive text that is retired. */
|
|
35
|
-
term: string
|
|
36
|
-
/** The CR that retired it. */
|
|
37
|
-
since: string
|
|
38
|
-
/** What to use instead. */
|
|
39
|
-
replacement: string
|
|
40
|
-
/** Repo-relative include prefixes. Absent/empty means the whole tracked tree. */
|
|
41
|
-
scope?: string[]
|
|
42
|
-
/** Sanctioned occurrences: a bare path (whole file) or "path :: substring" (matching lines only). */
|
|
43
|
-
allow?: string[]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class RegistryParseError extends Error {}
|
|
47
|
-
|
|
48
|
-
const REGISTRY_PATH = '.agents/sdd/retired-terms.toml'
|
|
49
|
-
|
|
50
|
-
function isBlankOrComment(line: string): boolean {
|
|
51
|
-
const t = line.trim()
|
|
52
|
-
return t === '' || t.startsWith('#')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function extractString(block: string, key: string): string {
|
|
56
|
-
const m = new RegExp(`(^|\\n)\\s*${key}\\s*=\\s*"([^"]*)"[^\\n]*`).exec(block)
|
|
57
|
-
if (!m) throw new RegistryParseError(`[[retired]] entry is missing a string "${key}"`)
|
|
58
|
-
return m[2]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// A `[key = [...]]` array, tolerant of the array spanning several lines (as the README's worked
|
|
62
|
-
// example does). An opened-but-never-closed array is a parse error, not a silently empty result —
|
|
63
|
-
// silently swallowing it would be exactly the false-green this guard exists to prevent.
|
|
64
|
-
function extractArray(block: string, key: string): string[] {
|
|
65
|
-
const openRe = new RegExp(`(^|\\n)\\s*${key}\\s*=\\s*\\[`)
|
|
66
|
-
const openMatch = openRe.exec(block)
|
|
67
|
-
if (!openMatch) return []
|
|
68
|
-
const openIdx = openMatch.index + openMatch[0].length - 1 // index of the "["
|
|
69
|
-
const closeIdx = block.indexOf(']', openIdx)
|
|
70
|
-
if (closeIdx === -1) throw new RegistryParseError(`"${key}" array is never closed with "]"`)
|
|
71
|
-
const arrText = block.slice(openIdx, closeIdx)
|
|
72
|
-
const out: string[] = []
|
|
73
|
-
for (const q of arrText.matchAll(/"([^"]*)"/g)) out.push(q[1])
|
|
74
|
-
return out
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Parses the registry's minimal TOML subset: an array of `[[retired]]` tables carrying string
|
|
78
|
-
* keys (term/since/replacement, all required) and optional string-array keys (scope/allow). Throws
|
|
79
|
-
* `RegistryParseError` on anything that does not fit that shape — an absent registry is handled by
|
|
80
|
-
* the caller (`readRegistry`), not here; an empty/comment-only file parses to `[]`. */
|
|
81
|
-
export function parseRegistryToml(text: string): RetiredEntry[] {
|
|
82
|
-
const marker = '[[retired]]'
|
|
83
|
-
const firstIdx = text.indexOf(marker)
|
|
84
|
-
if (firstIdx === -1) {
|
|
85
|
-
if (text.split('\n').every(isBlankOrComment)) return []
|
|
86
|
-
throw new RegistryParseError('no [[retired]] table found and the file is not empty/comment-only')
|
|
87
|
-
}
|
|
88
|
-
const preamble = text.slice(0, firstIdx)
|
|
89
|
-
if (!preamble.split('\n').every(isBlankOrComment)) {
|
|
90
|
-
throw new RegistryParseError('content found before the first [[retired]] table header')
|
|
91
|
-
}
|
|
92
|
-
const blocks = text.slice(firstIdx).split(marker).slice(1)
|
|
93
|
-
return blocks.map((block) => {
|
|
94
|
-
const term = extractString(block, 'term')
|
|
95
|
-
const since = extractString(block, 'since')
|
|
96
|
-
const replacement = extractString(block, 'replacement')
|
|
97
|
-
const scope = extractArray(block, 'scope')
|
|
98
|
-
const allow = extractArray(block, 'allow')
|
|
99
|
-
const entry: RetiredEntry = { term, since, replacement }
|
|
100
|
-
if (scope.length) entry.scope = scope
|
|
101
|
-
if (allow.length) entry.allow = allow
|
|
102
|
-
return entry
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Reads and parses the registry at `<root>/.agents/sdd/retired-terms.toml`. An absent file yields
|
|
107
|
-
* `[]` (an "an absent registry sweeps clean" registry, not an error). A present-but-malformed file
|
|
108
|
-
* throws `RegistryParseError` — the caller must surface that loudly and exit non-zero. */
|
|
109
|
-
export function readRegistry(root: string): RetiredEntry[] {
|
|
110
|
-
const file = join(root, REGISTRY_PATH)
|
|
111
|
-
if (!existsSync(file)) return []
|
|
112
|
-
return parseRegistryToml(readFileSync(file, 'utf8'))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Built-in exclusions — always applied, never configurable ──
|
|
116
|
-
// The rule: a surface whose job is to name the retired term is not drift. Two kinds qualify:
|
|
117
|
-
// (a) the guard's own definition — the registry, this engine's source + test, and this node's own
|
|
118
|
-
// README + .feature (it states the banned text to define it), and
|
|
119
|
-
// (b) durable provenance — every ledger/ directory, and everything under .agents/plans/.
|
|
120
|
-
// Nothing else is excluded: a spec README that merely *mentions* a retired convention is drift.
|
|
121
|
-
|
|
122
|
-
const ENGINE_SOURCE = 'plugins/sdd/skills/check-retired-terms/scripts/check-retired-terms.mts'
|
|
123
|
-
const ENGINE_TEST = 'plugins/sdd/skills/check-retired-terms/scripts/check-retired-terms.test.mts'
|
|
124
|
-
const NODE_README = '.agents/specs/sdd/corpus/retired-terms/README.md'
|
|
125
|
-
const NODE_FEATURE = '.agents/specs/sdd/corpus/retired-terms/retired-terms.feature'
|
|
126
|
-
|
|
127
|
-
const DEFAULT_BUILTIN_EXCLUDED_FILES: readonly string[] = [
|
|
128
|
-
REGISTRY_PATH,
|
|
129
|
-
ENGINE_SOURCE,
|
|
130
|
-
ENGINE_TEST,
|
|
131
|
-
NODE_README,
|
|
132
|
-
NODE_FEATURE,
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
const DEFAULT_BUILTIN_EXCLUDED_PREFIXES: readonly string[] = ['.agents/plans/']
|
|
136
|
-
|
|
137
|
-
// Any file sitting inside a directory literally named "ledger", at any depth (e.g.
|
|
138
|
-
// ".agents/specs/aced/ledger/x.jsonl") — durable provenance, not a directory this guard walks by
|
|
139
|
-
// prefix from the root.
|
|
140
|
-
function isUnderLedgerDir(relPath: string): boolean {
|
|
141
|
-
const segs = relPath.split('/')
|
|
142
|
-
return segs.slice(0, -1).includes('ledger')
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function isBuiltinExcluded(
|
|
146
|
-
relPath: string,
|
|
147
|
-
excludedFiles: readonly string[],
|
|
148
|
-
excludedPrefixes: readonly string[],
|
|
149
|
-
): boolean {
|
|
150
|
-
if (excludedFiles.includes(relPath)) return true
|
|
151
|
-
if (excludedPrefixes.some((p) => relPath.startsWith(p))) return true
|
|
152
|
-
if (isUnderLedgerDir(relPath)) return true
|
|
153
|
-
return false
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── Scope + allow ──
|
|
157
|
-
|
|
158
|
-
function inScope(relPath: string, scope?: string[]): boolean {
|
|
159
|
-
if (!scope || scope.length === 0) return true
|
|
160
|
-
return scope.some((prefix) => relPath.startsWith(prefix))
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
interface ParsedAllow {
|
|
164
|
-
file: string
|
|
165
|
-
substring?: string
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function parseAllowEntry(raw: string): ParsedAllow {
|
|
169
|
-
const idx = raw.indexOf('::')
|
|
170
|
-
if (idx === -1) return { file: raw.trim() }
|
|
171
|
-
return { file: raw.slice(0, idx).trim(), substring: raw.slice(idx + 2).trim() }
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// A bare-path allow entry sanctions every line of that file; a `path :: substring` entry sanctions
|
|
175
|
-
// only lines carrying that substring, leaving the rest of the file guarded.
|
|
176
|
-
function isAllowed(relPath: string, lineText: string, allow: string[] | undefined): boolean {
|
|
177
|
-
if (!allow) return false
|
|
178
|
-
for (const raw of allow) {
|
|
179
|
-
const parsed = parseAllowEntry(raw)
|
|
180
|
-
if (parsed.file !== relPath) continue
|
|
181
|
-
if (parsed.substring === undefined) return true
|
|
182
|
-
if (lineText.includes(parsed.substring)) return true
|
|
183
|
-
}
|
|
184
|
-
return false
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ── Tracked files ──
|
|
188
|
-
|
|
189
|
-
/** The git-tracked file set, repo-relative, one per line. An untracked file is outside the sweep
|
|
190
|
-
* by construction — it is never in this list. */
|
|
191
|
-
export function listTrackedFiles(root: string): string[] {
|
|
192
|
-
const out = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' })
|
|
193
|
-
return out.split('\n').filter((l) => l !== '')
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ── Sweep ──
|
|
197
|
-
|
|
198
|
-
export interface Violation {
|
|
199
|
-
file: string
|
|
200
|
-
line: number
|
|
201
|
-
term: string
|
|
202
|
-
replacement: string
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export interface SweepOptions {
|
|
206
|
-
/** Substitute the tracked-file lister (tests inject a fixed list instead of shelling to git). */
|
|
207
|
-
listTrackedFiles?: (root: string) => string[]
|
|
208
|
-
/** Substitute the built-in excluded-file set (tests probe the exclusion mechanism in isolation). */
|
|
209
|
-
builtinExcludedFiles?: readonly string[]
|
|
210
|
-
/** Substitute the built-in excluded-prefix set. */
|
|
211
|
-
builtinExcludedPrefixes?: readonly string[]
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Sweeps `root` for survivors of every entry in `entries`. `options` lets callers (tests)
|
|
215
|
-
* substitute fixture-scoped config — the tracked-file source and the built-in exclusion lists —
|
|
216
|
-
* in place of the real repo and the real defaults, mirroring `findMetaphorViolations`'s
|
|
217
|
-
* `ScanOptions` (packages/cyberlegion/src/metaphor-free.ts). */
|
|
218
|
-
export function sweep(root: string, entries: RetiredEntry[], options: SweepOptions = {}): Violation[] {
|
|
219
|
-
const listFiles = options.listTrackedFiles ?? listTrackedFiles
|
|
220
|
-
const excludedFiles = options.builtinExcludedFiles ?? DEFAULT_BUILTIN_EXCLUDED_FILES
|
|
221
|
-
const excludedPrefixes = options.builtinExcludedPrefixes ?? DEFAULT_BUILTIN_EXCLUDED_PREFIXES
|
|
222
|
-
|
|
223
|
-
const violations: Violation[] = []
|
|
224
|
-
for (const relPath of listFiles(root)) {
|
|
225
|
-
if (isBuiltinExcluded(relPath, excludedFiles, excludedPrefixes)) continue
|
|
226
|
-
|
|
227
|
-
let text: string
|
|
228
|
-
try {
|
|
229
|
-
text = readFileSync(join(root, relPath), 'utf8')
|
|
230
|
-
} catch {
|
|
231
|
-
continue
|
|
232
|
-
}
|
|
233
|
-
const lines = text.split('\n')
|
|
234
|
-
|
|
235
|
-
for (const entry of entries) {
|
|
236
|
-
if (!inScope(relPath, entry.scope)) continue
|
|
237
|
-
lines.forEach((lineText, i) => {
|
|
238
|
-
if (!lineText.includes(entry.term)) return
|
|
239
|
-
if (isAllowed(relPath, lineText, entry.allow)) return
|
|
240
|
-
violations.push({ file: relPath, line: i + 1, term: entry.term, replacement: entry.replacement })
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return violations.sort((a, b) => (a.file !== b.file ? (a.file < b.file ? -1 : 1) : a.line - b.line))
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ── CLI ──
|
|
248
|
-
|
|
249
|
-
/** Renders `--list` output: one line per registered term with its `since` and `replacement`, or a
|
|
250
|
-
* definitive empty-state line when nothing is registered — never silence. */
|
|
251
|
-
export function formatList(entries: RetiredEntry[]): string {
|
|
252
|
-
if (entries.length === 0) return 'check-retired-terms: no term is registered\n'
|
|
253
|
-
return entries.map((e) => `${e.term} (since ${e.since}) -> ${e.replacement}\n`).join('')
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Renders the sweep report: `file:line:term` plus the declared replacement, one per survivor
|
|
257
|
-
* (every survivor, not just the first), then a count summary. */
|
|
258
|
-
export function formatViolations(violations: Violation[]): string {
|
|
259
|
-
const lines = violations.map((v) => `${v.file}:${v.line}:${v.term} — replace with: ${v.replacement}\n`)
|
|
260
|
-
lines.push(`check-retired-terms: ${violations.length} survivor(s) found\n`)
|
|
261
|
-
return lines.join('')
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export function main(argv: string[]): number {
|
|
265
|
-
const root = argv.includes('--root') ? (argv[argv.indexOf('--root') + 1] ?? '.') : '.'
|
|
266
|
-
const list = argv.includes('--list')
|
|
267
|
-
|
|
268
|
-
let entries: RetiredEntry[]
|
|
269
|
-
try {
|
|
270
|
-
entries = readRegistry(root)
|
|
271
|
-
} catch (err) {
|
|
272
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
273
|
-
process.stderr.write(`check-retired-terms: malformed registry — ${message}\n`)
|
|
274
|
-
return 1
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (list) {
|
|
278
|
-
process.stdout.write(formatList(entries))
|
|
279
|
-
return 0
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const violations = sweep(root, entries)
|
|
283
|
-
if (violations.length === 0) {
|
|
284
|
-
process.stdout.write('check-retired-terms: clean — no survivors found\n')
|
|
285
|
-
return 0
|
|
286
|
-
}
|
|
287
|
-
process.stdout.write(formatViolations(violations))
|
|
288
|
-
return 1
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
292
|
-
process.exit(main(process.argv.slice(2)))
|
|
293
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
//#region skills/check-scenario-overlap/scripts/check-scenario-overlap.mts
|
|
6
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
7
|
-
"node_modules",
|
|
8
|
-
".git",
|
|
9
|
-
"dist",
|
|
10
|
-
".turbo",
|
|
11
|
-
".next",
|
|
12
|
-
"coverage"
|
|
13
|
-
]);
|
|
14
|
-
function normalize(s) {
|
|
15
|
-
return s.trim().replace(/\s+/g, " ").toLowerCase();
|
|
16
|
-
}
|
|
17
|
-
const STEP = /^\s*(Given|When|Then|And|But)\s+(.*\S)\s*$/;
|
|
18
|
-
const SCENARIO = /^\s*Scenario(?: Outline)?:\s*(.*\S)\s*$/;
|
|
19
|
-
const TABLE_ROW = /^\s*\|(.*)\|\s*$/;
|
|
20
|
-
function normalizeRow(line) {
|
|
21
|
-
return (TABLE_ROW.exec(line)?.[1] ?? line).split("|").map((cell) => normalize(cell)).join("|");
|
|
22
|
-
}
|
|
23
|
-
function parseFeature(featureText) {
|
|
24
|
-
const scenarios = [];
|
|
25
|
-
let title = null;
|
|
26
|
-
let steps = [];
|
|
27
|
-
let exampleRows = [];
|
|
28
|
-
const flush = () => {
|
|
29
|
-
if (title !== null) {
|
|
30
|
-
const fingerprint = exampleRows.length === 0 ? steps.map(normalize).join("\n") : `${steps.map(normalize).join("\n")}\n EXAMPLES \n${exampleRows.join("\n")}`;
|
|
31
|
-
scenarios.push({
|
|
32
|
-
title,
|
|
33
|
-
titleKey: normalize(title),
|
|
34
|
-
fingerprint
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
title = null;
|
|
38
|
-
steps = [];
|
|
39
|
-
exampleRows = [];
|
|
40
|
-
};
|
|
41
|
-
for (const line of featureText.split("\n")) {
|
|
42
|
-
const sc = SCENARIO.exec(line);
|
|
43
|
-
if (sc) {
|
|
44
|
-
flush();
|
|
45
|
-
title = sc[1];
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (title === null) continue;
|
|
49
|
-
const st = STEP.exec(line);
|
|
50
|
-
if (st) {
|
|
51
|
-
steps.push(st[2]);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (TABLE_ROW.test(line)) exampleRows.push(normalizeRow(line));
|
|
55
|
-
}
|
|
56
|
-
flush();
|
|
57
|
-
return scenarios;
|
|
58
|
-
}
|
|
59
|
-
function displayPath(relPath) {
|
|
60
|
-
const p = relPath.replace(/\\/g, "/");
|
|
61
|
-
return p.endsWith("/README.md") ? p.slice(0, -9) : p;
|
|
62
|
-
}
|
|
63
|
-
function scanSuites(specDir) {
|
|
64
|
-
const suites = [];
|
|
65
|
-
walk(specDir, specDir, suites);
|
|
66
|
-
return suites.sort((a, b) => a.node.localeCompare(b.node));
|
|
67
|
-
}
|
|
68
|
-
function walk(dir, specDir, out) {
|
|
69
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
70
|
-
for (const entry of entries) {
|
|
71
|
-
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
72
|
-
const full = join(dir, entry.name);
|
|
73
|
-
if (entry.isDirectory()) {
|
|
74
|
-
walk(full, specDir, out);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (!entry.name.endsWith(".feature")) continue;
|
|
78
|
-
const scenarios = parseFeature(readFileSync(full, "utf8"));
|
|
79
|
-
if (scenarios.length === 0) continue;
|
|
80
|
-
const relDir = dir.slice(specDir.length + 1).replace(/\\/g, "/");
|
|
81
|
-
out.push({
|
|
82
|
-
node: displayPath(`${relDir}/`),
|
|
83
|
-
scenarios
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function detect(suites) {
|
|
88
|
-
const byFingerprint = /* @__PURE__ */ new Map();
|
|
89
|
-
const byTitle = /* @__PURE__ */ new Map();
|
|
90
|
-
for (const suite of suites) for (const sc of suite.scenarios) {
|
|
91
|
-
if (sc.fingerprint !== "") {
|
|
92
|
-
let m = byFingerprint.get(sc.fingerprint);
|
|
93
|
-
if (!m) byFingerprint.set(sc.fingerprint, m = /* @__PURE__ */ new Map());
|
|
94
|
-
if (!m.has(suite.node)) m.set(suite.node, sc.title);
|
|
95
|
-
}
|
|
96
|
-
let t = byTitle.get(sc.titleKey);
|
|
97
|
-
if (!t) byTitle.set(sc.titleKey, t = /* @__PURE__ */ new Map());
|
|
98
|
-
let e = t.get(suite.node);
|
|
99
|
-
if (!e) t.set(suite.node, e = {
|
|
100
|
-
fps: /* @__PURE__ */ new Set(),
|
|
101
|
-
title: sc.title
|
|
102
|
-
});
|
|
103
|
-
e.fps.add(sc.fingerprint);
|
|
104
|
-
}
|
|
105
|
-
const candidates = [];
|
|
106
|
-
for (const [, nodeMap] of byFingerprint) {
|
|
107
|
-
if (nodeMap.size < 2) continue;
|
|
108
|
-
const nodes = [...nodeMap.keys()].sort();
|
|
109
|
-
for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) candidates.push({
|
|
110
|
-
kind: "exact-duplicate",
|
|
111
|
-
severity: "blocking",
|
|
112
|
-
nodes: [nodes[i], nodes[j]],
|
|
113
|
-
scenario: nodeMap.get(nodes[i]) ?? "",
|
|
114
|
-
detail: "identical step fingerprint — the same behavior is specified in both nodes; one behavior = one owning node"
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
for (const [, nodeMap] of byTitle) {
|
|
118
|
-
if (nodeMap.size < 2) continue;
|
|
119
|
-
const nodes = [...nodeMap.keys()].sort();
|
|
120
|
-
for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
|
|
121
|
-
const a = nodeMap.get(nodes[i]);
|
|
122
|
-
const b = nodeMap.get(nodes[j]);
|
|
123
|
-
if (!a || !b) continue;
|
|
124
|
-
if ([...a.fps].some((fp) => b.fps.has(fp))) continue;
|
|
125
|
-
candidates.push({
|
|
126
|
-
kind: "title-overlap",
|
|
127
|
-
severity: "advisory",
|
|
128
|
-
nodes: [nodes[i], nodes[j]],
|
|
129
|
-
scenario: a.title,
|
|
130
|
-
detail: "same scenario title, differing steps — a weaker overlap hint the Warden judges"
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return candidates;
|
|
135
|
-
}
|
|
136
|
-
function hasBlocking(candidates) {
|
|
137
|
-
return candidates.some((c) => c.severity === "blocking");
|
|
138
|
-
}
|
|
139
|
-
function renderCandidates(candidates, specDir) {
|
|
140
|
-
const blocking = candidates.filter((c) => c.severity === "blocking");
|
|
141
|
-
const advisory = candidates.filter((c) => c.severity === "advisory");
|
|
142
|
-
const lines = [`check-scenario-overlap: spec-dir=${specDir}`];
|
|
143
|
-
lines.push(`blocking[${blocking.length}]:`);
|
|
144
|
-
for (const c of blocking) lines.push(` ${c.nodes[0]} <-> ${c.nodes[1]} — ${c.kind}: "${c.scenario}" — ${c.detail}`);
|
|
145
|
-
lines.push(`advisory[${advisory.length}]:`);
|
|
146
|
-
for (const c of advisory) lines.push(` ${c.nodes[0]} <-> ${c.nodes[1]} — ${c.kind}: "${c.scenario}" — ${c.detail}`);
|
|
147
|
-
lines.push("note: advisory — candidates feed the Warden formation pass; the engine writes nothing");
|
|
148
|
-
return lines.join("\n");
|
|
149
|
-
}
|
|
150
|
-
function main(argv) {
|
|
151
|
-
let specDir = ".";
|
|
152
|
-
let mode = "audit";
|
|
153
|
-
let format = "toon";
|
|
154
|
-
for (let i = 0; i < argv.length; i++) {
|
|
155
|
-
const a = argv[i];
|
|
156
|
-
if (a === "--spec-dir") specDir = argv[++i] ?? ".";
|
|
157
|
-
else if (a === "--check") mode = "check";
|
|
158
|
-
else if (a === "--format") format = argv[++i] ?? "toon";
|
|
159
|
-
}
|
|
160
|
-
const candidates = detect(scanSuites(specDir));
|
|
161
|
-
if (mode === "check") {
|
|
162
|
-
if (hasBlocking(candidates)) {
|
|
163
|
-
process.stderr.write(`check-scenario-overlap: ${candidates.filter((c) => c.severity === "blocking").length} exact-duplicate candidate(s)\n`);
|
|
164
|
-
return 1;
|
|
165
|
-
}
|
|
166
|
-
process.stdout.write("check-scenario-overlap: no exact-duplicate candidates\n");
|
|
167
|
-
return 0;
|
|
168
|
-
}
|
|
169
|
-
if (format === "json") process.stdout.write(`${JSON.stringify(candidates)}\n`);
|
|
170
|
-
else process.stdout.write(`${renderCandidates(candidates, specDir)}\n`);
|
|
171
|
-
return 0;
|
|
172
|
-
}
|
|
173
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
174
|
-
//#endregion
|
|
175
|
-
export { detect, hasBlocking, main, normalize, parseFeature, renderCandidates, scanSuites };
|