cyber-sdd 0.0.0

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 (133) hide show
  1. package/.claude-plugin/plugin.json +17 -0
  2. package/.codex-plugin/plugin.json +17 -0
  3. package/.plugin/plugin.json +17 -0
  4. package/README.md +159 -0
  5. package/agents/sdd-automaton.md +97 -0
  6. package/agents/sdd-impl-judge.md +214 -0
  7. package/agents/sdd-scanner.md +120 -0
  8. package/agents/sdd-spec-judge.md +224 -0
  9. package/agents/sdd-warden.md +101 -0
  10. package/package.json +24 -0
  11. package/skills/align-spec/README.md +20 -0
  12. package/skills/align-spec/SKILL.md +111 -0
  13. package/skills/align-spec/scripts/align-spec.mts +187 -0
  14. package/skills/architect-impl-governance/README.md +46 -0
  15. package/skills/architect-impl-governance/SKILL.md +45 -0
  16. package/skills/architect-spec-governance/README.md +48 -0
  17. package/skills/architect-spec-governance/SKILL.md +59 -0
  18. package/skills/blast-estimate/README.md +47 -0
  19. package/skills/blast-estimate/SKILL.md +133 -0
  20. package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
  21. package/skills/builder-impl-governance/README.md +47 -0
  22. package/skills/builder-impl-governance/SKILL.md +47 -0
  23. package/skills/builder-spec-governance/README.md +49 -0
  24. package/skills/builder-spec-governance/SKILL.md +36 -0
  25. package/skills/check-partition-quality/README.md +22 -0
  26. package/skills/check-partition-quality/SKILL.md +51 -0
  27. package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
  28. package/skills/check-plan-safety/README.md +17 -0
  29. package/skills/check-plan-safety/SKILL.md +60 -0
  30. package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
  31. package/skills/check-project-specs/README.md +19 -0
  32. package/skills/check-project-specs/SKILL.md +69 -0
  33. package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
  34. package/skills/check-scenario-overlap/README.md +19 -0
  35. package/skills/check-scenario-overlap/SKILL.md +74 -0
  36. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
  37. package/skills/check-spec-structure/README.md +17 -0
  38. package/skills/check-spec-structure/SKILL.md +66 -0
  39. package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
  40. package/skills/collision-ladder/README.md +18 -0
  41. package/skills/collision-ladder/SKILL.md +83 -0
  42. package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
  43. package/skills/combat-log-governance/README.md +13 -0
  44. package/skills/combat-log-governance/SKILL.md +257 -0
  45. package/skills/concept-index/README.md +13 -0
  46. package/skills/concept-index/SKILL.md +38 -0
  47. package/skills/concept-index/scripts/concept-index.mts +245 -0
  48. package/skills/discover-plans/README.md +16 -0
  49. package/skills/discover-plans/SKILL.md +74 -0
  50. package/skills/discover-plans/scripts/discover-plans.mts +212 -0
  51. package/skills/discover-specs/README.md +15 -0
  52. package/skills/discover-specs/SKILL.md +76 -0
  53. package/skills/discover-specs/scripts/discover-specs.mts +396 -0
  54. package/skills/doctrine-loop/README.md +15 -0
  55. package/skills/doctrine-loop/SKILL.md +97 -0
  56. package/skills/formation-loop/README.md +17 -0
  57. package/skills/formation-loop/SKILL.md +140 -0
  58. package/skills/gate-validation-governance/README.md +12 -0
  59. package/skills/gate-validation-governance/SKILL.md +87 -0
  60. package/skills/impl-producer-governance/README.md +48 -0
  61. package/skills/impl-producer-governance/SKILL.md +85 -0
  62. package/skills/init/README.md +27 -0
  63. package/skills/init/SKILL.md +68 -0
  64. package/skills/init/scripts/wire-statusline.mts +276 -0
  65. package/skills/lifecycle-governance/README.md +11 -0
  66. package/skills/lifecycle-governance/SKILL.md +168 -0
  67. package/skills/manage/README.md +9 -0
  68. package/skills/manage/SKILL.md +62 -0
  69. package/skills/manage-ignore/README.md +19 -0
  70. package/skills/manage-ignore/SKILL.md +52 -0
  71. package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
  72. package/skills/manage-scenario-bridge/README.md +20 -0
  73. package/skills/manage-scenario-bridge/SKILL.md +60 -0
  74. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
  75. package/skills/manage-spec-anchors/README.md +18 -0
  76. package/skills/manage-spec-anchors/SKILL.md +56 -0
  77. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
  78. package/skills/mission-graph/README.md +15 -0
  79. package/skills/mission-graph/SKILL.md +67 -0
  80. package/skills/mission-graph/scripts/mission-graph.mts +844 -0
  81. package/skills/oracle-spec-governance/README.md +45 -0
  82. package/skills/oracle-spec-governance/SKILL.md +45 -0
  83. package/skills/ownership-governance/README.md +65 -0
  84. package/skills/ownership-governance/SKILL.md +104 -0
  85. package/skills/pause-mission/README.md +18 -0
  86. package/skills/pause-mission/SKILL.md +112 -0
  87. package/skills/place-node/README.md +12 -0
  88. package/skills/place-node/SKILL.md +47 -0
  89. package/skills/place-node/scripts/place-node.mts +157 -0
  90. package/skills/plan-retirement/README.md +32 -0
  91. package/skills/plan-retirement/SKILL.md +90 -0
  92. package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
  93. package/skills/plugin-contract-governance/README.md +12 -0
  94. package/skills/plugin-contract-governance/SKILL.md +112 -0
  95. package/skills/remediation-governance/README.md +46 -0
  96. package/skills/remediation-governance/SKILL.md +78 -0
  97. package/skills/resolve-governances/README.md +18 -0
  98. package/skills/resolve-governances/SKILL.md +50 -0
  99. package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
  100. package/skills/resolve-tracking/SKILL.md +64 -0
  101. package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
  102. package/skills/resume-mission/README.md +12 -0
  103. package/skills/resume-mission/SKILL.md +53 -0
  104. package/skills/scaffold-project-spec/README.md +7 -0
  105. package/skills/scaffold-project-spec/SKILL.md +192 -0
  106. package/skills/sdd/README.md +7 -0
  107. package/skills/sdd/SKILL.md +92 -0
  108. package/skills/solution-producer-governance/README.md +9 -0
  109. package/skills/solution-producer-governance/SKILL.md +44 -0
  110. package/skills/spec-format-governance/README.md +73 -0
  111. package/skills/spec-format-governance/SKILL.md +114 -0
  112. package/skills/spec-gate/README.md +26 -0
  113. package/skills/spec-gate/SKILL.md +201 -0
  114. package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
  115. package/skills/spec-gate/scripts/check-suite.mts +501 -0
  116. package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
  117. package/skills/spec-producer-governance/README.md +7 -0
  118. package/skills/spec-producer-governance/SKILL.md +86 -0
  119. package/skills/spec-structure-governance/README.md +40 -0
  120. package/skills/spec-structure-governance/SKILL.md +169 -0
  121. package/skills/ssa-lowering/README.md +26 -0
  122. package/skills/ssa-lowering/SKILL.md +181 -0
  123. package/skills/start-mission/README.md +7 -0
  124. package/skills/start-mission/SKILL.md +115 -0
  125. package/skills/suite-format-governance/README.md +75 -0
  126. package/skills/suite-format-governance/SKILL.md +299 -0
  127. package/skills/suite-format-governance/references/rubric.md +313 -0
  128. package/skills/touch-set-correction/README.md +16 -0
  129. package/skills/touch-set-correction/SKILL.md +67 -0
  130. package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
  131. package/skills/verify-scenarios/README.md +17 -0
  132. package/skills/verify-scenarios/SKILL.md +109 -0
  133. package/skills/verify-scenarios/scripts/verify-scenarios.mts +386 -0
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+ // classify-edit-class — the structural edit-class classifier for a touched frozen `.feature`
3
+ // (`.agents/specs/sdd/authoring/spec-gate/README.md` — "Structural edit-class classification
4
+ // (freeze integrity)"). Classifies each touched file's change against its committed baseline so
5
+ // the gate can route: additive / no-content-change self-clear; narrowing / mixed take the
6
+ // existing narrowing -> Clearance path. This engine only CLASSIFIES — it fires no verdict and
7
+ // adds no new floor.
8
+ //
9
+ // The classification is STRUCTURAL, never a raw git line-diff. A raw line-diff is fooled by a
10
+ // trailing step orphaned off a frozen scenario onto a newly added adjacent scenario: the orphan
11
+ // shows no `-` line and reads as purely additive, so a narrowing self-clears silently and
12
+ // Clearance never fires. The pinned `gherkin-cli@0.0.2` `diffFeatures(paths, {base})` is AST-level
13
+ // and is not fooled — it reports the losing baseline scenario as `modified` (`addOnly: false`).
14
+ //
15
+ // The pin is load-bearing, not incidental. Through `0.0.1` the differ's scenario identity covered
16
+ // step keyword + text only, so a step's DocString / DataTable could be rewritten while the scenario
17
+ // still reported `unchanged` — and a `@rubric` lives wholly inside a DocString, which put every
18
+ // graded scenario outside the freeze. `0.0.2` hashes what an argument SAYS (content, media type) and
19
+ // still excludes how it is WRITTEN (delimiter, table padding, source locations), so a reformat does
20
+ // not fire Clearance. Both directions are bound by the frozen scenarios in this engine's suite;
21
+ // moving the pin backwards fails three of them.
22
+ //
23
+ // Classifications:
24
+ // unfrozen-skip — the file carries no feature-level @frozen tag in the baseline OR the
25
+ // working version; the edit-class routing does not apply to it.
26
+ // additive — addOnly true and at least one whole scenario was added; no baseline
27
+ // scenario was modified or removed.
28
+ // no-content-change — a pure rename (git rename detection, 100% content match) or a diff with
29
+ // zero added/modified/removed scenarios.
30
+ // narrowing — at least one baseline scenario is modified or removed (no scenario added).
31
+ // mixed — both a whole-scenario addition AND a modified/removed baseline scenario.
32
+ //
33
+ // Pure functions are exported for node:test; running the file directly drives the CLI.
34
+
35
+ import { execFileSync } from 'node:child_process'
36
+ import { readFileSync } from 'node:fs'
37
+ import { dirname, join, relative, resolve, sep } from 'node:path'
38
+ import { type DiffReader, diffFeatures, GitError } from 'gherkin-cli'
39
+
40
+ // ─── types ────────────────────────────────────────────────────────────────────
41
+
42
+ export type EditClassification =
43
+ | 'unfrozen-skip'
44
+ | 'additive'
45
+ | 'no-content-change'
46
+ | 'narrowing'
47
+ | 'mixed'
48
+ | 'unclassifiable'
49
+
50
+ export type ScenarioChangeKind = 'added' | 'modified' | 'removed' | 'unchanged'
51
+
52
+ export interface ScenarioChange {
53
+ name: string
54
+ change: ScenarioChangeKind
55
+ }
56
+
57
+ export interface GherkinDiffFileResult {
58
+ file: string
59
+ addOnly: boolean
60
+ scenarios: ScenarioChange[]
61
+ // Present when the differ could not parse one side of the comparison for this file. Its
62
+ // presence must be checked BEFORE reading `scenarios` — an unparseable file yields an EMPTY
63
+ // `scenarios` array, and empty reads as "nothing changed" (see classifyFromDiff) whether the
64
+ // file genuinely did not change or the differ simply could not see it. That emptiness is
65
+ // structurally guaranteed here, not measured, so it is not evidence of anything.
66
+ error?: { code: string; message: string }
67
+ }
68
+
69
+ export interface GherkinDiffOutput {
70
+ summary: { addOnly: boolean }
71
+ files: GherkinDiffFileResult[]
72
+ }
73
+
74
+ export interface ClassificationResult {
75
+ file: string
76
+ classification: EditClassification
77
+ scenarios: ScenarioChange[]
78
+ // Set only for `unclassifiable` — why the differ's result could not be trusted.
79
+ reason?: string
80
+ }
81
+
82
+ // ─── feature-level @frozen tag ─────────────────────────────────────────────────
83
+
84
+ // A feature-level tag sits on the contiguous block of `@tag` lines immediately above the
85
+ // `Feature:` line (blank lines between tags and Feature: are tolerated). Only that block is
86
+ // consulted — a scenario-level tag further down the file is not a feature-level freeze marker.
87
+ export function hasFeatureFrozenTag(text: string): boolean {
88
+ const lines = text.split('\n')
89
+ const featureIdx = lines.findIndex((l) => /^\s*Feature:/i.test(l))
90
+ if (featureIdx === -1) return false
91
+ const tags: string[] = []
92
+ for (let i = featureIdx - 1; i >= 0; i--) {
93
+ const line = lines[i].trim()
94
+ if (line === '') continue
95
+ if (line.startsWith('@')) {
96
+ tags.push(...line.split(/\s+/).filter(Boolean))
97
+ continue
98
+ }
99
+ break
100
+ }
101
+ return tags.includes('@frozen')
102
+ }
103
+
104
+ // ─── structural classification from a gherkin-cli diff result ─────────────────
105
+
106
+ // Classifies purely from the per-scenario `change` array a gherkin-cli diff reports — never from
107
+ // a raw line count. `additive` requires at least one whole-scenario addition and zero
108
+ // modified/removed baseline scenarios; `narrowing` is any modified/removed baseline scenario with
109
+ // no addition; both together is `mixed`; neither is `no-content-change` (a diff against the
110
+ // baseline with nothing structurally different).
111
+ export function classifyFromDiff(fileResult: { addOnly: boolean; scenarios: ScenarioChange[] }): EditClassification {
112
+ const added = fileResult.scenarios.some((s) => s.change === 'added')
113
+ const narrowed = fileResult.scenarios.some((s) => s.change === 'modified' || s.change === 'removed')
114
+ if (added && narrowed) return 'mixed'
115
+ if (narrowed) return 'narrowing'
116
+ if (added) return 'additive'
117
+ return 'no-content-change'
118
+ }
119
+
120
+ // ─── the escalation boundary — an input the classifier cannot classify ────────
121
+
122
+ // A file the differ reports a parse error for, or does not report at all, is not "no change" —
123
+ // it is an input the classifier has no evidence about, and absence of evidence never reads as
124
+ // evidence of no change. `error` is checked FIRST, before `scenarios` is read at all: an
125
+ // unparseable file gives the differ nothing to compare, so it reports an empty `scenarios` array
126
+ // (and a top-level `summary.addOnly: true`), which classifyFromDiff would otherwise read as
127
+ // `no-content-change`. That emptiness is structurally guaranteed by the parse failure rather than
128
+ // measured against the baseline — which is how a rewritten suite came to self-clear.
129
+ export function classifyFromFileResult(fileResult: GherkinDiffFileResult | undefined): {
130
+ classification: EditClassification
131
+ reason?: string
132
+ } {
133
+ if (fileResult === undefined) {
134
+ return { classification: 'unclassifiable', reason: 'the structural differ returned no result for this file' }
135
+ }
136
+ if (fileResult.error) {
137
+ return {
138
+ classification: 'unclassifiable',
139
+ reason: `cannot parse (${fileResult.error.code}): ${fileResult.error.message}`,
140
+ }
141
+ }
142
+ return { classification: classifyFromDiff(fileResult) }
143
+ }
144
+
145
+ // ─── pure rename detection (git, not gherkin-cli) ──────────────────────────────
146
+ // gherkin-cli diff reads the base ref's content AT THE SAME PATH — a renamed file has no content
147
+ // at its new path in the base ref, so gherkin-cli alone reports every scenario as freshly
148
+ // "added" (addOnly: true) for a pure rename, indistinguishable from a genuinely new file. Git's
149
+ // own rename detection (`-M`) pairs the old and new paths across the whole tree and scores their
150
+ // similarity; a 100% score is a zero-content-delta pure rename.
151
+
152
+ export interface RenameStatus {
153
+ score: number
154
+ oldPath: string
155
+ newPath: string
156
+ }
157
+
158
+ export function parseRenameStatus(nameStatusOutput: string): RenameStatus[] {
159
+ const out: RenameStatus[] = []
160
+ for (const line of nameStatusOutput.split('\n')) {
161
+ const m = /^R(\d+)\t([^\t]+)\t([^\t]+)$/.exec(line)
162
+ if (m) out.push({ score: Number(m[1]), oldPath: m[2], newPath: m[3] })
163
+ }
164
+ return out
165
+ }
166
+
167
+ function detectPureRename(base: string, path: string, cwd: string): boolean {
168
+ try {
169
+ const out = execFileSync('git', ['diff', '--name-status', '-M', base], {
170
+ encoding: 'utf8',
171
+ cwd,
172
+ stdio: ['ignore', 'pipe', 'ignore'],
173
+ })
174
+ return parseRenameStatus(out).some((r) => r.newPath === path && r.score === 100)
175
+ } catch {
176
+ return false
177
+ }
178
+ }
179
+
180
+ // Reads a path's content at a given git ref (the committed baseline). Any failure (new file at
181
+ // base, path not tracked at base, git absent) returns '' — the safe default that reads as
182
+ // "no baseline", matching check-spec-state.mts's readBaselineFromGit convention.
183
+ function readGitShow(base: string, path: string, cwd: string): string {
184
+ try {
185
+ return execFileSync('git', ['show', `${base}:${path}`], {
186
+ encoding: 'utf8',
187
+ cwd,
188
+ stdio: ['ignore', 'pipe', 'ignore'],
189
+ })
190
+ } catch {
191
+ return ''
192
+ }
193
+ }
194
+
195
+ // Thin boundary around the pinned `gherkin-cli` `diffFeatures` — never a re-implemented differ.
196
+ // classifyFromDiff / classifyFromFileResult carry the tested logic; this only wires the engine in.
197
+ // Takes a batch of paths (`diffFeatures` is already variadic over paths) so a multi-file caller
198
+ // pays for one parse pass, not one per file.
199
+ export type GherkinDiffRunner = (base: string, paths: string[], cwd: string) => GherkinDiffOutput
200
+
201
+ // `diffFeatures`'s default reader resolves each path via `path.resolve(file)` against
202
+ // `process.cwd()` and derives git's own cwd from THAT resolved location (`dirname` of the
203
+ // resolved path, then `git ls-files --full-name` to recover the repo-relative path) — it never
204
+ // trusts a caller-supplied cwd for the git commands at all. That self-derivation is why the
205
+ // original CLI boundary tolerated a caller's relative-path bookkeeping being off: whatever `file`
206
+ // resolves to, the reader walks UP from there to find the right repo and the right relative path.
207
+ // This reader is the library's own extension seam ("Injectable so tests can skip git"), replicated
208
+ // verbatim with the one substitution that matters here — `resolve(cwd, file)` instead of
209
+ // `resolve(file)` — so a caller's `cwd` participates without discarding that self-correction.
210
+ function makeCwdReader(cwd: string): DiffReader {
211
+ return (file, base) => {
212
+ const abs = resolve(cwd, file)
213
+ const dir = dirname(abs)
214
+ let head: string | undefined
215
+ try {
216
+ head = readFileSync(abs, 'utf8')
217
+ } catch {
218
+ head = undefined
219
+ }
220
+ const gitIo = {
221
+ cwd: dir,
222
+ encoding: 'utf8' as const,
223
+ stdio: ['ignore', 'pipe', 'ignore'] as ['ignore', 'pipe', 'ignore'],
224
+ }
225
+ let rel: string
226
+ try {
227
+ rel = execFileSync('git', ['ls-files', '--full-name', '--', abs], gitIo).trim()
228
+ } catch (err) {
229
+ throw new GitError(`git ls-files failed for ${file}: ${(err as Error).message}`)
230
+ }
231
+ if (rel === '') {
232
+ const top = execFileSync('git', ['rev-parse', '--show-toplevel'], gitIo).trim()
233
+ rel = relative(top, abs).split(sep).join('/')
234
+ }
235
+ let baseText: string | undefined
236
+ try {
237
+ baseText = execFileSync('git', ['show', `${base}:${rel}`], gitIo)
238
+ } catch {
239
+ try {
240
+ execFileSync('git', ['rev-parse', '--verify', '--quiet', `${base}^{commit}`], { cwd: dir, stdio: 'ignore' })
241
+ baseText = undefined
242
+ } catch {
243
+ throw new GitError(`git could not resolve base ref '${base}'`)
244
+ }
245
+ }
246
+ return { head, base: baseText }
247
+ }
248
+ }
249
+
250
+ export const runGherkinDiff: GherkinDiffRunner = (base, paths, cwd) =>
251
+ diffFeatures(paths, { base, reader: makeCwdReader(cwd) })
252
+
253
+ // ─── per-file classification ────────────────────────────────────────────────────
254
+
255
+ // The frozen-tag and rename checks a path resolves before ever needing a diff. `early` is set when
256
+ // those checks alone decide the classification (no gherkin-cli call needed); its absence means the
257
+ // caller still owes this path a diff lookup.
258
+ interface PreCheckedFile {
259
+ path: string
260
+ early?: ClassificationResult
261
+ }
262
+
263
+ function preCheckFile(path: string, base: string, cwd: string): PreCheckedFile {
264
+ let currentText: string
265
+ try {
266
+ currentText = readFileSync(join(cwd, path), 'utf8')
267
+ } catch {
268
+ currentText = ''
269
+ }
270
+ const baselineText = readGitShow(base, path, cwd)
271
+
272
+ // Skip files carrying no feature-level @frozen tag in EITHER the baseline or the working
273
+ // version — the edit-class routing this engine feeds only applies to frozen files.
274
+ if (!hasFeatureFrozenTag(currentText) && !hasFeatureFrozenTag(baselineText)) {
275
+ return { path, early: { file: path, classification: 'unfrozen-skip', scenarios: [] } }
276
+ }
277
+
278
+ // A pure rename is not a gate-able edit regardless of what gherkin-cli would (mis)report at
279
+ // the new path — check git's rename detection before ever calling gherkin-cli.
280
+ if (detectPureRename(base, path, cwd)) {
281
+ return { path, early: { file: path, classification: 'no-content-change', scenarios: [] } }
282
+ }
283
+
284
+ return { path }
285
+ }
286
+
287
+ // The CLI echoes each path as given, so an exact match is the expected case. A diff that reports
288
+ // under a different path (rather than omitting it) still resolves for a single-file batch — but
289
+ // with multiple files reported and no exact match, which one is "this file" is not knowable, so it
290
+ // falls through to `undefined` and classifyFromFileResult's no-result escalation.
291
+ function classifyFromDiffOutput(path: string, diff: GherkinDiffOutput): ClassificationResult {
292
+ const fileResult = diff.files.find((f) => f.file === path) ?? (diff.files.length === 1 ? diff.files[0] : undefined)
293
+ const { classification, reason } = classifyFromFileResult(fileResult)
294
+ return { file: path, classification, scenarios: fileResult?.scenarios ?? [], reason }
295
+ }
296
+
297
+ // `diff` is injected so the tests can drive the differ's failure branch — a binary that exits
298
+ // without a readable result is a real, reachable path (a bad base ref makes the CLI exit nonzero
299
+ // with no `files` array), and it must escalate rather than resolve to a reassuring class.
300
+ export function classifyFile(
301
+ path: string,
302
+ base: string,
303
+ cwd = '.',
304
+ diffWith: GherkinDiffRunner = runGherkinDiff,
305
+ ): ClassificationResult {
306
+ const pre = preCheckFile(path, base, cwd)
307
+ if (pre.early) return pre.early
308
+
309
+ let diff: GherkinDiffOutput
310
+ try {
311
+ diff = diffWith(base, [path], cwd)
312
+ } catch {
313
+ return {
314
+ file: path,
315
+ classification: 'unclassifiable',
316
+ scenarios: [],
317
+ reason: 'the structural differ produced no readable result',
318
+ }
319
+ }
320
+ return classifyFromDiffOutput(path, diff)
321
+ }
322
+
323
+ // The classification is scoped to the CR's touched .feature files only — a caller passes the
324
+ // explicit touched-path list, never a tree-wide sweep. Pre-checks (frozen tag, rename) run per file
325
+ // with no subprocess cost; only the paths still needing a structural diff after that are batched
326
+ // into a SINGLE `gherkin-cli diff` call, so an N-file touch set pays for one parse pass, not N.
327
+ export function classifyFiles(
328
+ paths: string[],
329
+ base: string,
330
+ cwd = '.',
331
+ diffWith: GherkinDiffRunner = runGherkinDiff,
332
+ ): ClassificationResult[] {
333
+ const preChecked = paths.filter((p) => p.endsWith('.feature')).map((path) => preCheckFile(path, base, cwd))
334
+ const needsDiff = preChecked.filter((r) => r.early === undefined).map((r) => r.path)
335
+
336
+ let diff: GherkinDiffOutput | undefined
337
+ if (needsDiff.length > 0) {
338
+ try {
339
+ diff = diffWith(base, needsDiff, cwd)
340
+ } catch {
341
+ diff = undefined
342
+ }
343
+ }
344
+
345
+ return preChecked.map(({ path, early }) => {
346
+ if (early) return early
347
+ if (diff === undefined) {
348
+ return {
349
+ file: path,
350
+ classification: 'unclassifiable',
351
+ scenarios: [],
352
+ reason: 'the structural differ produced no readable result',
353
+ }
354
+ }
355
+ return classifyFromDiffOutput(path, diff)
356
+ })
357
+ }
358
+
359
+ // ─── output ──────────────────────────────────────────────────────────────────
360
+
361
+ export function formatText(results: ClassificationResult[]): string {
362
+ const lines: string[] = []
363
+ for (const r of results) {
364
+ lines.push(`${r.classification.toUpperCase().padEnd(18)} ${r.file}`)
365
+ if (r.reason) lines.push(` ${r.reason}`)
366
+ for (const s of r.scenarios) {
367
+ if (s.change !== 'unchanged') lines.push(` ${s.change.padEnd(10)} ${s.name}`)
368
+ }
369
+ }
370
+ return lines.join('\n')
371
+ }
372
+
373
+ // ─── CLI entry ────────────────────────────────────────────────────────────────
374
+
375
+ function parseFilesArg(argv: string[]): string[] {
376
+ const idx = argv.indexOf('--files')
377
+ if (idx === -1) return []
378
+ const paths: string[] = []
379
+ for (let i = idx + 1; i < argv.length; i++) {
380
+ if (argv[i].startsWith('--')) break
381
+ paths.push(argv[i])
382
+ }
383
+ return paths
384
+ }
385
+
386
+ export function main(argv: string[]): number {
387
+ const paths = parseFilesArg(argv)
388
+ if (paths.length === 0) {
389
+ console.error('✗ --files requires at least one .feature path')
390
+ return 1
391
+ }
392
+ const base = argv.includes('--base') ? argv[argv.indexOf('--base') + 1] : 'HEAD'
393
+ const format = argv.includes('--format') ? argv[argv.indexOf('--format') + 1] : 'text'
394
+
395
+ const results = classifyFiles(paths, base)
396
+
397
+ if (format === 'json') {
398
+ process.stdout.write(`${JSON.stringify(results, null, 2)}\n`)
399
+ } else {
400
+ process.stdout.write(`${formatText(results)}\n`)
401
+ }
402
+
403
+ const unclassifiable = results.filter((r) => r.classification === 'unclassifiable')
404
+ if (unclassifiable.length) {
405
+ for (const r of unclassifiable) console.error(`✗ ${r.file}: unclassifiable — ${r.reason}`)
406
+ return 1
407
+ }
408
+ return 0
409
+ }
410
+
411
+ if (import.meta.main) process.exit(main(process.argv.slice(2)))
@@ -0,0 +1,7 @@
1
+ # spec-producer-governance
2
+
3
+ Non-user-invocable SDD skill holding the **default spec-producer procedure**: how to author the `spec.md` body and a boolean Gherkin `.feature` for a domain no plugin covers.
4
+
5
+ Loaded via the harness (`Skill`) by the **conductor** (the main session) when it runs the spec-producer role from the SDD default — the conductor authors **inline** in its own warm context (recorded `produced-by.spec-producer: sdd:automaton`) rather than spawning a producer agent. The grader stays separate: a cold `sdd-spec-judge` reviews the output.
6
+
7
+ References `sdd:spec-governance` (the universal format bar — including the required `## Use Cases` section and the use-case→scenario coverage rule) plus the resolved oracle + builder + architect actor bars (the spec-gate lens set, forward face) as its self-alignment criteria, and `sdd:ownership-governance` for the write-ownership matrix. Bakes in the grilling discipline (breadth-first, depth one-at-a-time, prose before suite) and the reconcile-toward-the-correct-answer rule for contradictions surfaced during grilling.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: spec-producer-governance
3
+ description: "Partial Skill: invoke by name only — the SDD default spec-producer procedure. Loaded in-session by the conductor when it runs the spec-producer role inline, not user-triggered."
4
+ user-invocable: false
5
+ ---
6
+
7
+ # Spec-Producer Governance — the default authoring procedure
8
+
9
+ The procedure the **conductor** follows when it runs the **spec-producer** role from the SDD default — i.e. no plugin covers the domain and no model-tuned producer agent is named for the slot, so the conductor **loads this governance and authors inline** in its own warm context (recorded `produced-by.spec-producer: sdd:automaton`). The grader is separate — a **cold spec-judge** (`sdd:sdd-spec-judge` or the plugin's judge) always reviews the output; this governance never judges its own work.
10
+
11
+ Load alongside this governance: `sdd:spec-format-governance` (the required `## Use Cases` section and the `spec.md` enrichment / human-readability rule), `sdd:suite-format-governance` (the `.feature` format bar and scenario-ordering convention), and the resolved **oracle**, **builder**, and **architect** actor bars — **forward** face — to self-align before writing (scope and kill-or-ship, testability/coverage, structural fit). These are exactly the bars the spec-judge grades **backward** at the spec gate, so the producer self-aligns to the same lens set it will be graded against. Load `sdd:ownership-governance` for the write-ownership matrix — which fields the spec-producer may write and which belong to the conductor or the gate skill.
12
+
13
+ **Track every governance you load.** The spec-judge cannot otherwise tell a skipped pre-flight from a correctly run one — both look like the same output gap. Keep a running list of each governance name as you load it (this governance plus every bar named above) and declare the full list as `governances_loaded` in your structured output — a **required** field, listed even when empty, and **never** written into `spec.md` or the `.feature`.
14
+
15
+ ## Inputs (folded in by the conductor)
16
+
17
+ ```
18
+ DOMAIN, DOMAIN_PATH, SPEC_PATH
19
+ COMMAND_SURFACE: <command syntax / signatures / events — or null>
20
+ DESIGN_DECISIONS: <known choices — or null>
21
+ USER_INPUT: <What / Why / command surface for a new feature — or null>
22
+ BACKFILL: <true if implementation already exists>
23
+ JUDGE_FEEDBACK: <spec-judge SCENARIOS_FAILING / BLOCKER from a prior pass — or null>
24
+ USER_ANSWERS: <answers to previously returned QUESTIONS — or null>
25
+ ```
26
+
27
+ ## Procedure
28
+
29
+ 1. **Gather intent, grilling breadth-first and depth one-at-a-time.** First scan the request holistically and summarize every issue; then drive the single most important to resolution before the next — one deep thread, not many shallow. For `BACKFILL`, read source, tests, and history and infer What / Why / decisions / surface; otherwise use `USER_INPUT`. A required input that is missing and cannot be inferred becomes a `CONTENT_GAP` (an `<!-- open: -->` marker), never an invention. Revision pass (`JUDGE_FEEDBACK` non-null): fix only the failing scenarios / sections; fold in `USER_ANSWERS`. **Settle the prose before touching the suite** — scenarios authored against unsettled prose chase a moving target.
30
+
31
+ 2. **Reconcile contradictions toward the correct answer, not the popular one.** When grilling surfaces a conflict — between the `spec.md` body and the `.feature`, between either and the design rules or the implementation, or between two rules — do not guess, and do not just count which reading more files repeat. Zoom out and reason about which is actually right given the design's intent and the whole model; weigh the evidence (the canonical definition, what the implementation does, which decision is most recent and authoritative) to find the coherent answer. Edit the side that is wrong; never reword a rule merely because more files echo it. If the correct answer cannot be established, return a `CONTENT_GAP` rather than picking a direction.
32
+
33
+ 3. **Write the `spec.md` body per `sdd:spec-format-governance`.** That bar owns the required structure — the `## Use Cases` section (subject, non-goals, and the entry-point table of trigger / inputs / outcome) and the enrichment rules; follow it rather than re-listing sections here (a hardcoded list drifts from the bar). Author the body content — What, Why, design decisions, and the command / API surface where one exists — and enrich for human review (headings, tables, short paragraphs, a diagram where it carries the idea). Never leave placeholders (`TBD`, `TODO`, empty sections). **Do not** write the control frontmatter (`status`, `project-path`, `approval`, `produced-by`) — those belong to the conductor and the gate skill. Every referenced engine, skill, or artifact path you name must be real — a reference that resolves to nothing is caught mechanically at step 5 below, but naming a real path the first time spends no round on it. **On `BACKFILL` the four sections are still mandatory** — draw the `## Control Flow` CFG and its `## Scenario map` from the code, never stop at `## Use Cases` (`sdd:spec-format-governance`; `check-spec-structure`'s `incomplete-node` flags a leaf that skips them).
34
+
35
+ 4. **Write `<DOMAIN_PATH>/<DOMAIN>.feature`** — pure boolean Gherkin per `sdd:suite-format-governance`. **Cover every use case from the `## Use Cases` section with one-or-more scenarios** (happy path, negative mirror, boundary) — a use case with no scenario is unverified intent; a scenario with no use case is an orphan. **On `BACKFILL`, re-derive the scenario set from the CFG's edges** rather than patching the standing suite; the retired corpus is **reference only**, a claim to verify against the current code (`sdd:suite-format-governance`). Each `Then` is an observable boolean — name the artifact a verifier reads to settle it; an act is assertable only when it leaves a trace, and where it records nothing, add the record rather than dropping the act. Never internal state, function names, "sometimes", or how the artifact was authored. Order scenarios by lifecycle stage (the step-down convention). Keep the `.feature` plain; rubric form is legal only inside an `@rubric`-tagged scenario.
36
+
37
+ **A `Given` is a test vector, not specification** (`sdd:suite-format-governance` carries the canonical bar and the swap test). Author each `Given`'s apparatus — its domain, entities, names, framing — from a domain **the artifact does not illustrate**. On a revise CR the apparatus never reuses the artifact's existing worked examples; on `BACKFILL` it never reuses the illustrations you read out of source. Read those examples in full at step 1 — they are evidence of the behavior you are specifying; exclude them only from the apparatus you author into a `Given`.
38
+
39
+ 5. **Self-check the `.feature` form before returning.** Run the deterministic form check — the `check-suite` engine (`scripts/check-suite.mts` in the `spec-gate` skill), the executable form of `sdd:suite-format-governance` — scoped to what you just authored:
40
+
41
+ ```bash
42
+ node "<spec-gate skill>/scripts/check-suite.mts" --files <the authored .feature path(s)>
43
+ ```
44
+
45
+ Exit `0` = form clean; exit `1` prints each `✗ <file>: <reason>`. **Fix every violation** (a non-boolean/hedged `Then`, leaked rubric lingo in an untagged scenario, a missing `Feature`/`Then`, or missing section comments over the sectioning threshold) and re-run until clean **before reporting `STATUS: complete`**. Settling this mechanical bar here spends no cold-judge round on a defect a linter catches every time; the same engine runs fail-closed at the gate (`../spec-gate/`), so an unfixed violation would block there anyway. If `node` is unavailable, self-review against the suite-format bar by hand.
46
+
47
+ **A clean form check does not clear an entangled `Given`.** The engine reads form, not apparatus — it reports no violation on a `Given` whose apparatus reuses the artifact's worked examples. Re-read each authored `Given` against the test-vector bar by hand and rewrite the apparatus before returning `STATUS: complete`.
48
+
49
+ **A clean form check does not clear a scenario that cannot fail.** The engine reads form, not discrimination — a well-formed `Then`, and a well-formed `@rubric` block, are both reported clean when no subject can ever fail them. Apply the **miss test** (`sdd:suite-format-governance`) to every scenario and every `@rubric` dimension you authored: name a **plausible wrong subject** — a memorizer, a copier, a procedure-follower, a single-brancher — and check that it *loses*. Name none and the scenario is inert; rewrite it. The wrong subject must be plausible — an empty artifact fails everything and clears nothing. Rewrite any dimension grading **presence** (a line is emitted), **restatement** (the doctrine's own words), or **procedure** (the steps, not the judgment). Rewrite the **toothless finding** — a `Then` asserting a signal or finding is *raised* but not its **binding consequence** (it withholds the pass, blocks the gate, changes the outcome): name the wrong subject that raises the finding and acts on nothing, then rewrite the `Then` to assert the consequence. Rewrite the **process-`Then`** — a `Then` asserting **how** the artifact was produced ("co-developed with the code", "written test-first", "refactored before completing", authoring order) rather than its **observable behavior or end-state**; nothing in the artifact or a run reveals the authoring sequence, so it can never be a `Then` — rewrite it to assert the observable behavior instead (`sdd:suite-format-governance`). Before writing any criterion as a dimension, run the **substitutability test** (`sdd:suite-format-governance`): a criterion belongs in a `@rubric` **only if** you accept that strength elsewhere may pay for weakness here — otherwise it is not in the sum at all, it is a boolean `Then`. **Write the trade down** for each dimension you author or revise, in the same record that carries the cut's reason, naming it and **what pays for it** — an unrecorded trade is an unowned selection nobody can disagree with. The duty is **yours alone**: no judge reports a missing record, so nothing catches you skipping it. For a `@rubric`, sum what each named wrong subject **banks** (never zero a dimension to make a point): that sum sits **strictly under** the threshold — a tie passes, since the collapsing `Then` passes a score *at least* the threshold. How far under is **not** a constant: it is your judge's noise at the cut (**cSEM**), measured by scoring the subject more than once, never decreed here.
50
+
51
+ **In `revise` mode, run the substitutability test over the standing `@rubric` dimensions your CR touches, not only the ones you author.** A dimension already in the sum that fails the test is a **correction**, and a correction is not a deletion: removing it changes the attainable maximum, so the cut it leaves behind is **un-re-derived whether or not its number still needs to change**. Re-derive that cut as a fresh policy call **in the same edit** and record its reason against the new attainable maximum; raise **one clearance per corrected scenario** and never let one blanket approval stand in for each scenario's own cut decision. Route it on the **removal** — never on whether the diff calls the edit `mixed`, which the in-scenario shape is not. The full procedure is *Correcting a standing rubric* (`sdd:suite-format-governance`). A green form check does **not** clear this: `check-suite.mts` reports only the vacuous `sum(max) < threshold` rubric, and a cut nobody re-derived clears that check every time.
52
+
53
+ **Read your authored scenarios against each other.** No two scenarios sharing a `When` may demand opposite verdicts on one constructible snapshot (`sdd:suite-format-governance`); narrow one `Given` to exclude the overlap before returning. Overlapping `Given`s whose `Then`s agree, and scenarios whose `When`s name different operations, are not contradictions — the bar is the contradiction, never the overlap. **Specialization is not contradiction:** a general scenario and a specific sibling whose narrower `Given` carves out an exception do not contradict, even when the general `Given` does not literally exclude it — a contradiction is a pair with **no intended winner**. When *you* are authoring the general `Given` fresh, state the exclusion anyway; it is clearer. Never retrofit it into a frozen scenario — that is a narrowing that fires **Clearance**.
54
+
55
+ **Check coverage and mirroring before returning.** Every outcome and carve-out the node's `## Use Cases` / README states — including an exception named only in prose — must have at least one scenario; do not report `STATUS: complete` while a stated outcome or carve-out has none — add the missing scenario. A **duty specified on one node of a mirrored pair** (producer/judge, sender/receiver) must be **mirrored on the counterpart node**; do not report complete while only one side carries the duty — specify it on the counterpart too (`sdd:suite-format-governance`).
56
+
57
+ Also self-run **referenced-artifact-exists** — `check-spec-state.mts` in `scripts/`, scoped to
58
+ the `spec.md`/`README.md` you just authored or touched:
59
+
60
+ ```bash
61
+ node "<spec-gate skill>/scripts/check-spec-state.mts" --files <the authored spec.md/README.md path(s)>
62
+ ```
63
+
64
+ Exit `0` = every referenced path resolves; exit `1` prints each `✗ <file>: references nonexistent
65
+ artifact ...`. Fix every violation the same way — a broken reference to a skill/engine/artifact
66
+ that never existed is a content gap, not a typo to shrug at.
67
+
68
+ ## Responding to a `change` verdict
69
+
70
+ Load `sdd:remediation-governance` — the findings are **evidence, not a work order**. It carries the
71
+ four rules (substantiate before acting · state the rule and sweep, scope-aware · re-derive against the
72
+ rule governing the artifact · account for provenance, where a regression stops the loop) and the
73
+ `REMEDIATION` trace this role returns in its `Output` below.
74
+
75
+ ## Output (the conductor collects)
76
+
77
+ ```
78
+ REMEDIATION: <per finding answered: verdict, rule, swept, ruled-out, provenance — `sdd:remediation-governance`; omit when no verdict was answered>
79
+ STATUS: complete | needs-input | blocked
80
+ SCENARIOS_WRITTEN: <count>
81
+ NOTES: <what was written / revised>
82
+ GOVERNANCES_LOADED: [ every governance name loaded before writing — required, [] when none, never written into spec.md or the .feature ]
83
+ QUESTIONS: [ batched, when needs-input ]
84
+ CONTENT_GAPS: [ { artifact, location, gap } ] # become <!-- open: --> markers
85
+ OBSERVATIONS: [ { owner: architect | strategist, note, evidence } ]
86
+ ```
@@ -0,0 +1,40 @@
1
+ # spec-structure-governance
2
+
3
+ This is an internal SDD governance about how a project specification is organized.
4
+
5
+ It answers two questions that are really one: **what kind of node is this spec**, and **where does
6
+ that kind live in the tree**. They cannot be separated — a rules document in `design/` is correctly
7
+ placed where a behavioral node in the same folder is a defect, and the folder alone does not tell you
8
+ which you are looking at.
9
+
10
+ ## Usage
11
+
12
+ - **scaffold-project-spec:** lay out a new tree to it
13
+ - **place-node:** decide where a node belongs
14
+ - **formation Warden:** audit whether the corpus still matches it
15
+ - **architect bars:** judge placement at the spec and impl gates
16
+
17
+ Read `SKILL.md` beside this file for the law itself — that is what those four load.
18
+
19
+ ## What it covers
20
+
21
+ - **The node taxonomy** — descriptive, reference artifact, behavioral artifact; declared in
22
+ frontmatter, never guessed from which files happen to exist.
23
+ - **Placement** — screaming architecture (folders named for capabilities), the three deliberate
24
+ non-capability folders, rules-in-design vs behavior-in-capability, the two-level depth cap, and
25
+ the concept axis for anything that cuts across.
26
+ - **One spec per project** — and when a spec is hoisted out of its project directory rather than
27
+ colocated in it.
28
+ - **Strategy is policy, homes are data** — the layout strategy is a choice, so it is declared once
29
+ and read; which folder a concept lives in is a fact, so it is derived and never stored.
30
+
31
+ ## What this bar does not own
32
+
33
+ | Concern | Owner |
34
+ | --- | --- |
35
+ | The sections inside one node's `spec.md` | `spec-format-governance` |
36
+ | How the `.feature` suite is written and judged | `suite-format-governance` |
37
+ | The states a spec moves through, and freezing | `lifecycle-governance` |
38
+ | Who may write which field | `ownership-governance` |
39
+
40
+ Internal SDD governance (`user-invocable: false`). Not triggered by users directly.