cyber-sdd 0.0.0 → 0.2.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 (71) 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/architect-spec-governance/README.md +1 -0
  10. package/skills/architect-spec-governance/SKILL.md +12 -1
  11. package/skills/blast-estimate/README.md +3 -5
  12. package/skills/blast-estimate/SKILL.md +2 -2
  13. package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
  14. package/skills/builder-impl-governance/SKILL.md +9 -1
  15. package/skills/builder-spec-governance/README.md +1 -0
  16. package/skills/builder-spec-governance/SKILL.md +31 -3
  17. package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
  18. package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
  19. package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
  20. package/skills/check-retired-terms/README.md +18 -0
  21. package/skills/check-retired-terms/SKILL.md +81 -0
  22. package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
  23. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
  25. package/skills/collision-ladder/README.md +3 -5
  26. package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
  27. package/skills/combat-log-governance/SKILL.md +43 -4
  28. package/skills/concept-index/scripts/concept-index.mts +3 -2
  29. package/skills/discover-plans/scripts/discover-plans.mts +5 -2
  30. package/skills/discover-specs/scripts/discover-specs.mts +5 -2
  31. package/skills/doctrine-loop/README.md +6 -0
  32. package/skills/doctrine-loop/SKILL.md +136 -2
  33. package/skills/formation-loop/SKILL.md +21 -1
  34. package/skills/gate-validation-governance/SKILL.md +2 -2
  35. package/skills/impl-producer-governance/SKILL.md +10 -1
  36. package/skills/init/scripts/wire-statusline.mts +5 -2
  37. package/skills/lifecycle-governance/SKILL.md +1 -1
  38. package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
  39. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
  40. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
  41. package/skills/mission-graph/README.md +3 -5
  42. package/skills/mission-graph/SKILL.md +72 -5
  43. package/skills/mission-graph/scripts/mission-graph.mts +505 -16
  44. package/skills/oracle-spec-governance/README.md +7 -2
  45. package/skills/oracle-spec-governance/SKILL.md +21 -4
  46. package/skills/place-node/scripts/place-node.mts +3 -2
  47. package/skills/plan-retirement/README.md +5 -2
  48. package/skills/plan-retirement/SKILL.md +5 -1
  49. package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
  50. package/skills/plugin-contract-governance/SKILL.md +7 -1
  51. package/skills/remediation-governance/SKILL.md +36 -1
  52. package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
  53. package/skills/resolve-tracking/SKILL.md +2 -2
  54. package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
  55. package/skills/sdd/SKILL.md +1 -1
  56. package/skills/spec-format-governance/README.md +1 -1
  57. package/skills/spec-format-governance/SKILL.md +76 -8
  58. package/skills/spec-gate/SKILL.md +18 -2
  59. package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
  60. package/skills/spec-gate/scripts/check-suite.mts +53 -17
  61. package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
  62. package/skills/spec-producer-governance/README.md +1 -1
  63. package/skills/spec-producer-governance/SKILL.md +7 -3
  64. package/skills/ssa-lowering/README.md +3 -5
  65. package/skills/start-mission/README.md +1 -1
  66. package/skills/start-mission/SKILL.md +9 -5
  67. package/skills/suite-format-governance/SKILL.md +43 -4
  68. package/skills/touch-set-correction/README.md +3 -5
  69. package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
  70. package/skills/verify-scenarios/SKILL.md +10 -3
  71. package/skills/verify-scenarios/scripts/verify-scenarios.mts +92 -8
@@ -23,13 +23,25 @@ writing, the cold spec-judge judges kill-or-ship. Oracle has no impl face. The S
23
23
  capabilities** — split them, each into its own node. Test: can you name the outcome without "and"?
24
24
  - **Scope is bounded and stated.** What is out of scope is named. A capability that keeps absorbing
25
25
  adjacent problems is scope creep — cut it back.
26
+ - **Every surface element is paid for by a use case.** An element the capability exposes — a flag,
27
+ an option, a parameter, a prop, an event — that **no use case needs** is unbought scope: the
28
+ verdict is cut it or name the use case, never ship it and see (`sdd:spec-format-governance`).
29
+ This is the same kill-or-ship judgment one level down: the capability answers for its cost, and so
30
+ does each thing it exposes. An actor or goal that restates the mechanism has not identified who
31
+ wants this — treat it as an unanswered Why, not as a filled-in field.
26
32
  - **The suite's decisions are the node's to hold.** Every scenario tests a **decision the node owns**.
27
33
  A property **co-owned** across a seam — activation/routing, a sibling's behavior, harness wiring —
28
34
  is out of scope: relocate it to the node that owns it, or kill it.
29
35
  - **Strict — a non-decision is killed.** An **invariant** that always holds is not acceptance and does
30
36
  not enter the suite. The one exception is a user **`@pinned`** scenario, kept whatever strict prunes.
