cyber-sdd 0.0.0 → 0.1.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 (63) hide show
  1. package/.plugin/pins.json +3 -0
  2. package/LICENSE +21 -0
  3. package/agents/sdd-automaton.md +13 -2
  4. package/agents/sdd-scanner.md +85 -0
  5. package/agents/sdd-spec-judge.md +32 -2
  6. package/agents/sdd-warden.md +9 -0
  7. package/package.json +30 -23
  8. package/skills/align-spec/scripts/align-spec.mts +3 -2
  9. package/skills/blast-estimate/README.md +3 -5
  10. package/skills/blast-estimate/SKILL.md +2 -2
  11. package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
  12. package/skills/builder-impl-governance/SKILL.md +9 -1
  13. package/skills/builder-spec-governance/SKILL.md +13 -1
  14. package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
  15. package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
  16. package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
  17. package/skills/check-retired-terms/README.md +18 -0
  18. package/skills/check-retired-terms/SKILL.md +81 -0
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
  20. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
  21. package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
  22. package/skills/collision-ladder/README.md +3 -5
  23. package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
  24. package/skills/combat-log-governance/SKILL.md +43 -4
  25. package/skills/concept-index/scripts/concept-index.mts +3 -2
  26. package/skills/discover-plans/scripts/discover-plans.mts +5 -2
  27. package/skills/discover-specs/scripts/discover-specs.mts +5 -2
  28. package/skills/doctrine-loop/README.md +6 -0
  29. package/skills/doctrine-loop/SKILL.md +136 -2
  30. package/skills/formation-loop/SKILL.md +21 -1
  31. package/skills/gate-validation-governance/SKILL.md +2 -2
  32. package/skills/impl-producer-governance/SKILL.md +10 -1
  33. package/skills/init/scripts/wire-statusline.mts +5 -2
  34. package/skills/lifecycle-governance/SKILL.md +1 -1
  35. package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
  36. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
  37. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
  38. package/skills/mission-graph/README.md +3 -5
  39. package/skills/mission-graph/SKILL.md +72 -5
  40. package/skills/mission-graph/scripts/mission-graph.mts +505 -16
  41. package/skills/place-node/scripts/place-node.mts +3 -2
  42. package/skills/plan-retirement/README.md +5 -2
  43. package/skills/plan-retirement/SKILL.md +5 -1
  44. package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
  45. package/skills/plugin-contract-governance/SKILL.md +7 -1
  46. package/skills/remediation-governance/SKILL.md +36 -1
  47. package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
  48. package/skills/resolve-tracking/SKILL.md +2 -2
  49. package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
  50. package/skills/sdd/SKILL.md +1 -1
  51. package/skills/spec-format-governance/SKILL.md +5 -0
  52. package/skills/spec-gate/SKILL.md +18 -2
  53. package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
  54. package/skills/spec-gate/scripts/check-suite.mts +53 -17
  55. package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
  56. package/skills/spec-producer-governance/SKILL.md +2 -2
  57. package/skills/ssa-lowering/README.md +3 -5
  58. package/skills/start-mission/SKILL.md +8 -4
  59. package/skills/suite-format-governance/SKILL.md +43 -4
  60. package/skills/touch-set-correction/README.md +3 -5
  61. package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
  62. package/skills/verify-scenarios/SKILL.md +10 -3
  63. package/skills/verify-scenarios/scripts/verify-scenarios.mts +92 -8
@@ -70,6 +70,45 @@ the current code**, never the baseline to patch. Reading the standing suite and
70
70
  a diff notices is not this procedure — it leaves stale scenarios in place and misses edges the CFG
71
71
  mandates (ADR-0029).
72
72
 
