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,429 +0,0 @@
1
- #!/usr/bin/env node
2
- // touch-set-correction — a post-hoc, read-only reconciliation of a Mission's DECLARED touch-set
3
- // (the pre-work guess used to keep the mission-graph's WAW frontier apart) against its ACTUAL
4
- // touch-set (recovered from `git diff base..head`). It composes three tools — `git diff` (changed
5
- // files), `resolve-governances` (each file's artifact-type, best-effort), and `gherkin-cli diff`
6
- // (a touched .feature's changed scenarios) — into one three-way split (confirmed / missed /
7
- // over-declared) plus the corrected touch-set (= the actual touched set). The touch-set-correction
8
- // node of the SDD project spec (repo-only) carries the full contract.
9
- //
10
- // Architecture — pure derivation kept apart from IO, on purpose (mission-graph.mts's convention):
11
- // - isFeature / fileToNode / reconcile / assembleCorrection are PURE: they take and return plain
12
- // data — no fs/network access. Tests exercise these directly over CONSTRUCTED fixtures — never
13
- // a live git diff or the live mission-graph store.
14
- // - readChangedFiles / resolveArtifactType / changedScenarios / collectChangedFiles /
15
- // discoverLayouts are the thin IO SEAM: they shell out to `git`, `resolve-governances.mts`, and
16
- // the pinned `gherkin-cli` `diff` (the same differ classify-edit-class.mts uses —
17
- // this tool never reimplements a differ). NOT unit-tested (binary/fs boundary) — the tested
18
- // logic is everything downstream of the file list.
19
- // - main() is a thin CLI: argv -> collectChangedFiles + assembleCorrection, rendering TOON by
20
- // default or `--format json`.
21
- //
22
- // This tool is READ-ONLY with respect to the mission graph: it never writes to it. The graph's
23
- // single writer appends the corrected touch-set at Mission retirement (deferred, F3). It also does
24
- // NOT classify a collision hard/soft, run the finer-than-node ladder, descend to region/hunk tier,
25
- // or do SSA lowering (all deferred, issue #189 remainder).
26
- //
27
- // Pure functions are exported for node:test; running the file directly drives the CLI.
28
-
29
- import { execFileSync } from 'node:child_process'
30
- import { readFileSync, realpathSync } from 'node:fs'
31
- import { dirname, join, relative, resolve, sep } from 'node:path'
32
- import { fileURLToPath, pathToFileURL } from 'node:url'
33
- import { diff as gherkinDiff, type ReadsGitDiff } from '../../spec-gate/vendor/gherkin-cli.mjs'
34
-
35
- // ── Types ──
36
-
37
- export interface ProjectLayout {
38
- project: string
39
- roots: string[]
40
- }
41
-
42
- /** A changed file with its resolved annotations — the IO seam fills these in; tests construct them. */
43
- export interface ChangedFile {
44
- path: string
45
- artifactType: string
46
- changedScenarios: string[]
47
- }
48
-
49
- export interface FileEntry {
50
- path: string
51
- node: string | null
52
- artifactType: string
53
- changedScenarios: string[]
54
- }
55
-
56
- /** One changed file under a node, with the artifact-type resolved for it (best-effort — `unknown`
57
- * when it does not resolve). The per-file annotation the frozen "carries the artifact-type"
58
- * contract requires observable in the output. */
59
- export interface NodeFile {
60
- path: string
61
- artifactType: string
62
- }
63
-
64
- export interface NodeDetail {
65
- node: string
66
- files: NodeFile[]
67
- changedScenarios: string[]
68
- }
69
-
70
- export interface Reconciliation {
71
- confirmed: string[]
72
- missed: string[]
73
- overDeclared: string[]
74
- corrected: string[]
75
- }
76
-
77
- export interface Correction extends Reconciliation {
78
- nodes: NodeDetail[]
79
- unmapped: string[]
80
- }
81
-
82
- // ── Pure derivations ──
83
-
84
- /** The scenario-rung gate is STRUCTURAL — the `.feature` extension — never the resolved
85
- * artifact-type (a `.feature` with an unresolved artifact-type still gets scenario detail; a
86
- * non-`.feature` with a resolved artifact-type never does). */
87
- export function isFeature(path: string): boolean {
88
- return path.endsWith('.feature')
89
- }
90
-
91
- function stripTrailingSlash(root: string): string {
92
- return root.endsWith('/') ? root.slice(0, -1) : root
93
- }
94
-
95
- /**
96
- * fileToNode — capability-first work-area recovery. For each project's root, if `path` sits under
97
- * `root/`, the CAPABILITY is the first path segment after the matched root; the node is
98
- * `project/capability`. Matches the LONGEST matching root prefix across every project + root (so a
99
- * deeper root like `plugins/sdd/skills` wins over a shallower `plugins/sdd`, when both are
100
- * declared). Returns null when no root matches — the file is unmapped.
101
- */
102
- export function fileToNode(path: string, projects: ProjectLayout[]): string | null {
103
- let best: { project: string; capability: string; rootLen: number } | null = null
104
- for (const p of projects) {
105
- for (const rawRoot of p.roots) {
106
- const root = stripTrailingSlash(rawRoot)
107
- const prefix = root === '' ? '' : `${root}/`
108
- if (prefix === '' || !path.startsWith(prefix)) continue
109
- const rest = path.slice(prefix.length)
110
- const capability = rest.split('/')[0]
111
- if (!capability) continue
112
- if (best === null || prefix.length > best.rootLen) {
113
- best = { project: p.project, capability, rootLen: prefix.length }
114
- }
115
- }
116
- }
117
- return best ? `${best.project}/${best.capability}` : null
118
- }
119
-
120
- function sortedUnique(values: readonly string[]): string[] {
121
- return [...new Set(values)].sort()
122
- }
123
-
124
- /**
125
- * reconcile — the declared-vs-actual three-way split: confirmed = declared ∩ actual, missed =
126
- * actual − declared, overDeclared = declared − actual. The corrected touch-set is simply the
127
- * actual set — the real change is the ground truth. Every list de-duplicated and sorted, so a
128
- * fixed input always reconciles to the same answer in the same order.
129
- */
130
- export function reconcile(declared: string[], actual: string[]): Reconciliation {
131
- const declaredSet = new Set(declared)
132
- const actualSet = new Set(actual)
133
- return {
134
- confirmed: sortedUnique(declared.filter((d) => actualSet.has(d))),
135
- missed: sortedUnique(actual.filter((a) => !declaredSet.has(a))),
136
- overDeclared: sortedUnique(declared.filter((d) => !actualSet.has(d))),
137
- corrected: sortedUnique(actual),
138
- }
139
- }
140
-
141
- /**
142
- * assembleCorrection — the whole pure assembly. Builds a FileEntry per changed file (node via
143
- * fileToNode; changedScenarios gated by isFeature — belt-and-suspenders: a non-.feature never
144
- * records scenario detail even if fed some), collects the unmapped paths and the actual (non-null,
145
- * de-duplicated) node set, groups per-node file + scenario detail, and reconciles declared against
146
- * actual. Deterministic + stably ordered for fixed inputs — no fs/network access of its own.
147
- */
148
- export function assembleCorrection(declared: string[], files: ChangedFile[], projects: ProjectLayout[]): Correction {
149
- const entries: FileEntry[] = files.map((f) => ({
150
- path: f.path,
151
- node: fileToNode(f.path, projects),
152
- artifactType: f.artifactType,
153
- changedScenarios: isFeature(f.path) ? f.changedScenarios : [],
154
- }))
155
-
156
- const unmapped = sortedUnique(entries.filter((e) => e.node === null).map((e) => e.path))
157
- const actual = sortedUnique(
158
- entries.filter((e): e is FileEntry & { node: string } => e.node !== null).map((e) => e.node),
159
- )
160
-
161
- const nodes: NodeDetail[] = actual.map((node) => {
162
- const nodeEntries = entries.filter((e) => e.node === node)
163
- // One NodeFile per distinct path (first artifact-type wins on the rare duplicate path), sorted
164
- // by path — so the per-file artifact-type rides through into the returned record, deterministically.
165
- const byPath = new Map<string, string>()
166
- for (const e of nodeEntries) if (!byPath.has(e.path)) byPath.set(e.path, e.artifactType)
167
- const files: NodeFile[] = [...byPath.entries()]
168
- .map(([path, artifactType]) => ({ path, artifactType }))
169
- .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
170
- return {
171
- node,
172
- files,
173
- changedScenarios: sortedUnique(nodeEntries.flatMap((e) => e.changedScenarios)),
174
- }
175
- })
176
-
177
- return { ...reconcile(declared, actual), nodes, unmapped }
178
- }
179
-
180
- // ── IO seam (thin — network/binary/fs boundary; not unit-tested, mirrors classify-edit-class.mts) ──
181
-
182
- /** Reads the changed-file list of `base..head` via `git diff --name-status`. A rename records its
183
- * NEW path only. On any git failure (absent binary, bad refs, not a repo) returns []. */
184
- export function readChangedFiles(base: string, head: string, cwd: string): { path: string; status: string }[] {
185
- let out: string
186
- try {
187
- out = execFileSync('git', ['diff', '--name-status', `${base}..${head}`], {
188
- encoding: 'utf8',
189
- cwd,
190
- stdio: ['ignore', 'pipe', 'ignore'],
191
- })
192
- } catch {
193
- return []
194
- }
195
- const results: { path: string; status: string }[] = []
196
- for (const line of out.split('\n')) {
197
- if (line.trim() === '') continue
198
- const rename = /^R\d+\t([^\t]+)\t([^\t]+)$/.exec(line)
199
- if (rename) {
200
- results.push({ path: rename[2], status: 'R' })
201
- continue
202
- }
203
- const m = /^(\S+)\t(.+)$/.exec(line)
204
- if (m) results.push({ path: m[2], status: m[1] })
205
- }
206
- return results
207
- }
208
-
209
- const HERE = fileURLToPath(import.meta.url)
210
- // A bundle (built for npm, where Node will not strip types) runs its sibling engines as bundles too.
211
- const ENGINE_EXT = HERE.endsWith('.mts') ? '.mts' : '.bundle.mjs'
212
- const RESOLVE_GOVERNANCES_PATH = join(
213
- dirname(HERE),
214
- '..',
215
- '..',
216
- 'resolve-governances',
217
- 'scripts',
218
- `resolve-governances${ENGINE_EXT}`,
219
- )
220
-
221
- /** Resolves one file's artifact-type via `resolve-governances --path` (best-effort — its own
222
- * no-match "classify by convention" case). On ANY failure (absent tool, bad JSON, non-zero exit)
223
- * returns 'unknown' — never throws. */
224
- export function resolveArtifactType(path: string, root: string, cwd: string): string {
225
- try {
226
- const out = execFileSync('node', [RESOLVE_GOVERNANCES_PATH, '--root', root, '--path', path, '--format', 'json'], {
227
- encoding: 'utf8',
228
- cwd,
229
- stdio: ['ignore', 'pipe', 'ignore'],
230
- })
231
- const parsed = JSON.parse(out) as { artifactType?: string | null }
232
- return parsed.artifactType ?? 'unknown'
233
- } catch {
234
- return 'unknown'
235
- }
236
- }
237
-
238
- // `diff`'s default reader resolves paths against `process.cwd()` and derives git's own cwd
239
- // from that resolved location (`git ls-files --full-name` to recover the repo-relative path) —
240
- // this reader is that same algorithm, re-pointed at `cwd` (same seam classify-edit-class.mts
241
- // uses), so it stays robust to a caller whose relative-path bookkeeping doesn't line up with its
242
- // own `cwd`. Any failure (bad ref, unreadable file) surfaces as `undefined` text, which the differ
243
- // reads as "absent" — the outer `changedScenarios` catch-all is this call site's real fail-open
244
- // boundary, so a thrown `GitError` here is caught there rather than escalated.
245
- const cwdReader =
246
- (cwd: string): ReadsGitDiff['readDiff'] =>
247
- (file, base) => {
248
- const abs = resolve(cwd, file)
249
- const dir = dirname(abs)
250
- let head: string | undefined
251
- try {
252
- head = readFileSync(abs, 'utf8')
253
- } catch {
254
- head = undefined
255
- }
256
- const gitIo = {
257
- cwd: dir,
258
- encoding: 'utf8' as const,
259
- stdio: ['ignore', 'pipe', 'ignore'] as ['ignore', 'pipe', 'ignore'],
260
- }
261
- let rel: string
262
- try {
263
- rel = execFileSync('git', ['ls-files', '--full-name', '--', abs], gitIo).trim()
264
- } catch {
265
- return { head, base: undefined }
266
- }
267
- if (rel === '') {
268
- try {
269
- const top = execFileSync('git', ['rev-parse', '--show-toplevel'], gitIo).trim()
270
- rel = relative(top, abs).split(sep).join('/')
271
- } catch {
272
- return { head, base: undefined }
273
- }
274
- }
275
- let baseText: string | undefined
276
- try {
277
- baseText = execFileSync('git', ['show', `${base}:${rel}`], gitIo)
278
- } catch {
279
- baseText = undefined
280
- }
281
- return { head, base: baseText }
282
- }
283
-
284
- /** The changed scenario names of a touched `.feature`, via the pinned `gherkin-cli`
285
- * `diff` (same tool classify-edit-class.mts uses — never a reimplemented differ). Gated
286
- * by isFeature — a non-.feature never calls out. On any failure returns []. Reads any `.feature`
287
- * regardless of freeze — the freeze gate is a separate concern (spec-gate), not this tool's
288
- * business. */
289
- export function changedScenarios(base: string, path: string, cwd: string): string[] {
290
- if (!isFeature(path)) return []
291
- try {
292
- const { files } = gherkinDiff([path], { base, full: true }, { readDiff: cwdReader(cwd) })
293
- const fileResult = files.find((f) => f.file === path) ?? files[0]
294
- return (fileResult?.scenarios ?? []).filter((s) => s.change !== 'unchanged').map((s) => s.name)
295
- } catch {
296
- return []
297
- }
298
- }
299
-
300
- /** Composes the three IO calls per changed file into the ChangedFile list `assembleCorrection`
301
- * consumes. */
302
- export function collectChangedFiles(base: string, head: string, root: string, cwd: string): ChangedFile[] {
303
- const changed = readChangedFiles(base, head, cwd)
304
- return changed.map(({ path }) => ({
305
- path,
306
- artifactType: resolveArtifactType(path, root, cwd),
307
- changedScenarios: changedScenarios(base, path, cwd),
308
- }))
309
- }
310
-
311
- const DISCOVER_SPECS_PATH = join(dirname(HERE), '..', '..', 'discover-specs', 'scripts', `discover-specs${ENGINE_EXT}`)
312
-
313
- interface DiscoveredSpec {
314
- path: string
315
- name: string
316
- projectPath: string
317
- }
318
-
319
- /** Best-effort project-layout discovery via `discover-specs`: each project's spec-path plus an
320
- * impl-root convention (`plugins/<p>` → also `plugins/<p>/skills`; `packages/<p>` → also
321
- * `packages/<p>/src`; else the project-path itself). `--layout` (CLI) overrides this entirely. On
322
- * any failure returns []. */
323
- export function discoverLayouts(root: string, cwd: string): ProjectLayout[] {
324
- try {
325
- const out = execFileSync('node', [DISCOVER_SPECS_PATH, '--root', root, '--format', 'json'], {
326
- encoding: 'utf8',
327
- cwd,
328
- stdio: ['ignore', 'pipe', 'ignore'],
329
- })
330
- const specs = JSON.parse(out) as DiscoveredSpec[]
331
- return specs.map((s) => {
332
- const roots = [s.path]
333
- const projectPath = s.projectPath ?? ''
334
- const pluginsMatch = /^plugins\/([^/]+)$/.exec(projectPath)
335
- const packagesMatch = /^packages\/([^/]+)$/.exec(projectPath)
336
- if (pluginsMatch) roots.push(`plugins/${pluginsMatch[1]}/skills`)
337
- else if (packagesMatch) roots.push(`packages/${packagesMatch[1]}/src`)
338
- else if (projectPath) roots.push(projectPath)
339
- return { project: s.name, roots }
340
- })
341
- } catch {
342
- return []
343
- }
344
- }
345
-
346
- // ── Render (TOON — the token-efficient tabular form the repo's other sdd engines emit) ──
347
-
348
- function toonQuote(v: string): string {
349
- if (v === '' || /[",;]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, '""')}"`
350
- return v
351
- }
352
-
353
- export function renderCorrectionToon(correction: Correction): string {
354
- const lines: string[] = []
355
- lines.push(`corrected[${correction.corrected.length}]: ${correction.corrected.map(toonQuote).join(';')}`)
356
- lines.push(`confirmed[${correction.confirmed.length}]: ${correction.confirmed.map(toonQuote).join(';')}`)
357
- lines.push(`missed[${correction.missed.length}]: ${correction.missed.map(toonQuote).join(';')}`)
358
- lines.push(`overDeclared[${correction.overDeclared.length}]: ${correction.overDeclared.map(toonQuote).join(';')}`)
359
- lines.push(`nodes[${correction.nodes.length}]{node,files,changedScenarios}:`)
360
- for (const n of correction.nodes) {
361
- const files = n.files.map((f) => `${f.path}(${f.artifactType})`).join(';')
362
- lines.push(` ${toonQuote(n.node)},"${files}","${n.changedScenarios.join(';')}"`)
363
- }
364
- lines.push(`unmapped[${correction.unmapped.length}]: ${correction.unmapped.map(toonQuote).join(';')}`)
365
- return lines.join('\n')
366
- }
367
-
368
- // ── CLI ──
369
-
370
- function flag(argv: string[], name: string): string | undefined {
371
- const i = argv.indexOf(name)
372
- return i === -1 ? undefined : argv[i + 1]
373
- }
374
-
375
- function allFlags(argv: string[], name: string): string[] {
376
- const out: string[] = []
377
- for (let i = 0; i < argv.length; i++) {
378
- if (argv[i] === name && argv[i + 1] !== undefined) out.push(argv[i + 1])
379
- }
380
- return out
381
- }
382
-
383
- function splitCsv(v: string | undefined): string[] {
384
- if (v === undefined || v === '') return []
385
- return v
386
- .split(',')
387
- .map((s) => s.trim())
388
- .filter((s) => s.length > 0)
389
- }
390
-
391
- /** Parses one `--layout '<project>:<root1>,<root2>'` flag value into a ProjectLayout. */
392
- export function parseLayoutFlag(value: string): ProjectLayout | null {
393
- const idx = value.indexOf(':')
394
- if (idx === -1) return null
395
- const project = value.slice(0, idx).trim()
396
- const roots = splitCsv(value.slice(idx + 1))
397
- if (project === '' || roots.length === 0) return null
398
- return { project, roots }
399
- }
400
-
401
- export function main(argv: string[]): number {
402
- const base = flag(argv, '--base')
403
- if (base === undefined) {
404
- process.stderr.write('touch-set-correction: --base <ref> is required\n')
405
- return 1
406
- }
407
- const head = flag(argv, '--head') ?? 'HEAD'
408
- const root = flag(argv, '--root') ?? '.'
409
- const declared = splitCsv(flag(argv, '--declared'))
410
- const format = flag(argv, '--format') === 'json' ? 'json' : 'toon'
411
-
412
- const layoutFlags = allFlags(argv, '--layout')
413
- const layouts =
414
- layoutFlags.length > 0
415
- ? layoutFlags.map(parseLayoutFlag).filter((l): l is ProjectLayout => l !== null)
416
- : discoverLayouts(root, root)
417
-
418
- const files = collectChangedFiles(base, head, root, root)
419
- const correction = assembleCorrection(declared, files, layouts)
420
-
421
- process.stdout.write(
422
- `${format === 'json' ? JSON.stringify(correction, null, 2) : renderCorrectionToon(correction)}\n`,
423
- )
424
- return 0
425
- }
426
-
427
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
428
- process.exit(main(process.argv.slice(2)))
429
- }