31
- - **Worth shipping, or kill.** The Why names a real problem and who feels it. If value does not clear
32
- the cost of building, the verdict is **kill**.
37
+ - **The actors are enumerated, and the enumeration closes.** The Why names a real problem and **who
38
+ feels it** — so the use cases are derived from a stated set of actors, not from the interface
39
+ (`sdd:spec-format-governance`). Grade it **both ways**: an actor carrying no use case, and a use
40
+ case whose actor is absent from the list, are each a hole. A goal that restates the mechanism has
41
+ renamed the function rather than found the use case — an unanswered Why, not a filled-in field.
42
+ On a backfill, an enumeration drawn only from source is **served** use cases by construction:
43
+ judge whether the unserved ones were sought, not merely whether the list is tidy.
44
+ - **Worth shipping, or kill.** If value does not clear the cost of building, the verdict is **kill**.
33
45
  - **Kill-or-revert is allowed.** A capability that passes every check but proves fatal goes back to
34
46
  Draft — surface the deal-breaker.
35
47
  - **No premature commitment.** Defer a decision that need not be made yet to the last responsible
@@ -37,9 +49,14 @@ writing, the cold spec-judge judges kill-or-ship. Oracle has no impl face. The S
37
49
 
38
50
  ## Key points (read-check)
39
51
 
40
- 1. **One coherent intent** — two concerns are two capabilities; bounded and stated scope.
52
+ 1. **One coherent intent** — two concerns are two capabilities; bounded and stated scope. **Every
53
+ surface element is paid for by a use case** — an orphan element is unbought scope (cut or
54
+ justify); an actor/goal restating the mechanism is an unanswered Why.
41
55
  2. **Every scenario tests a decision the node owns** — a co-owned seam property is out of scope
42
56
  (relocate or kill).
43
57
  3. **Strict** — an invariant / non-decision does not enter the suite; only a user `@pinned` scenario
44
58
  escapes.
45
- 4. **Worth shipping or kill** — value must clear the build cost; a fatal proof reverts to Draft.
59
+ 4. **The actors are enumerated and the enumeration closes** — graded both ways (an actor with no use
60
+ case, a use case with no listed actor); a goal restating the mechanism is an unanswered Why; a
61
+ backfill list drawn only from source covers the served cases by construction.
62
+ 5. **Worth shipping or kill** — value must clear the build cost; a fatal proof reverts to Draft.
@@ -10,8 +10,9 @@
10
10
  // No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions are exported for
11
11
  // node:test; running the file directly drives the CLI.
12
12
 
13
- import { readdirSync, readFileSync } from 'node:fs'
13
+ import { readdirSync, readFileSync, realpathSync } from 'node:fs'
14
14
  import { join } from 'node:path'
15
+ import { pathToFileURL } from 'node:url'
15
16
 
16
17
  const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
17
18
 
@@ -152,6 +153,6 @@ export function main(argv: string[]): number {
152
153
  return 0
153
154
  }
154
155
 
155
- if (import.meta.url === `file://${process.argv[1]}`) {
156
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
156
157
  process.exit(main(process.argv.slice(2)))
157
158
  }
@@ -7,8 +7,11 @@ gated, idempotent **tracked deletion** of a retired mission plan. It holds a sel
7
7
  **Distill and delete are decoupled.** The Scanner distills the combat log into ledger `strategy`
8
8
  early (at `→ implemented`); this sweep deletes the plan **later**, gated on source = `done`/merged
9
9
  **and** distilled. The two signals **split by verifiability**: **source** stays the caller's
10
- judgment (`sdd:doctrine-loop` Scanner passes the source-cleared set via `--retire`; it needs
11
- network/`gh`), while **distilled** is **verified mechanically by the sweep** against the project
10
+ judgment (`sdd:doctrine-loop`'s Scanner is the standard caller — during its pass it cross-checks
11
+ each brief's own `todos-all-done` against `source-closed` and passes `--retire` only the cr-refs
12
+ where **both** agree terminal; a disagreement is held back and flagged, never passed through on
13
+ source alone; querying source natively needs network/`gh`), while **distilled** is **verified
14
+ mechanically by the sweep** against the project
12
15
  ledger (`--ledger <dir>`) — a `strategy` entry with `distills == <cr-ref>` must exist (keyed on the
13
16
  `distills` field, never an `evidence` mention; unratified still counts). The distilled gate guards
14
17
  an **existing** combat log: a cr-ref whose `<cr-ref>.log.jsonl` was **never written** (a non-gated
@@ -42,7 +42,11 @@ The two gating signals split by what the sweep can check itself:
42
42
 
43
43
  - **source = `done`/merged** — the **caller's judgment**: query the source natively (`github-NN` → GH
44
44
  issue, `asana-<gid>` → Asana, `local-<slug>` → the local store); needs network/`gh`. The caller