73
+ ## A fold node states its rule in closed form before its scenarios
74
+
75
+ A **fold** (aggregation) node folds several sub-conditions into one verdict — a ready-frontier folding
76
+ reachability against a mutex, a gate-legality aggregate, a per-cell matrix claim. **When the fold
77
+ combines two or more *interacting* sub-conditions, state its rule in closed form — and re-derive that
78
+ rule's soundness against the real data model — before you draw the CFG.** A **single-condition** fold
79
+ may be specified by example; demanding a closed form of it is the failure mode of this rule, not its
80
+ point. Scenarios drawn off a rule never written down are drawn off a rule never *agreed*, and each
81
+ producer-judge round then rewrites a different corner of it: the corpus ran the A/B — the `github-192`
82
+ fence, specified by example, **diverged** (`1 → 1 → 3` contradictions, each manufactured by the prior
83
+ fix, reverted at the cap); `github-224` stated the rule first and **converged** (zero). Closed form
84
+ buys **iteration convergence** — insurance a single-condition fold does not need and a
85
+ multi-condition one rarely survives without. Three qualifications, each a way it is misapplied:
86
+
87
+ - **Fire only on ≥2 *interacting* sub-conditions — over-firing is the failure mode.** A
88
+ single-condition fold by example converges fine (the WAW-mutex touch-set-intersection scenarios
89
+ settled in a handful, no stated rule). Mere aggregation is not the trigger; genuine interaction is.
90
+ - **Closed form is not soundness — re-derive against the real data model.** A rule in closed form,
91
+ even one carrying a proof, can be unsound against the data it folds: `R''` shipped a termination
92
+ proof and still deadlocked, its project-scoped exemption violated by graph-global RAW closure, fixed
93
+ to `R'''` only once the assumption was re-derived against the real graph. A proof over an assumed
94
+ model proves nothing about the real one.
95
+ - **Convergence is not coverage — pair the rule with a mutation sweep and a safety dual.** Stating
96
+ the rule kills the divergence, not the gaps. A **mutation sweep** mutates each interacting condition
97
+ and confirms each break lands on a *distinct* scenario — two mutations breaking one scenario mean
98
+ the conditions collapsed into one and the CFG has fewer real branches than it claims. A **safety
99
+ dual** guards the blind spot a convergence check cannot see: a liveness rule (*some grant path
100
+ exists*, *the frontier advances*) passes an **over-permissive** fold green, because over-permission
101
+ adds paths rather than removing them — pair every liveness scenario with the safety scenario
102
+ asserting the case it structurally cannot observe.
103
+
104
+ **The matrix corollary — a per-cell claim is this rule applied.** An outcome stated per cell of a grid
105
+ of interacting conditions is this rule with the closed form written as the **cell function**. Draw
106
+ **every independent cell as its own CFG branch** and exclude the **degenerate** ones (a cell whose
107
+ outcome reconverges with a sibling collapses under the reconvergence rule above, exactly as a
108
+ **universal** "every cell behaves the same" claim is one convergence scenario, not a grid — the
109
+ `github-278` round-4 draft asserted such a universal one row too wide and dropped it). Confirm the
110
+ cells genuinely independent by the mutation sweep. This is not a separate bar; it is the fold rule.
111
+
73
112
  ## Sections mirror the spec's use-case groups; every scenario binds to a map edge
74
113
 
75
114
  `spec.md` sections the node by **use-case group**, each carrying a drawn **CFG** and an
@@ -119,10 +158,10 @@ only *where the node genuinely owns the routing decision*, and two different dec
119
158
  node's own content, so **the node owns it outright**.
120
159
 
121
160
  The two look alike in shape and differ only in who decides, so **step form does not classify them**
122
- and no mechanical check should try (see `.agents/specs/sdd/ssa-lowering/ssa-lowering.feature`, where
123
- a deletion that read the second case as the first was blocked at the gate and reverted). A
124
- deterministic, fully-owned decision table that selects *what an already-invoked subject does* is
125
- conduct, not engagement — it wants `@behavior`.
161
+ and no mechanical check should try (see the frozen suite of the SDD project spec's `ssa-lowering`
162
+ node, repo-only, where a deletion that read the second case as the first was blocked at the gate and
163
+ reverted). A deterministic, fully-owned decision table that selects *what an already-invoked subject
164
+ does* is conduct, not engagement — it wants `@behavior`.
126
165
 
127
166
  **`@frozen` is the only file-level tag** — it sits on the `Feature`, not a scenario.
128
167
 
@@ -4,11 +4,9 @@ The concrete engine for **touch-set-correction** — a read-only, post-hoc recon
4
4
  Mission's declared touch-set against what its `git diff` actually changed, composing `git diff`,
5
5
  [`resolve-governances`](../resolve-governances/SKILL.md), and `gherkin-cli diff` into the corrected
6
6
  touch-set the mission-graph's single writer records at retirement. Built for the Op2 deferral of the
7
- cyberfleet-batch change request; see
8
- [`.agents/specs/sdd/touch-set-correction/README.md`](../../../../.agents/specs/sdd/touch-set-correction/README.md)
9
- for the authoritative behavior description and
10
- [`touch-set-correction.feature`](../../../../.agents/specs/sdd/touch-set-correction/touch-set-correction.feature)
11
- for the frozen 21-scenario contract.
7
+ cyberfleet-batch change request; the `touch-set-correction` node of the SDD project spec (in the
8
+ cyberplace repository, not shipped in this package) carries the authoritative behavior description
9
+ and the frozen 21-scenario contract.
12
10
 
