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.
- package/.plugin/pins.json +3 -0
- package/LICENSE +21 -0
- package/agents/sdd-automaton.md +13 -2
- package/agents/sdd-scanner.md +85 -0
- package/agents/sdd-spec-judge.md +32 -2
- package/agents/sdd-warden.md +9 -0
- package/package.json +30 -23
- package/skills/align-spec/scripts/align-spec.mts +3 -2
- package/skills/architect-spec-governance/README.md +1 -0
- package/skills/architect-spec-governance/SKILL.md +12 -1
- package/skills/blast-estimate/README.md +3 -5
- package/skills/blast-estimate/SKILL.md +2 -2
- package/skills/blast-estimate/scripts/blast-estimate.mts +7 -4
- package/skills/builder-impl-governance/SKILL.md +9 -1
- package/skills/builder-spec-governance/README.md +1 -0
- package/skills/builder-spec-governance/SKILL.md +31 -3
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +3 -1
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +5 -2
- package/skills/check-project-specs/scripts/check-project-specs.mts +67 -5
- package/skills/check-retired-terms/README.md +18 -0
- package/skills/check-retired-terms/SKILL.md +81 -0
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +293 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +3 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +3 -2
- package/skills/collision-ladder/README.md +3 -5
- package/skills/collision-ladder/scripts/collision-ladder.mts +7 -4
- package/skills/combat-log-governance/SKILL.md +43 -4
- package/skills/concept-index/scripts/concept-index.mts +3 -2
- package/skills/discover-plans/scripts/discover-plans.mts +5 -2
- package/skills/discover-specs/scripts/discover-specs.mts +5 -2
- package/skills/doctrine-loop/README.md +6 -0
- package/skills/doctrine-loop/SKILL.md +136 -2
- package/skills/formation-loop/SKILL.md +21 -1
- package/skills/gate-validation-governance/SKILL.md +2 -2
- package/skills/impl-producer-governance/SKILL.md +10 -1
- package/skills/init/scripts/wire-statusline.mts +5 -2
- package/skills/lifecycle-governance/SKILL.md +1 -1
- package/skills/manage-ignore/scripts/manage-ignore.mts +5 -2
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +5 -2
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +5 -2
- package/skills/mission-graph/README.md +3 -5
- package/skills/mission-graph/SKILL.md +72 -5
- package/skills/mission-graph/scripts/mission-graph.mts +505 -16
- package/skills/oracle-spec-governance/README.md +7 -2
- package/skills/oracle-spec-governance/SKILL.md +21 -4
- package/skills/place-node/scripts/place-node.mts +3 -2
- package/skills/plan-retirement/README.md +5 -2
- package/skills/plan-retirement/SKILL.md +5 -1
- package/skills/plan-retirement/scripts/retire-plans.mts +5 -2
- package/skills/plugin-contract-governance/SKILL.md +7 -1
- package/skills/remediation-governance/SKILL.md +36 -1
- package/skills/resolve-governances/scripts/resolve-governances.mts +5 -2
- package/skills/resolve-tracking/SKILL.md +2 -2
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +5 -4
- package/skills/sdd/SKILL.md +1 -1
- package/skills/spec-format-governance/README.md +1 -1
- package/skills/spec-format-governance/SKILL.md +76 -8
- package/skills/spec-gate/SKILL.md +18 -2
- package/skills/spec-gate/scripts/check-spec-state.mts +47 -11
- package/skills/spec-gate/scripts/check-suite.mts +53 -17
- package/skills/spec-gate/scripts/classify-edit-class.mts +10 -7
- package/skills/spec-producer-governance/README.md +1 -1
- package/skills/spec-producer-governance/SKILL.md +7 -3
- package/skills/ssa-lowering/README.md +3 -5
- package/skills/start-mission/README.md +1 -1
- package/skills/start-mission/SKILL.md +9 -5
- package/skills/suite-format-governance/SKILL.md +43 -4
- package/skills/touch-set-correction/README.md +3 -5
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +7 -5
- package/skills/verify-scenarios/SKILL.md +10 -3
- 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
|
-
- **
|
|
32
|
-
the
|
|
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. **
|
|
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 ===
|
|
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
|
|
11
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
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 ===
|
|
212
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
212
213
|
main(process.argv.slice(2))
|
|
213
214
|
}
|
package/skills/sdd/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
**trigger / inputs / outcome
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
483
|
-
|
|
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
|
|
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.
|
|
635
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
636
|
+
process.exit(main(process.argv.slice(2)))
|
|
637
|
+
}
|