45
- passes the source-cleared set via `--retire`.
45
+ passes the source-cleared set via `--retire`. `sdd:doctrine-loop`'s Scanner is the standard
46
+ caller: during its pass it cross-checks each brief's own `todos-all-done` against `source-closed`
47
+ and only passes through a cr-ref where **both** agree terminal — a disagreement (source closed
48
+ but the brief's own todos are not all done, or the reverse) is held back and surfaced as a
49
+ flagged finding for a human, never passed through on source alone.
46
50
  - **distilled** — **verified mechanically by the sweep**: a `strategy` entry with `distills ==
47
51
  <cr-ref>` must exist in the project ledger (`--ledger`). The sweep keys on the structured
48
52
  `distills` field, **never** a `<cr-ref>` that appears only in a strategy's `evidence`
@@ -39,8 +39,9 @@
39
39
  // Pure functions are exported for node:test; running the file directly drives the CLI.
40
40
  // No dependencies. Use --dry-run to print the planned deletions without touching the tree.
41
41
 
42
- import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs'
42
+ import { existsSync, readdirSync, readFileSync, realpathSync, unlinkSync } from 'node:fs'
43
43
  import { join } from 'node:path'
44
+ import { pathToFileURL } from 'node:url'
44
45
 
45
46
  const PLAN_SUFFIX = '.plan.md'
46
47
  const LOG_SUFFIX = '.log.jsonl'
@@ -193,4 +194,6 @@ export function main(argv: string[]): number {
193
194
  return 0
194
195
  }
195
196
 
196
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
197
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
198
+ process.exit(main(process.argv.slice(2)))
199
+ }
@@ -48,9 +48,15 @@ actor bars are the shipped `sdd:{oracle,builder,architect}-{spec,impl}-governanc
48
48
  self-aligns to exactly the bars its judge grades. The lens sets are spec gate `{oracle, builder,
49
49
  architect}`, impl gate `{builder, architect}`, solution `{architect}` (ungated).
50
50
 
51
+ **Read each row against that sentence.** A producer row that does not carry its whole lens set is a
52
+ transcription slip, not a narrowing — this table is a shipped copy of one owned by SDD's own spec
53
+ (`design/specialists-and-squads.md`), restated here because a governance loads standalone and cannot
54
+ reach the spec tree. It has drifted once: the spec-producer row lost `architect-spec`, and plugin
55
+ authors building to it shipped agents that loaded three bars and were graded against four.
56
+
51
57
  | Role | Loads |
52
58
  |---|---|
53
- | spec-producer | `spec-format`, `suite-format`, `ownership`, the resolved `oracle-spec` + `builder-spec` bars |
59
+ | spec-producer | `spec-format`, `suite-format`, `ownership`, the resolved `oracle-spec` + `builder-spec` + `architect-spec` bars |
54
60
  | solution-producer | `ownership`, the resolved `architect-spec` bar |
55
61
  | spec-judge | `spec-format`, `suite-format`, `lifecycle`, `gate-validation`, the resolved `oracle-spec` + `builder-spec` + `architect-spec` bars |
56
62
  | impl-producer | `ownership`, the resolved `builder-impl` + `architect-impl` bars |
@@ -31,6 +31,33 @@ producer is responding.
31
31
  artifact predates them. Any regression means the loop is **no longer converging**: stop, report
32
32
  it, and re-plan. Do not open another remediation round on a regressing loop.
33
33
 
34
+ ## The Clearance-repair proof — a repaired frozen scenario must fail its pre-repair draft
35
+
36
+ A `change` verdict that re-opens an **already-frozen** scenario under a **ratified Clearance re-open**
37
+ (a narrowing/rewrite of specified behavior, or an impl-gate Oracle-lens revert) carries one extra bar
38
+ on top of the four rules. The repair changes a contract that was already frozen, so the danger is a
39
+ **back-fit**: a "correction" reverse-engineered to fit whatever the current draft/implementation
40
+ already says, changing nothing of substance. The proof against that is directional —
41
+
42
+ > a genuine repair makes the **pre-repair** artifact **FAIL**; a contract narrowed to fit an existing
43
+ > draft moves *toward* passing it.
44
+
45
+ So the repair is re-approved only when the repaired scenario **fails when checked against the
46
+ pre-repair artifact/draft** — not merely that it passes against the post-repair one:
47
+
48
+ - a repaired scenario that **fails** the pre-repair artifact is **accepted** as a genuine contract
49
+ correction — the pre-repair failure is the evidence its substance changed;
50
+ - a repaired scenario that **already passes** the pre-repair artifact is **rejected** as a suspected
51
+ **back-fit** — it may be reverse-engineered from what already existed, not a real correction;
52
+ - a **post-repair pass with no demonstrated pre-repair failure is not enough** — a repair carrying no
53
+ pre-repair-failure proof is **not re-approved** (absence of the proof is not proof of substance).
54
+
55
+ The bar has **two faces**, both owed: the **producer** *demonstrates* the pre-repair failure as part
56
+ of the repair (run the repaired scenario against the pre-repair artifact and show it fails); the
57
+ **gate/judge** *requires* that proof before re-approving (a post-repair pass alone never re-approves).
58
+ An independent cold judge confirming the repaired scenario against the still-unrevised artifact is the
59
+ strongest form of the demonstration.
60
+
34
61
  ## A sweep is scope-aware, never a blanket match
35
62
 
36
63
  Rule 2's sweep answers "every instance of the rule", which is **not** "every occurrence of a string".
@@ -58,9 +85,13 @@ REMEDIATION:
58
85
  swept=<the other instances found, or none>
59
86
  ruled-out=<candidates inspected and excluded, with the reason>
60
87
  provenance=<pre-existing | regression>
88
+ pre-repair-proof=<the repaired scenario FAILS the pre-repair artifact | n/a — not a Clearance-gated frozen-scenario repair>
61
89
  ```