13
11
  - **Skill contract:** [`SKILL.md`](./SKILL.md)
14
12
  - **Script:** [`scripts/touch-set-correction.mts`](./scripts/touch-set-correction.mts)
@@ -4,8 +4,8 @@
4
4
  // touch-set (recovered from `git diff base..head`). It composes three tools — `git diff` (changed
5
5
  // files), `resolve-governances` (each file's artifact-type, best-effort), and `gherkin-cli diff`
6
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). See
8
- // .agents/specs/sdd/touch-set-correction/README.md for the full contract.
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
9
  //
10
10
  // Architecture — pure derivation kept apart from IO, on purpose (mission-graph.mts's convention):
11
11
  // - isFeature / fileToNode / reconcile / assembleCorrection are PURE: they take and return plain
@@ -27,9 +27,9 @@
27
27
  // Pure functions are exported for node:test; running the file directly drives the CLI.
28
28
 
29
29
  import { execFileSync } from 'node:child_process'
30
- import { readFileSync } from 'node:fs'
30
+ import { readFileSync, realpathSync } from 'node:fs'
31
31
  import { dirname, join, relative, resolve, sep } from 'node:path'
32
- import { fileURLToPath } from 'node:url'
32
+ import { fileURLToPath, pathToFileURL } from 'node:url'
33
33
  import { type DiffReader, diffFeatures } from 'gherkin-cli'
34
34
 
35
35
  // ── Types ──
@@ -415,4 +415,6 @@ export function main(argv: string[]): number {
415
415
  return 0
416
416
  }
417
417
 
418
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
418
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
419
+ process.exit(main(process.argv.slice(2)))
420
+ }
@@ -29,6 +29,13 @@ artifact-type already falls through to SDD defaults).
29
29
  - **Many-to-one is fine.** Two tests can bind the same key; the fold is PASS only if none of them
30
30
  fail. A test that maps to an already-covered key and isn't the canonical rename shows up as an
31
31
  EXTRA (diagnostic, not a failure) — leave it.
32
+ - **A punctuation-only near-miss still binds.** When no result matches a key exactly, the fold
33
+ retries on a comparison key that folds curly quotes/apostrophes, dashes, and the ellipsis to their
34
+ ASCII forms and collapses whitespace — so a pasted `’` for a `'` does not land the same scenario in
35
+ BOTH the UNBOUND list and the EXTRA list with nothing linking them. The bind is reported as a
36
+ **PROBABLE TITLE MISMATCH** naming both verbatim titles, so the typo still gets fixed. Nothing
37
+ rewrites a title, an **exact** match always wins, case is **not** folded, and an **ambiguous** fold
38
+ (two candidates folding alike) stays UNBOUND rather than binding the wrong one.
32
39
 
33
40
  ## Config schema
34
41
 
@@ -76,9 +83,9 @@ node "<skill>/scripts/verify-scenarios.mts" \
76
83
  - `--report <xml>` bypasses `--config` entirely — a single ad-hoc junit source, no command.
77
84
  - `--run` executes each source's `command` first; without it, existing reports are read as-is.
78
85
  - Default output is a readable per-scenario table + a `N/M BOUND, P pass, F fail, U unbound`
79
- summary line + any EXTRA keys. `--format json` emits
80
- `{node,total,bound,pass,fail,unbound,scenarios[],extras[]}`. `--format toon` emits the repo's
81
- TOON tabular form.
86
+ summary line + any EXTRA keys + any PROBABLE TITLE MISMATCH pairs. `--format json` emits
87
+ `{node,total,bound,pass,fail,unbound,scenarios[],extras[],mismatches[]}`. `--format toon` emits the
88
+ repo's TOON tabular form.
82
89
  - Exit code is non-zero when any scenario is UNBOUND or FAIL; zero only at full BOUND+PASS.
83
90
 
84
91
  ## Monorepo rooting — `--feature-root` vs. `--root`
@@ -35,11 +35,20 @@
35
35
  // feature scenario KEY: UNBOUND if no result; PASS if ≥1 result and none fail; FAIL if any fails.
36
36
  // EXTRA = bound result keys matching no feature scenario key (diagnostic, not a failure).
37
37
  //
