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.
Files changed (150) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/CHANGELOG.md +320 -0
  4. package/agents/sdd-scanner.md +1 -1
  5. package/package.json +11 -12
  6. package/plugin.json +55 -0
  7. package/skills/align-spec/SKILL.md +1 -1
  8. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  9. package/skills/blast-estimate/README.md +3 -3
  10. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  11. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  12. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  13. package/skills/check-plan-safety/SKILL.md +2 -3
  14. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  15. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  16. package/skills/check-project-specs/SKILL.md +1 -1
  17. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  18. package/skills/check-retired-terms/SKILL.md +2 -3
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  20. package/skills/check-scenario-overlap/SKILL.md +1 -2
  21. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  22. package/skills/check-spec-references/SKILL.md +2 -2
  23. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  24. package/skills/check-spec-structure/SKILL.md +1 -2
  25. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  26. package/skills/collision-ladder/README.md +2 -2
  27. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  28. package/skills/combat-log-governance/SKILL.md +1 -1
  29. package/skills/concept-index/SKILL.md +1 -1
  30. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  31. package/skills/discover-plans/README.md +3 -3
  32. package/skills/discover-plans/SKILL.md +1 -1
  33. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  34. package/skills/discover-specs/README.md +3 -3
  35. package/skills/discover-specs/SKILL.md +1 -1
  36. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  37. package/skills/doctrine-loop/SKILL.md +1 -1
  38. package/skills/gate-validation-governance/SKILL.md +1 -1
  39. package/skills/init/README.md +2 -2
  40. package/skills/init/scripts/wire-statusline.mjs +168 -12
  41. package/skills/manage-ignore/README.md +3 -3
  42. package/skills/manage-ignore/SKILL.md +1 -2
  43. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  44. package/skills/manage-scenario-bridge/README.md +3 -3
  45. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  46. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  47. package/skills/manage-spec-anchors/README.md +3 -3
  48. package/skills/manage-spec-anchors/SKILL.md +1 -1
  49. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  50. package/skills/mission-graph/README.md +2 -2
  51. package/skills/mission-graph/SKILL.md +1 -1
  52. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  53. package/skills/pause-mission/SKILL.md +1 -1
  54. package/skills/place-node/SKILL.md +2 -2
  55. package/skills/place-node/scripts/place-node.mjs +117 -12
  56. package/skills/plan-retirement/README.md +2 -2
  57. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  58. package/skills/resolve-governances/README.md +3 -3
  59. package/skills/resolve-governances/SKILL.md +1 -1
  60. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  61. package/skills/resolve-tracking/SKILL.md +1 -1
  62. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  63. package/skills/scaffold-project-spec/SKILL.md +1 -1
  64. package/skills/spec-gate/README.md +12 -9
  65. package/skills/spec-gate/SKILL.md +2 -2
  66. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  67. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  68. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  69. package/skills/spec-producer-governance/SKILL.md +2 -2
  70. package/skills/start-mission/SKILL.md +1 -1
  71. package/skills/suite-format-governance/README.md +1 -1
  72. package/skills/suite-format-governance/SKILL.md +1 -1
  73. package/skills/touch-set-correction/README.md +5 -5
  74. package/skills/touch-set-correction/SKILL.md +1 -1
  75. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  76. package/skills/verify-scenarios/README.md +6 -6
  77. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  78. package/.plugin/pins.json +0 -3
  79. package/.plugin/plugin.json +0 -19
  80. package/dist/align-spec/scripts/align-spec.js +0 -102
  81. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  82. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  83. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  84. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  85. package/dist/concept-index/scripts/concept-index.js +0 -181
  86. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  87. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  88. package/dist/spec-gate/scripts/check-suite.js +0 -317
  89. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  90. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  91. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  92. package/skills/align-spec/scripts/align-spec.mts +0 -188
  93. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  94. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  95. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  96. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  97. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  98. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  99. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  100. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  102. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  103. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  104. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  105. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  106. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  108. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  109. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  110. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  111. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  112. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  113. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  114. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  115. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  116. package/skills/concept-index/scripts/concept-index.mts +0 -246
  117. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  118. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  119. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  120. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  121. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  124. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  126. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  128. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  129. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  130. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  131. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  132. package/skills/place-node/scripts/place-node.mts +0 -158
  133. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  134. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  135. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  136. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  137. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  138. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  139. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  140. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  141. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  142. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  143. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  144. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  145. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  146. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  147. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  148. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  149. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  150. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,370 +0,0 @@