62
90
 
63
- A `contested` finding carries the evidence against it and **no edit** to the artifact it named.
91
+ A `contested` finding carries the evidence against it and **no edit** to the artifact it named. A
92
+ Clearance-gated repair of a frozen scenario carries its **`pre-repair-proof`** — the demonstration
93
+ that the repaired scenario fails the pre-repair artifact; a repair without it (or one that passes the
94
+ pre-repair draft) is not re-approved.
64
95
 
65
96
  ## Key points (read-check)
66
97
 
@@ -76,3 +107,7 @@ A `contested` finding carries the evidence against it and **no edit** to the art
76
107
  alone.
77
108
  6. **Provenance is derived from the diff** — an artifact changed by the previous round's commits
78
109
  makes its finding a **regression**, which stops the loop for a re-plan rather than another round.
110
+ 7. **A Clearance-gated repair of a frozen scenario must fail its pre-repair draft** — re-approval
111
+ requires the repaired scenario to **fail** against the pre-repair artifact (a repair that already
112
+ passes it is a suspected back-fit; a post-repair pass alone is not enough). The producer
113
+ demonstrates the failure; the gate/judge requires it.
@@ -19,8 +19,9 @@
19
19
  // Pure functions are exported for node:test; running the file directly drives the
20
20
  // CLI. No dependencies — plain node strips the types.
21
21
 
22
- import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs'
22
+ import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
23
23
  import { join } from 'node:path'
24
+ import { pathToFileURL } from 'node:url'
24
25
 
25
26
  // ─── the closed sets ───────────────────────────────────────────────────────────
26
27
 
@@ -512,4 +513,6 @@ export function main(argv: string[]): number {
512
513
  return 0
513
514
  }
514
515
 
515
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
516
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
517
+ process.exit(main(process.argv.slice(2)))
518
+ }
@@ -8,8 +8,8 @@ metadata:
8
8
 
9
9
  # Resolve Tracking
10
10
 
11
- The concrete engine for **tracking resolution** — the second escape-hatch trigger
12
- (`.agents/specs/sdd/intake/README.md`). For one touched artifact it decides **tracked** (SDD
11
+ The concrete engine for **tracking resolution** — the second escape-hatch trigger of the SDD
12
+ project spec's `intake` node (repo-only). For one touched artifact it decides **tracked** (SDD
13
13
  governs it — spec + gates) or **ignored** (SDD does not govern it; it still gets built) and
14
14
  reports which step decided it, so the conductor can skip a task outright (no CR, no draft, no
15
15
  gate, no record) when it resolves ignored. The split mirrors git's **tracked vs ignored** files.
@@ -1,9 +1,10 @@
1
1
  // resolve-tracking — resolve one artifact's tracking signal (tracked | ignored).
2
- // Self-contained, no deps (repo's node-≥23.6 convention). Spec:
3
- // .agents/specs/sdd/intake/resolve-tracking/README.md
2
+ // Self-contained, no deps (repo's node-≥23.6 convention). Spec: the intake/resolve-tracking
3
+ // node of the SDD project spec (repo-only).
4
4
 
5
- import { existsSync, readFileSync } from 'node:fs'
5
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
+ import { pathToFileURL } from 'node:url'
7
8
 
8
9
  export type Tracking = 'tracked' | 'ignored'
9
10
 
@@ -208,6 +209,6 @@ export function main(argv: string[]): void {
208
209
  process.stdout.write(`reason: ${result.reason}\n`)
209
210
  }
210
211
 
211
- if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
212
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
212
213
  main(process.argv.slice(2))
213
214
  }
@@ -17,7 +17,7 @@ Treat `$sdd`, "use SDD", and "use Spec-Driven Development" as explicit activatio
17
17
 
18
18
  ### Surface pending strategy
19
19
 