38
+ // Near-miss binding: a key that matches no result EXACTLY gets a second pass on a punctuation- and
39
+ // whitespace-folded COMPARISON key (a pasted curly apostrophe, an em dash, a non-breaking space),
40
+ // so a real binding is not split across the UNBOUND list and the EXTRA list with nothing saying the
41
+ // two are the same scenario. Only unclaimed results are eligible, and only an unambiguous 1:1 fold
42
+ // binds — an ambiguous fold stays UNBOUND rather than manufacturing a false bind. Nothing rewrites
43
+ // a title: the fold is the only place the folded form exists, and every reported field carries the
44
+ // verbatim key, with the matched test title reported alongside it as a probable title mismatch.
45
+ //
38
46
  // Pure functions are exported for node:test; running the file directly drives the CLI.
39
47
 
40
48
  import { execSync } from 'node:child_process'
41
- import { existsSync, readFileSync } from 'node:fs'
49
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
42
50
  import { isAbsolute, join } from 'node:path'
51
+ import { pathToFileURL } from 'node:url'
43
52
  import { parseFeatures } from 'gherkin-cli'
44
53
 
45
54
  // Resolves a path argument against `--root`: relative paths join beneath root (which defaults to the
@@ -248,6 +257,15 @@ export interface ScenarioReport {
248
257
  key: string
249
258
  state: ScenarioState
250
259
  resultCount: number
260
+ /** The result key this scenario bound to when it bound on the folded comparison key, not exactly. */
261
+ matchedKey?: string
262
+ }
263
+
264
+ export interface TitleMismatch {
265
+ /** The frozen scenario's verbatim key. */
266
+ key: string
267
+ /** The test's verbatim leaf title it bound to — differing only by punctuation or whitespace. */
268
+ matchedKey: string
251
269
  }
252
270
 