1
- #!/usr/bin/env node
2
- // Runs every project-spec engine against the ONE project spec that governs the
3
- // invoking package — resolved from the spec's own `project-path`, never from a
4
- // hardcoded path. Pure functions are exported for node:test; running the file
5
- // directly drives the CLI. No dependencies — plain node strips the types.
6
- //
7
- // The resolution is deliberately spec-first: a package knows its own directory,
8
- // and exactly one spec declares `project-path` pointing at it. The reverse map
9
- // (project dir -> spec dir) is irregular and cannot be derived by name —
10
- // `plugins/cyberfleet` is governed by `.agents/specs/cyberfleet-plugin`.
11
-
12
- import { execFileSync } from 'node:child_process'
13
- import { existsSync, readFileSync, realpathSync } from 'node:fs'
14
- import { dirname, join, relative, resolve } from 'node:path'
15
- import { fileURLToPath, pathToFileURL } from 'node:url'
16
- import {
17
- collectSpecs,
18
- discoverSpecFiles,
19
- parseFrontmatter,
20
- type SpecRecord,
21
- } from '../../discover-specs/scripts/discover-specs.mts'
22
-
23
- const HERE = fileURLToPath(import.meta.url)
24
-
25
- // ─── the engine set ───────────────────────────────────────────────────────────
26
-
27
- // Each engine is handed the resolved spec dir. The two `--root` engines are
28
- // corpus-shaped (they treat the first path segment under root as a project
29
- // slug) but accept a single project-spec dir as root — the slug is only a
30
- // message tag, so a one-project root reports paths relative to that project.
31
- interface Engine {
32
- name: string
33
- script: string
34
- args: (specDir: string) => string[]
35
- }
36
-
37
- export const ENGINES: Engine[] = [
38
- { name: 'check-spec-state', script: 'spec-gate/scripts/check-spec-state.mts', args: (d) => ['--root', d] },
39
- { name: 'check-suite', script: 'spec-gate/scripts/check-suite.mts', args: (d) => ['--root', d] },
40
- {
41
- name: 'concept-index',
42
- script: 'concept-index/scripts/concept-index.mts',
43
- args: (d) => ['--spec-dir', d, '--check'],
44
- },
45
- {
46
- name: 'check-spec-structure',
47
- script: 'check-spec-structure/scripts/check-spec-structure.mts',
48
- args: (d) => ['--spec-dir', d, '--check'],
49
- },
50
- { name: 'align-spec', script: 'align-spec/scripts/align-spec.mts', args: (d) => ['--spec-dir', d, '--check'] },
51
- {
52
- name: 'check-spec-references',
53
- script: 'check-spec-references/scripts/check-spec-references.mts',
54
- // No --check: the engine has one mode, because every finding it makes is a defect.
55
- args: (d) => ['--spec-dir', d],
56
- },
57
- {
58
- name: 'check-scenario-overlap',
59
- script: 'check-scenario-overlap/scripts/check-scenario-overlap.mts',
60
- args: (d) => ['--spec-dir', d, '--check'],
61
- },
62
- ]
63
-
64
- /**
65
- * Where an engine runs from. The .mts source runs its siblings in the skills tree; the
66
- * published bin runs from dist/, which mirrors that tree compiled to .js (tsdown.config.ts)
67
- * — Node will not strip types under node_modules, so the bin must never reach back into
68
- * the .mts tree.
69
- */
70
- export function enginePath(e: Engine, here: string = HERE): string {
71
- const skillsDir = resolve(dirname(here), '../..')
72
- if (here.endsWith('.bundle.mjs')) return join(skillsDir, e.script.replace(/\.mts$/, '.bundle.mjs'))
73
- return join(skillsDir, here.endsWith('.mts') ? e.script : e.script.replace(/\.mts$/, '.js'))
74
- }
75
-
76
- // ─── repo root ────────────────────────────────────────────────────────────────
77
-
78
- /** Walk up from `start` for the workspace marker. Returns '' when not found. */
79
- export function findRepoRoot(start: string): string {
80
- let dir = resolve(start)
81
- for (;;) {
82
- if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir
83
- const up = dirname(dir)
84
- if (up === dir) return ''
85
- dir = up
86
- }
87
- }
88
-
89
- // ─── resolution ───────────────────────────────────────────────────────────────
90
-
91
- export type Resolution =
92
- | { kind: 'resolved'; spec: SpecRecord }
93
- | { kind: 'none' }
94
- | { kind: 'ambiguous'; specs: SpecRecord[] }
95
-
96
- /**
97
- * Find the spec whose `project-path` names `projectRel` (a repo-relative dir).
98
- * A project with no spec is `none` — not an error: the script is uniform across
99
- * every workspace member, and some members are governed by no spec.
100
- */
101
- export function resolveSpecFor(specs: SpecRecord[], projectRel: string): Resolution {
102
- const hits = specs.filter((s) => s.projectPath !== '' && s.projectPath === projectRel)
103
- if (hits.length === 0) return { kind: 'none' }
104
- if (hits.length > 1) return { kind: 'ambiguous', specs: hits }
105
- return { kind: 'resolved', spec: hits[0] as SpecRecord }
106
- }
107
-
108
- // ─── coverage ─────────────────────────────────────────────────────────────────
109
-
110
- export interface CoverageGap {
111
- spec: string
112
- projectPath: string
113
- reason: 'unrecognized' | 'no-project-path' | 'no-manifest' | 'no-check-script'
114
- }
115
-
116
- /**
117
- * Every spec must be reachable from a project that actually checks it. Without
118
- * this, a spec silently goes unchecked the moment its project drops the script —
119
- * which is exactly how the corpus ended up with one audited project out of ten.
120
- *
121
- * `specFiles` are the spec.md paths found at the recognized locations *before*
122
- * the lifecycle-status filter; `specs` are the ones that survived it. A file in
123
- * the first set and not the second is a spec whose status is not in the enum:
124
- * discovery drops it, so every engine silently skips it and it is checked by
125
- * nothing. That is a gap to escalate, not to exempt.
126
- */
127
- export function findCoverageGaps(
128
- root: string,
129
- specFiles: string[],
130
- specs: SpecRecord[],
131
- readPkg: (p: string) => unknown,
132
- ): CoverageGap[] {
133
- const gaps: CoverageGap[] = []
134
- const recognized = new Set(specs.map((s) => (s.path === '' ? 'spec.md' : `${s.path}/spec.md`)))
135
- for (const f of specFiles) {
136
- if (!recognized.has(f)) gaps.push({ spec: f, projectPath: '', reason: 'unrecognized' })
137
- }
138
- for (const s of specs) {
139
- if (s.projectPath === '') {
140
- gaps.push({ spec: s.path, projectPath: '', reason: 'no-project-path' })
141
- continue
142
- }
143
- const pkg = readPkg(join(root, s.projectPath, 'package.json')) as { scripts?: Record<string, string> } | null
144
- if (!pkg) {
145
- gaps.push({ spec: s.path, projectPath: s.projectPath, reason: 'no-manifest' })
146
- continue
147
- }
148
- if (!pkg.scripts?.['check:spec']) {
149
- gaps.push({ spec: s.path, projectPath: s.projectPath, reason: 'no-check-script' })
150
- }
151
- }
152
- return gaps
153
- }
154
-
155
- /**
156
- * A spec.md sitting at a recognized location that discovery DROPPED (its status is not
157
- * in the lifecycle enum) and that belongs to `projectRel` — by the `project-path` it
158
- * declares, or by sitting inside the project (`<project>/.agents/spec/spec.md`, which
159
- * survives frontmatter corruption because it is location-derived).
160
- *
161
- * Without this, a per-project run resolves such a spec to `none` and prints "no spec
162
- * governs <project> — skipped" with exit 0: a status typo silently exempts the whole
163
- * project from every engine. A spec that exists but cannot be classified is escalated,
164
- * not exempted — the same call the corpus scope’s coverage guard makes.
165
- */
166
- export function findDroppedSpecFor(
167
- specFiles: string[],
168
- specs: SpecRecord[],
169
- projectRel: string,
170
- readText: (rel: string) => string | null,
171
- ): { file: string; status: string }[] {
172
- const recognized = new Set(specs.map((s) => (s.path === '' ? 'spec.md' : `${s.path}/spec.md`)))
173
- const out: { file: string; status: string }[] = []
174
- for (const f of specFiles) {
175
- if (recognized.has(f)) continue
176
- const text = readText(f)
177
- const fm = text === null ? null : parseFrontmatter(text)
178
- const dir = f.replace(/(^|\/)spec\.md$/, '')
179
- const nested = /^(.+)\/\.agents\/spec$/.exec(dir)?.[1] ?? ''
180
- if (fm?.projectPath === projectRel || nested === projectRel) out.push({ file: f, status: fm?.status ?? '' })
181
- }
182
- return out
183
- }
184
-
185
- const REASON_TEXT: Record<CoverageGap['reason'], string> = {
186
- unrecognized:
187
- 'sits at a spec location but its status is not in the lifecycle enum, so discovery drops it and nothing checks it',
188
- 'no-project-path': 'declares no project-path, so no project can be resolved to check it',
189
- 'no-manifest': 'names a project with no package.json, so it is not a workspace member',
190
- 'no-check-script': 'names a project that defines no `check:spec` script',
191
- }
192
-
193
- /** Read a file, or null when it cannot be read. */
194
- function readTextOrNull(path: string): string | null {
195
- try {
196
- return readFileSync(path, 'utf8')
197
- } catch {
198
- return null
199
- }
200
- }
201
-
202
- // ─── the engine run ───────────────────────────────────────────────────────────
203
-
204
- /**
205
- * Run the whole engine set against one project-spec dir. Every engine runs even
206
- * after one fails: a run that stops at the first failure reports one defect per
207
- * invocation, and the corpus scope exists to report the tree's defects in one go.
208
- */
209
- function runEngines(repoRoot: string, specDir: string): number {
210
- let failed = 0
211
- for (const e of ENGINES) {
212
- // cwd is the repo root, not the project dir: the engines resolve
213
- // repo-root-relative references against process.cwd().
214
- try {
215
- execFileSync('node', [enginePath(e), ...e.args(specDir)], {
216
- cwd: repoRoot,
217
- stdio: 'inherit',
218
- })
219
- process.stdout.write(` ok ${e.name}\n`)
220
- } catch {
221
- process.stderr.write(` FAIL ${e.name}\n`)
222
- failed++
223
- }
224
- }
225
- return failed
226
- }
227
-
228
- function checkCoverage(root: string): number {
229
- const gaps = findCoverageGaps(root, discoverSpecFiles(root), collectSpecs(root), (p) => {
230
- try {
231
- return JSON.parse(readFileSync(p, 'utf8'))
232
- } catch {
233
- return null
234
- }
235
- })
236
- if (gaps.length === 0) {
237
- process.stdout.write('check-project-specs: every spec is checked by its project\n')
238
- return 0
239
- }
240
- for (const g of gaps) process.stderr.write(` ${g.spec} — ${REASON_TEXT[g.reason]}\n`)
241
- process.stderr.write(`check-project-specs: ${gaps.length} spec(s) no project checks\n`)
242
- return 1
243
- }
244
-
245
- // ─── run ──────────────────────────────────────────────────────────────────────
246
-
247
- /** The flags the harness defines. Anything else is an error, never a default. */
248
- export const KNOWN_FLAGS = ['--corpus', '--project'] as const
249
-
250
- /**
251
- * The scope is chosen by a flag, so an unrecognized one must not fall through to a
252
- * default. It used to: project scope at a repo root resolves no governing spec and
253
- * exits 0, so a misspelled or retired flag reported success having checked nothing.
254
- * That is how `--check-coverage` guarded commits and CI while running no engine.
255
- */
256
- export function unknownFlags(argv: string[]): string[] {
257
- const out: string[] = []
258
- for (let i = 0; i < argv.length; i++) {
259
- const a = argv[i] as string
260
- if (!a.startsWith('-')) continue
261
- if (a === '--project') {
262
- i++ // its value is not a flag
263
- continue
264
- }
265
- if (!(KNOWN_FLAGS as readonly string[]).includes(a)) out.push(a)
266
- }
267
- return out
268
- }
269
-
270
- export function main(argv: string[]): number {
271
- const unknown = unknownFlags(argv)
272
- if (unknown.length) {
273
- process.stderr.write(
274
- `check-project-specs: unrecognized flag(s) ${unknown.join(', ')} — ` +
275
- `the scope is ${KNOWN_FLAGS.join(' | ')} (or none, meaning the cwd's project)\n`,
276
- )
277
- return 1
278
- }
279
- if (argv.includes('--corpus') && argv.includes('--project')) {
280
- process.stderr.write('check-project-specs: --corpus and --project name contradictory scopes — pass one\n')
281
- return 1
282
- }
283
- if (argv.includes('--corpus')) return checkCorpus()
284
- return checkProject(argv)
285
- }
286
-
287
- /**
288
- * Corpus scope — the commit floor. Total by definition: every project-spec the
289
- * corpus holds is checked, and a spec that cannot be classified fails rather than
290
- * being skipped.
291
- *
292
- * It runs BOTH sub-checks because neither subsumes the other. The sweep iterates
293
- * the specs discovery *recognizes*, so a spec whose lifecycle `status` is a typo is
294
- * invisible to it — and would report clean. The coverage guard sees that file on
295
- * disk and escalates it, but says nothing about whether the engines pass. Run either
296
- * alone and a whole project-spec leaves the floor silently.
297
- */
298
- function checkCorpus(): number {
299
- const root = findRepoRoot(process.cwd())
300
- if (!root) {
301
- process.stderr.write('check-project-specs: no pnpm-workspace.yaml found above the cwd\n')
302
- return 1
303
- }
304
-
305
- // Coverage first, but it never short-circuits the sweep: one run reports every
306
- // defect it can see, so an author fixes them in one pass rather than one per run.
307
- let failed = checkCoverage(root) === 0 ? 0 : 1
308
-
309
- const specs = collectSpecs(root)
310
- if (specs.length === 0) {
311
- process.stdout.write('check-project-specs: the corpus holds no project-spec\n')
312
- return failed
313
- }
314
- for (const s of specs) {
315
- process.stdout.write(`check-project-specs: ${s.path}\n`)
316
- if (runEngines(root, join(root, s.path)) > 0) failed = 1
317
- }
318
- return failed
319
- }
320
-
321
- function checkProject(argv: string[]): number {
322
- const projectArg = argv.includes('--project') ? argv[argv.indexOf('--project') + 1] : undefined
323
- const projectDir = resolve(projectArg ?? process.cwd())
324
-
325
- const repoRoot = findRepoRoot(projectDir)
326
- if (!repoRoot) {
327
- process.stderr.write(`check-project-specs: no pnpm-workspace.yaml found above ${projectDir}\n`)
328
- return 1
329
- }
330
-
331
- const projectRel = relative(repoRoot, projectDir) || '.'
332
- const specs = collectSpecs(repoRoot)
333
- const res = resolveSpecFor(specs, projectRel)
334
-
335
- if (res.kind === 'none') {
336
- // "No spec" is only legal when there is genuinely no spec file. A spec.md that
337
- // exists but was dropped by the status filter is unclassifiable, not absent.
338
- const dropped = findDroppedSpecFor(discoverSpecFiles(repoRoot), specs, projectRel, (rel) =>
339
- readTextOrNull(join(repoRoot, rel)),
340
- )
341
- for (const d of dropped) {
342
- process.stderr.write(
343
- `check-project-specs: \`${d.file}\` sits at a spec location and governs \`${projectRel}\` but ` +
344
- (d.status === ''
345
- ? 'declares no lifecycle status'
346
- : `its status \`${d.status}\` is not in the lifecycle enum`) +
347
- ' (draft | approved | implemented | deprecated) — discovery drops it, so it is checked by nothing\n',
348
- )
349
- }
350
- if (dropped.length) return 1
351
- process.stdout.write(`check-project-specs: no spec governs \`${projectRel}\` — skipped\n`)
352
- return 0
353
- }
354
- if (res.kind === 'ambiguous') {
355
- process.stderr.write(
356
- `check-project-specs: \`${projectRel}\` is claimed by ${res.specs.length} specs ` +
357
- `(${res.specs.map((s) => s.path).join(', ')}) — a project has exactly one spec\n`,
358
- )
359
- return 1
360
- }
361
-
362
- const specDir = join(repoRoot, res.spec.path)
363
- process.stdout.write(`check-project-specs: ${projectRel} -> ${res.spec.path}\n`)
364
-
365
- return runEngines(repoRoot, specDir) === 0 ? 0 : 1
366
- }
367
-
368
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
369
- process.exit(main(process.argv.slice(2)))
370
- }
@@ -1,184 +0,0 @@
1
- #!/usr/bin/env node
2
- import { execFileSync } from "node:child_process";
3
- import { existsSync, readFileSync, realpathSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- //#region skills/check-retired-terms/scripts/check-retired-terms.mts
7
- var RegistryParseError = class extends Error {};
8
- const REGISTRY_PATH = ".agents/sdd/retired-terms.toml";
9
- function isBlankOrComment(line) {
10
- const t = line.trim();
11
- return t === "" || t.startsWith("#");
12
- }
13
- function extractString(block, key) {
14
- const m = new RegExp(`(^|\\n)\\s*${key}\\s*=\\s*"([^"]*)"[^\\n]*`).exec(block);
15
- if (!m) throw new RegistryParseError(`[[retired]] entry is missing a string "${key}"`);
16
- return m[2];
17
- }
18
- function extractArray(block, key) {
19
- const openMatch = new RegExp(`(^|\\n)\\s*${key}\\s*=\\s*\\[`).exec(block);
20
- if (!openMatch) return [];
21
- const openIdx = openMatch.index + openMatch[0].length - 1;
22
- const closeIdx = block.indexOf("]", openIdx);
23
- if (closeIdx === -1) throw new RegistryParseError(`"${key}" array is never closed with "]"`);
24
- const arrText = block.slice(openIdx, closeIdx);
25
- const out = [];
26
- for (const q of arrText.matchAll(/"([^"]*)"/g)) out.push(q[1]);
27
- return out;
28
- }
29
- /** Parses the registry's minimal TOML subset: an array of `[[retired]]` tables carrying string
30
- * keys (term/since/replacement, all required) and optional string-array keys (scope/allow). Throws
31
- * `RegistryParseError` on anything that does not fit that shape — an absent registry is handled by
32
- * the caller (`readRegistry`), not here; an empty/comment-only file parses to `[]`. */
33
- function parseRegistryToml(text) {
34
- const marker = "[[retired]]";
35
- const firstIdx = text.indexOf(marker);
36
- if (firstIdx === -1) {
37
- if (text.split("\n").every(isBlankOrComment)) return [];
38
- throw new RegistryParseError("no [[retired]] table found and the file is not empty/comment-only");
39
- }
40
- if (!text.slice(0, firstIdx).split("\n").every(isBlankOrComment)) throw new RegistryParseError("content found before the first [[retired]] table header");
41
- return text.slice(firstIdx).split(marker).slice(1).map((block) => {
42
- const term = extractString(block, "term");
43
- const since = extractString(block, "since");
44
- const replacement = extractString(block, "replacement");
45
- const scope = extractArray(block, "scope");
46
- const allow = extractArray(block, "allow");
47
- const entry = {
48
- term,
49
- since,
50
- replacement
51
- };
52
- if (scope.length) entry.scope = scope;
53
- if (allow.length) entry.allow = allow;
54
- return entry;
55
- });
56
- }
57
- /** Reads and parses the registry at `<root>/.agents/sdd/retired-terms.toml`. An absent file yields
58
- * `[]` (an "an absent registry sweeps clean" registry, not an error). A present-but-malformed file
59
- * throws `RegistryParseError` — the caller must surface that loudly and exit non-zero. */
60
- function readRegistry(root) {
61
- const file = join(root, REGISTRY_PATH);
62
- if (!existsSync(file)) return [];
63
- return parseRegistryToml(readFileSync(file, "utf8"));
64
- }
65
- const DEFAULT_BUILTIN_EXCLUDED_FILES = [
66
- REGISTRY_PATH,
67
- "plugins/sdd/skills/check-retired-terms/scripts/check-retired-terms.mts",
68
- "plugins/sdd/skills/check-retired-terms/scripts/check-retired-terms.test.mts",
69
- ".agents/specs/sdd/corpus/retired-terms/README.md",
70
- ".agents/specs/sdd/corpus/retired-terms/retired-terms.feature"
71
- ];
72
- const DEFAULT_BUILTIN_EXCLUDED_PREFIXES = [".agents/plans/"];
73
- function isUnderLedgerDir(relPath) {
74
- return relPath.split("/").slice(0, -1).includes("ledger");
75
- }
76
- function isBuiltinExcluded(relPath, excludedFiles, excludedPrefixes) {
77
- if (excludedFiles.includes(relPath)) return true;
78
- if (excludedPrefixes.some((p) => relPath.startsWith(p))) return true;
79
- if (isUnderLedgerDir(relPath)) return true;
80
- return false;
81
- }
82
- function inScope(relPath, scope) {
83
- if (!scope || scope.length === 0) return true;
84
- return scope.some((prefix) => relPath.startsWith(prefix));
85
- }
86
- function parseAllowEntry(raw) {
87
- const idx = raw.indexOf("::");
88
- if (idx === -1) return { file: raw.trim() };
89
- return {
90
- file: raw.slice(0, idx).trim(),
91
- substring: raw.slice(idx + 2).trim()
92
- };
93
- }
94
- function isAllowed(relPath, lineText, allow) {
95
- if (!allow) return false;
96
- for (const raw of allow) {
97
- const parsed = parseAllowEntry(raw);
98
- if (parsed.file !== relPath) continue;
99
- if (parsed.substring === void 0) return true;
100
- if (lineText.includes(parsed.substring)) return true;
101
- }
102
- return false;
103
- }
104
- /** The git-tracked file set, repo-relative, one per line. An untracked file is outside the sweep
105
- * by construction — it is never in this list. */
106
- function listTrackedFiles(root) {
107
- return execFileSync("git", ["ls-files"], {
108
- cwd: root,
109
- encoding: "utf8"
110
- }).split("\n").filter((l) => l !== "");
111
- }
112
- /** Sweeps `root` for survivors of every entry in `entries`. `options` lets callers (tests)
113
- * substitute fixture-scoped config — the tracked-file source and the built-in exclusion lists —
114
- * in place of the real repo and the real defaults, mirroring `findMetaphorViolations`'s
115
- * `ScanOptions` (packages/cyberlegion/src/metaphor-free.ts). */
116
- function sweep(root, entries, options = {}) {
117
- const listFiles = options.listTrackedFiles ?? listTrackedFiles;
118
- const excludedFiles = options.builtinExcludedFiles ?? DEFAULT_BUILTIN_EXCLUDED_FILES;
119
- const excludedPrefixes = options.builtinExcludedPrefixes ?? DEFAULT_BUILTIN_EXCLUDED_PREFIXES;
120
- const violations = [];
121
- for (const relPath of listFiles(root)) {
122
- if (isBuiltinExcluded(relPath, excludedFiles, excludedPrefixes)) continue;
123
- let text;
124
- try {
125
- text = readFileSync(join(root, relPath), "utf8");
126
- } catch {
127
- continue;
128
- }
129
- const lines = text.split("\n");
130
- for (const entry of entries) {
131
- if (!inScope(relPath, entry.scope)) continue;
132
- lines.forEach((lineText, i) => {
133
- if (!lineText.includes(entry.term)) return;
134
- if (isAllowed(relPath, lineText, entry.allow)) return;
135
- violations.push({
136
- file: relPath,
137
- line: i + 1,
138
- term: entry.term,
139
- replacement: entry.replacement
140
- });
141
- });
142
- }
143
- }
144
- return violations.sort((a, b) => a.file !== b.file ? a.file < b.file ? -1 : 1 : a.line - b.line);
145
- }
146
- /** Renders `--list` output: one line per registered term with its `since` and `replacement`, or a
147
- * definitive empty-state line when nothing is registered — never silence. */
148
- function formatList(entries) {
149
- if (entries.length === 0) return "check-retired-terms: no term is registered\n";
150
- return entries.map((e) => `${e.term} (since ${e.since}) -> ${e.replacement}\n`).join("");
151
- }
152
- /** Renders the sweep report: `file:line:term` plus the declared replacement, one per survivor
153
- * (every survivor, not just the first), then a count summary. */
154
- function formatViolations(violations) {
155
- const lines = violations.map((v) => `${v.file}:${v.line}:${v.term} — replace with: ${v.replacement}\n`);
156
- lines.push(`check-retired-terms: ${violations.length} survivor(s) found\n`);
157
- return lines.join("");
158
- }
159
- function main(argv) {
160
- const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
161
- const list = argv.includes("--list");
162
- let entries;
163
- try {
164
- entries = readRegistry(root);
165
- } catch (err) {
166
- const message = err instanceof Error ? err.message : String(err);
167
- process.stderr.write(`check-retired-terms: malformed registry — ${message}\n`);
168
- return 1;
169
- }
170
- if (list) {
171
- process.stdout.write(formatList(entries));
172
- return 0;
173
- }
174
- const violations = sweep(root, entries);
175
- if (violations.length === 0) {
176
- process.stdout.write("check-retired-terms: clean — no survivors found\n");
177
- return 0;
178
- }
179
- process.stdout.write(formatViolations(violations));
180
- return 1;
181
- }
182
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
183
- //#endregion
184
- export { RegistryParseError, formatList, formatViolations, listTrackedFiles, main, parseRegistryToml, readRegistry, sweep };