20
- When the Council re-enters, **surface the count of pending (unratified) strategy** — count the `strategy` lines with `"ratified": false` globbed from the project's **root `ledger/` shards** (the durable sibling directory of the root `spec.md`; a legacy `ledger.jsonl` still counts) and state "N pending strategy" alongside the intake; if the Council picks it, route them to review those entries. Count only `kind: strategy` — the conductor's `kind: leash` run-start blocks are **not** strategy and never counted. The gateway only *surfaces* the count — it never **drafts** strategy (the Scanner's job) nor **ratifies** it (the Council's positional act). A zero count is not surfaced. (`strategy` lives in the durable `ledger/` shards, **never** in the per-mission `*.log.jsonl` combat log.)
20
+ When the Council re-enters, **surface the count of pending (unratified) strategy** — count the `strategy` lines with `"ratified": false` **and `disposition: open`-or-absent** globbed from the project's **root `ledger/` shards** (the durable sibling directory of the root `spec.md`; a legacy `ledger.jsonl` still counts) and state "N pending strategy" alongside the intake; if the Council picks it, route them to review those entries. Count only `kind: strategy` — the conductor's `kind: leash` run-start blocks are **not** strategy and never counted, and a `disposition: resolved` line is the Scanner's validation **tombstone** (a cut, `sdd:combat-log-governance`), **never counted** even though it is `kind: strategy` with `"ratified": false`. The gateway only *surfaces* the count — it never **drafts** strategy (the Scanner's job) nor **ratifies** it (the Council's positional act). A zero count is not surfaced. (`strategy` lives in the durable `ledger/` shards, **never** in the per-mission `*.log.jsonl` combat log.)
21
21
 
22
22
  ### Surface in-progress missions
23
23
 
@@ -44,7 +44,7 @@ A `spec.md` has **four sections, in this order**:
44
44
  | Section | What goes in it |
45
45
  | --- | --- |
46
46
  | `## What` | What the capability is, the problem it solves, who has that problem, and what it deliberately does not do (non-goals). |
47
- | `## Use Cases` | Every distinct way the capability is invoked — one row each, as trigger / inputs / outcome. Each is named after the thing you actually call: a CLI verb, a function, an endpoint. |
47
+ | `## Use Cases` | Every distinct way the capability is invoked, each named after the thing you actually call (a CLI verb, a function, an endpoint) and carrying four parts: the **actor and their goal**, the **entry point** (trigger / inputs / outcome), and its **extensions** — what else can happen, each divergence with its cause and outcome. Plus a trace of every element the capability exposes (flag, option, parameter, prop, event) to the use case that needs it and the elements it may not combine with. An element no use case needs is an orphan: cut it or justify it. |
48
48
  | `## Control Flow` | The decisions the capability makes once invoked, taken as one **control-flow graph (CFG)** and **drawn** as a diagram rather than described in prose. Use cases feed into one CFG; several usually share it. |
49
49
  | `## Scenario map` | A table pairing each branch in that diagram with the one test scenario covering it, grouped by use case. One-to-one, both directions — so a gap in coverage is visible instead of buried in prose. |
50
50
 
@@ -20,12 +20,73 @@ deliberately excludes). One or two short paragraphs; add a **Key terms** glossar
20
20
  jargon. Legible to a non-engineer.
21
21
 
22
22
  ### `## Use Cases`
23
- The **entry points** — one row per distinct way the capability is invoked, each **named to its
24
- implementation surface** (a CLI verb, a public function, an endpoint), given as
25
- **trigger / inputs / outcome**. A use case answers *"when, and with what, is this invoked?"* — never
26
- *"given this state, does it do that?"* (that is a scenario). Naming the impl surface keeps the spec,
27
- the suite, and the code on **one screaming structure**: the builder gives each use case its own
28
- module, so each change stays local.
23
+ One entry per distinct way the capability is invoked, each **named to its implementation surface**
24
+ (a CLI verb, a public function, an endpoint) and carrying four parts: **actor / goal**, the
25
+ **entry point** (trigger / inputs / outcome), and its **extensions**. Naming the impl surface keeps
26
+ the spec, the suite, and the code on **one screaming structure**: the builder gives each use case
27
+ its own module, so each change stays local.
28
+
29
+ A use case answers *"who is trying to do what, how do they invoke it, and what else can happen?"* —
30
+ never *"given this state, does it do that?"* (that is a scenario).
31
+
32
+ **Enumerate by actor, never by entry point.** Walking the interface and asking who calls each entry
33
+ point can only return use cases the interface already implies — it reproduces the surface and calls
34
+ it a requirement, and it is structurally blind to the use case nobody has built yet. So the section
35
+ is derived the other way round:
36
+
37
+ 1. **List the actors** — every person in a role, sibling capability, scheduler, or operator that
38
+ reaches this capability, **plus** whoever is affected by its outcome without invoking it (the
39
+ reviewer, the on-call, the next agent in a chain). The second group are stakeholders rather than
40
+ actors and are where a missed use case usually hides.
41
+ 2. **Per actor, name the goals** they come to this capability with — their result, not the call they
42
+ make.
43
+ 3. **Then map goals to entry points.** A goal with **no** entry point is the finding this ordering
44
+ exists to surface: either the capability is missing a way in, or the goal belongs to another
45
+ node. An entry point serving **no** listed actor's goal is the mirror finding — it is surface
46
+ nobody asked for.
47
+
48
+ The enumeration is checkable both ways: an actor carrying no use case, and a use case whose actor
49
+ is absent from the list, are each a hole. On **backfill** the source yields only the *served* use
50
+ cases by construction — recover the unserved ones from the request history, the issue tracker, and
51
+ recurring workarounds, and record where each came from.
52
+
53
+ - **Actor and goal — one line each, not a persona.** Name who invokes it (a person in a role,
54
+ another capability, a scheduler) and the outcome **they** want, stated as their result rather
55
+ than the mechanism ("recover the work after a crash", not "calls `resume()`"). An actor may be
56
+ an agent or a sibling capability; that is normal, not a degenerate case. Where the goal restates
57
+ the function name, the use case has not been found yet — it has been renamed.
58
+ - **Entry point** — the trigger, its inputs, and the success outcome. A table is the usual form.
59
+ - **Extensions — what else can happen, and the instrument that finds it.** An extension is **any
60
+ path from this use case's trigger that does not reach its success outcome**; state each with its
61
+ cause and its outcome. That criterion decides membership — the recurring kinds (an error, a
62
+ refusal, a boundary, a partial result, a contended or absent input) are a **prompt to search, not
63
+ a closed set**, so a divergence matching none of them still belongs and a kind that cannot arise
64
+ here is not owed a row. **A use case with no extensions is a claim that nothing can go wrong** —
65
+ state that claim explicitly (`extensions: none — <why>`) rather than leaving the field off, so a
66
+ reviewer can disagree with it.
67
+
68
+ Extensions are a **discovery instrument, not a second specification.** They exist to make the
69
+ **CFG complete**: a graph drawn from an implementation reproduces what the code already does and
70
+ can never tell you a branch is *missing*, whereas asking what can go wrong **for this actor**
71
+ finds it. So every extension you find belongs in `## Control Flow` as a path, and the scenarios
72
+ still derive from **the CFG alone** (`## Scenario map`, 1:1 on the **(path class, edge)** pair).
73
+ Never draw a scenario from the stated list directly: a suite derived from prose is 1:1 with that
74
+ prose by construction and can no longer surface a hole. A use case is therefore **not** 1:1 with
75
+ a scenario — one extension may need several scenarios where several path classes reach it, and
76
+ several extensions may reconverge onto one.
77
+
78
+ **Every element of the public surface traces to a use case that needs it.** List each element the
79
+ capability exposes — a flag, an option, a parameter, a prop, an event — against the use case
80
+ requiring it, and name the elements it **may not** be combined with. An element **no use case needs
81
+ is unjustified**: cut it, or name the use case. A pair whose combination is contradictory and
82
+ unstated is a gap, not a detail. This is the same orphan-detection discipline as `## Scenario map`,
83
+ applied one level up: there, a scenario with no edge is an orphan; here, an element with no use case
84
+ is an orphan.
85
+
86
+ Degenerate cases stay cheap. A capability exposing **one** entry point and **no** optional elements
87
+ carries the surface trace in a line, not a table — the obligation is that nothing on the surface is
88
+ unaccounted for, never that a table exists. A single-actor capability lists one actor; the
89
+ enumeration is the discipline, not the length.
29
90
 