253
271
  export interface FoldReport {
@@ -259,6 +277,43 @@ export interface FoldReport {
259
277
  unbound: number
260
278
  scenarios: ScenarioReport[]
261
279
  extras: string[]
280
+ /** Bindings made on the folded comparison key — a probable one-character typo, reported so it gets fixed. */
281
+ mismatches: TitleMismatch[]
282
+ }
283
+
284
+ // Punctuation and whitespace variants that carry no meaning of their own: the smart-quote and dash
285
+ // substitutions an editor, a copy-paste, or a Markdown renderer makes to a title on its way into a
286
+ // test file. Case is deliberately NOT folded — a differing case is not a punctuation variant, and
287
+ // folding it would bind two titles the author wrote differently on purpose.
288
+ const PUNCTUATION_FOLDS: [RegExp, string][] = [
289
+ [/[‘’‚‛′´]/g, "'"], // curly/prime apostrophes -> straight
290
+ [/[“”„‟″]/g, '"'], // curly double quotes -> straight
291
+ [/[‐‑‒–—―−]/g, '-'], // hyphens/dashes/minus -> hyphen
292
+ [/…/g, '...'], // ellipsis -> three dots
293
+ ]
294
+ // A no-break/thin/figure space needs no entry of its own — the trailing `\s+` collapse below
295
+ // already covers every Unicode space separator.
296
+
297
+ // The MATCH-ONLY form of a key: Unicode-composed, punctuation-folded, whitespace-collapsed. Never
298
+ // written back anywhere — reports carry the verbatim key.
299
+ export function comparisonKey(key: string): string {
300
+ let out = key.normalize('NFC')
301
+ for (const [re, rep] of PUNCTUATION_FOLDS) out = out.replace(re, rep)
302
+ return out.replace(/\s+/g, ' ').trim()
303
+ }
304
+
305
+ // Indexes values by their comparison key, dropping every comparison key claimed by more than one
306
+ // value — an ambiguous fold must not bind, so only the 1:1 entries survive.
307
+ function unambiguousByComparisonKey(keys: string[]): Map<string, string> {
308
+ const index = new Map<string, string>()
309
+ const ambiguous = new Set<string>()
310
+ for (const k of keys) {
311
+ const c = comparisonKey(k)
312
+ if (index.has(c)) ambiguous.add(c)
313
+ else index.set(c, k)
314
+ }
315
+ for (const c of ambiguous) index.delete(c)
316
+ return index
262
317
  }
263
318
 
264
319
  export function foldResults(scenarioKeys: ScenarioKey[], results: BoundResult[], node: string): FoldReport {
@@ -270,21 +325,39 @@ export function foldResults(scenarioKeys: ScenarioKey[], results: BoundResult[],
270
325
  else byKey.set(r.key, [r])
271
326
  }
272
327
 
328
+ const featureKeys = new Set(scenarioKeys.map((s) => s.key))
329
+ // Only a result no scenario claims exactly is eligible for a near-miss bind, and only a scenario
330
+ // that found no exact result looks for one — an exact binding always wins.
331
+ const unclaimed = [...byKey.keys()].filter((k) => !featureKeys.has(k))
332
+ const unmatchedScenarioKeys = scenarioKeys.map((s) => s.key).filter((k) => !byKey.has(k))
333
+ const unclaimedByComparison = unambiguousByComparisonKey(unclaimed)
334
+ const unmatchedByComparison = unambiguousByComparisonKey(unmatchedScenarioKeys)
335
+
336
+ const nearMisses = new Map<string, string>() // scenario key -> result key
337
+ for (const [comparison, scenarioKey] of unmatchedByComparison) {
338
+ const resultKey = unclaimedByComparison.get(comparison)
339
+ if (resultKey !== undefined) nearMisses.set(scenarioKey, resultKey)
340
+ }
341
+
273
342
  const scenarios: ScenarioReport[] = scenarioKeys.map(({ name, key }) => {
274
- const group = byKey.get(key) ?? []
343
+ const matchedKey = byKey.has(key) ? undefined : nearMisses.get(key)
344
+ const group = byKey.get(matchedKey ?? key) ?? []
275
345
  const state: ScenarioState =
276
346
  group.length === 0 ? 'unbound' : group.some((r) => r.outcome === 'fail') ? 'fail' : 'pass'
277
- return { name, key, state, resultCount: group.length }
347
+ return { name, key, state, resultCount: group.length, ...(matchedKey === undefined ? {} : { matchedKey }) }
278
348
  })
279
349
 
280
- const featureKeys = new Set(scenarioKeys.map((s) => s.key))
281
- const extras = [...byKey.keys()].filter((k) => !featureKeys.has(k)).sort()
350
+ const nearMissKeys = new Set(nearMisses.values())
351
+ const extras = unclaimed.filter((k) => !nearMissKeys.has(k)).sort()
352
+ const mismatches: TitleMismatch[] = scenarios
353
+ .filter((s): s is ScenarioReport & { matchedKey: string } => s.matchedKey !== undefined)
354
+ .map((s) => ({ key: s.key, matchedKey: s.matchedKey }))
282
355
 
283
356
  const pass = scenarios.filter((s) => s.state === 'pass').length
284
357
  const fail = scenarios.filter((s) => s.state === 'fail').length
285
358
  const unbound = scenarios.filter((s) => s.state === 'unbound').length
286
359
 
287
- return { node, total: scenarios.length, bound: pass + fail, pass, fail, unbound, scenarios, extras }
360
+ return { node, total: scenarios.length, bound: pass + fail, pass, fail, unbound, scenarios, extras, mismatches }
288
361
  }
289
362
 
290
363
  // ── Output ──
@@ -300,6 +373,10 @@ export function formatToon(report: FoldReport): string {
300
373
  (s) => ` ${[toonField(s.name), toonField(s.key), s.state, String(s.resultCount)].join(',')}`,
301
374
  )
302
375
  const extras = [`extras[${report.extras.length}]{key}:`, ...report.extras.map((e) => ` ${toonField(e)}`)]
376
+ const mismatches = [
377
+ `mismatches[${report.mismatches.length}]{key,matchedKey}:`,
378
+ ...report.mismatches.map((m) => ` ${[toonField(m.key), toonField(m.matchedKey)].join(',')}`),
379
+ ]
303
380
  const summary = `summary{node,total,bound,pass,fail,unbound}:\n ${[
304
381
  toonField(report.node),
305
382
  report.total,
@@ -308,7 +385,7 @@ export function formatToon(report: FoldReport): string {
308
385
  report.fail,
309
386
  report.unbound,
310
387
  ].join(',')}`
311
- return [header, ...rows, ...extras, summary].join('\n')
388
+ return [header, ...rows, ...extras, ...mismatches, summary].join('\n')
312
389
  }
313
390
 
314
391
  export function formatText(report: FoldReport): string {
@@ -322,6 +399,11 @@ export function formatText(report: FoldReport): string {
322
399
  lines.push('')
323
400
  lines.push(`EXTRA (bound results matching no scenario): ${report.extras.join(', ')}`)
324
401
  }
402
+ if (report.mismatches.length > 0) {
403
+ lines.push('')
404
+ lines.push('PROBABLE TITLE MISMATCH (bound on punctuation/whitespace only — fix the test title):')
405
+ for (const m of report.mismatches) lines.push(` scenario "${m.key}" <- test "${m.matchedKey}"`)
406
+ }
325
407
  return lines.join('\n')
326
408
  }
327
409
 
@@ -383,4 +465,6 @@ export function main(argv: string[]): number {
383
465
  return exitCode(report)
384
466
  }
385
467
 
386
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
468
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
469
+ process.exit(main(process.argv.slice(2)))
470
+ }