30
91
  ### `## Control Flow`
31
92
  The **control-flow graph (CFG)** the capability runs once invoked, **drawn** as a fenced Mermaid
@@ -41,6 +102,11 @@ the edge alone: a scenario's `Given` is the path reaching the edge, its `When` i
41
102
  (`sdd:suite-format-governance`).
42
103
 
43
104
  - **1:1 scenario↔row** — every scenario has exactly one row, every row one scenario.
105
+ - **Name the scenario in backticks.** The `Scenario` cell holds the scenario's title **backtick-wrapped**
106
+ (`` `send text types literal text and presses no Enter` ``). This is how `check-suite` tells a data
107
+ row from the header and separator: a data row whose `Scenario` cell is **not** backtick-wrapped is
108
+ reported as an **unparseable row**, not silently skipped — a map that reads complete but binds nothing
109
+ is the exact gap the map exists to prevent.
44
110
  - **An edge may carry several rows.** That is **permutation coverage**, not duplication — legitimate
45
111
  when each row's path class yields a *different* outcome. Same edge *and* same path class twice is a
46
112
  duplicate.
@@ -103,8 +169,10 @@ Enrichment (diagrams, formatting) is `spec.md` only; the suite stays plain Gherk
103
169
  1. **Four sections in order** — `## What` (overview + non-goals), `## Use Cases`, `## Control Flow`,
104
170
  `## Scenario map` — plus an optional `## References` last, citing research that backs a decision
105
171
  (the claim it supports, not the topic).
106
- 2. **A use case is an entry point named to its impl surface** (CLI verb / function / endpoint) — spec,
107
- suite, and code share one screaming structure.
172
+ 2. **A use case is actor + goal + entry point + extensions**, named to its impl surface (CLI verb /
173
+ function / endpoint) — spec, suite, and code share one screaming structure. No extensions is a
174
+ claim, stated explicitly. **Every surface element traces to a use case that needs it**, with its
175
+ forbidden combinations named; an element no use case needs is an orphan — cut it or justify it.
108
176
  3. **The CFG is shared** — use cases enter it (many-to-one); section by sub-graph only when the
109
177
  decision logic genuinely differs.
110
178
  4. **The scenario map is 1:1 and grouped by use case** — coverage visible per use case; `check-suite`
@@ -109,8 +109,17 @@ Resolve the **spec-judge** for each `artifact-types` (a plugin judge or the SDD
109
109
  **{oracle, builder, architect}**. Then take the judge's **contract-sync verdict** (derived at this
110
110
  gate, never stored) and **derive the leash** (the conductor's autonomy bar,
111
111
  baked into `start-mission`) in-session. Collect the judge's `STATUS`,
112
- `ALIGNED`, failing scenarios, remaining `<!-- open: -->` markers, `OBSERVATIONS`, and the gate
113
- report. The judge is a **distinct cold actor** and never edits the artifact it grades.
112
+ `ALIGNED`, failing scenarios, remaining `<!-- open: -->` markers, `CONFORMANCE`, `OBSERVATIONS`, and
113
+ the gate report. The judge is a **distinct cold actor** and never edits the artifact it grades.
114
+
115
+ **A `CONFORMANCE.result: warn` is surfaced, never a block.** When the judge reports a spec-format
116
+ conformance warning (a touched **behavioral** `spec.md` missing a required section — especially
117
+ `## Use Cases`, `## Control Flow` / CFG, or `## Scenario map`), **surface it in the gate report** and
118
+ **do not** let it advance, block, or set `ALIGNED: false` on its own — it is a non-blocking finding
119
+ like `CONTENT_GAPS` or an introduced-reference finding, distinct from the deterministic structural
120
+ fail-closed checks and from a lens failure. The advance is governed by the lenses, the open markers,
121
+ and the alignment verdict exactly as before; the conformance warning rides alongside them in the
122
+ report.
114
123
 
115
124
  **Never advance** — by self-assertion or human verdict — with judge failures, any remaining open
116
125
  markers, or a misaligned suite. They fail the confidence dimension, so they forbid self-assertion
@@ -162,6 +171,10 @@ node "<skill>/scripts/classify-edit-class.mts" --files <the CR's touched .featur
162
171
 
163
172
  A `narrowing`/`mixed` result on a still-`@frozen` file routes to **Clearance** (escalated unless the CR
164
173
  pre-authorized it); `additive`/`no-content-change` self-clears; `unfrozen-skip` needs no edit-class gate.
174
+ When a Clearance re-open **repairs** an already-frozen scenario, re-approving the repair carries the
175
+ **pre-repair-failure proof** bar (`sdd:remediation-governance`): the repaired scenario must **fail**
176
+ against the pre-repair artifact — a repair that already passes it is a suspected back-fit, and a
177
+ post-repair pass alone never re-approves.
165
178
  `spec.md`
166
179
  / the node READMEs are **kept aligned, never frozen** — editable, but may not contradict a frozen
167
180
  scenario (enforced by the alignment check and the judge, not a flat freeze). Vocabulary is
@@ -192,6 +205,9 @@ nothing, advances no status, renders no verdict**. Fixed sections:
192
205
  ## Report
193
206
 
194
207
  - PASS / FAIL per lens, relayed from the judge
208
+ - **Spec-format conformance:** the judge's `CONFORMANCE` — on `warn`, a **warning** line naming each
209
+ missing required section (Use Cases / Control Flow / Scenario map) on the touched behavioral
210
+ `spec.md`; non-blocking, surfaced alongside the verdict, never a block on its own
195
211
  - `ALIGNED: true | false`; if false, which artifacts are out of sync
196
212
  - Open markers / failing scenarios still blocking, if any
197
213
  - The leash derivation and the effective leash for this gate
@@ -35,8 +35,9 @@
35
35
  // exported for node:test; running the file directly drives the CLI. No dependencies.
36
36
 
37
37
  import { execFileSync } from 'node:child_process'
38
- import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs'
38
+ import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
39
39
  import { basename, dirname, join } from 'node:path'
40
+ import { pathToFileURL } from 'node:url'
40
41
 
41
42
  export interface GateVerdict {
42
43
  verdict?: string
@@ -63,6 +64,12 @@ export interface LedgerGate {
63
64
  verdict: string
64
65
  }
65
66
 
67
+ // The lifecycle enum (lifecycle-governance), mirrored from discover-specs' recognition filter.
68
+ // Discovery *drops* a spec whose status is outside it, so every engine iterating discovered
69
+ // specs skips it silently; this check walks the tree itself, so it is the one place that still
70
+ // sees the file — and must escalate rather than exempt it (a status typo would otherwise remove
71
+ // a spec from all checking with no signal).
72
+ const LIFECYCLE_STATUSES = ['draft', 'approved', 'implemented', 'deprecated']
66
73
  const GATES = ['spec', 'impl']
67
74
  const VERDICTS = ['approve', 'pause', 'reject']
68
75
  const SPEC_TYPES = ['reference', 'behavioral']
@@ -132,6 +139,17 @@ export function checkSpec(slug: string, state: SpecState): string[] {
132
139
  const v: string[] = []
133
140
  const tag = (msg: string) => v.push(`${slug}: ${msg}`)
134
141
 
142
+ // The status must be classifiable before anything below it means anything: an
143
+ // out-of-enum (or absent) status makes every tuple rule below vacuous — the spec
144
+ // reads as neither approved nor implemented and passes silently, while discovery
145
+ // has already dropped it from every other engine. Unclassifiable is a failure.
146
+ if (!LIFECYCLE_STATUSES.includes(status))
147
+ tag(
148
+ status === ''
149
+ ? `no lifecycle status in frontmatter — a spec.md must declare status (${LIFECYCLE_STATUSES.join(' | ')}), and one that does not is checked by nothing`
150
+ : `status "${status}" is not in the lifecycle enum (${LIFECYCLE_STATUSES.join(' | ')}) — discovery drops it, so it is checked by nothing`,
151
+ )
152
+
135
153
  // `implemented` is backed by the impl gate's runtime suite run (ADR-0017), not a
136
154
  // stored flag — the static guard here is the recorded approval.impl ratification
137
155
  // (below). No `aligned` cross-check.
@@ -436,6 +454,11 @@ export function filterProseMdInSpecTree(paths: string[]): string[] {
436
454
  export interface UseCaseScenarioRefs {
437
455
  hasSection: boolean
438
456
  refs: string[]
457
+ // Trimmed text of each DATA row whose Scenario cell carries no backtick reference —
458
+ // missing, empty, or present-but-unparseable. Every one is surfaced as a violation
459
+ // rather than silently dropped: a row that names no scenario is a coverage gap, and
460
+ // exempting it is the fail-open shape this check exists to close.
461
+ unparseable: string[]
439
462
  }
440
463
 
441
464
  // A markdown table row: strip the leading/trailing `|` then split on `|`, trimming each cell.
@@ -465,26 +488,32 @@ export function extractUseCaseScenarioRefs(text: string): UseCaseScenarioRefs {
465
488
  // would erase the very refs this function extracts).
466
489
  const body = text.replace(/```[\s\S]*?```/g, '')
467
490
  const section = extractSection(body, 'Use Cases')
468
- if (section === null) return { hasSection: false, refs: [] }
491
+ if (section === null) return { hasSection: false, refs: [], unparseable: [] }
469
492
  const lines = section.split('\n')
470
493
  const headerIdx = lines.findIndex((l) => l.trim().startsWith('|'))
471
- if (headerIdx === -1) return { hasSection: true, refs: [] } // prose or EARS — no table
494
+ if (headerIdx === -1) return { hasSection: true, refs: [], unparseable: [] } // prose or EARS — no table
472
495
  const header = splitTableRow(lines[headerIdx])
473
496
  const scenarioIdx = header.findIndex((c) => /^scenario$/i.test(c))
474
- if (scenarioIdx === -1) return { hasSection: true, refs: [] } // table with no Scenario column
497
+ if (scenarioIdx === -1) return { hasSection: true, refs: [], unparseable: [] } // table with no Scenario column
475
498
 
476
499
  const refs: string[] = []
500
+ const unparseable: string[] = []
477
501
  // data rows start after the header separator (`|---|---|`); stop at the first
478
- // non-`|` line, which ends the contiguous table block.
502
+ // non-`|` line, which ends the contiguous table block. A data row whose Scenario
503
+ // cell carries no backtick reference is collected as unparseable — never silently
504
+ // skipped, which would fail this coverage check open.
479
505
  for (let i = headerIdx + 2; i < lines.length; i++) {
480
506
  if (!lines[i].trim().startsWith('|')) break
481
507
  const cells = splitTableRow(lines[i])
482
- const cell = cells[scenarioIdx]
483
- if (!cell) continue
508
+ // A missing cell (a row shorter than the header) and an empty one are the same
509
+ // defect as an un-backticked one: the row names no covering scenario. All three
510
+ // are collected — skipping any of them fails this coverage check open.
511
+ const cell = cells[scenarioIdx] ?? ''
484
512
  const ref = /`([^`\n]+)`/.exec(cell)
485
513
  if (ref) refs.push(ref[1].trim())
514
+ else unparseable.push(lines[i].trim())
486
515
  }
487
- return { hasSection: true, refs }
516
+ return { hasSection: true, refs, unparseable }
488
517
  }
489
518
 
490
519
  function escapeRegExp(s: string): string {
@@ -520,8 +549,13 @@ export function findSiblingFeature(dir: string): string | null {
520
549
  export function checkUseCaseCoverage(slug: string, dir: string, text: string): string[] {
521
550
  const v: string[] = []
522
551
  const tag = (msg: string) => v.push(`${slug}: ${msg}`)
523
- const { hasSection, refs } = extractUseCaseScenarioRefs(text)
524
- if (!hasSection || refs.length === 0) return v
552
+ const { hasSection, refs, unparseable } = extractUseCaseScenarioRefs(text)
553
+ if (!hasSection) return v
554
+
555
+ for (const raw of unparseable) {
556
+ tag(`Use Cases data row has no backtick-wrapped Scenario cell — ${raw}`)
557
+ }
558
+ if (refs.length === 0) return v
525
559
 
526
560
  const featurePath = findSiblingFeature(dir)
527
561
  const featureText = featurePath ? readFileSync(featurePath, 'utf8') : ''
@@ -598,4 +632,6 @@ export function main(argv: string[]): number {
598
632
  return 0
599
633
  }
600
634
 
601
- if (import.meta.main) process.exit(main(process.argv.slice(2)))
635
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
636
+ process.exit(main(process.argv.slice(2)))
637
+ }