cyber-sdd 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +17 -0
- package/.codex-plugin/plugin.json +17 -0
- package/.plugin/plugin.json +17 -0
- package/README.md +159 -0
- package/agents/sdd-automaton.md +97 -0
- package/agents/sdd-impl-judge.md +214 -0
- package/agents/sdd-scanner.md +120 -0
- package/agents/sdd-spec-judge.md +224 -0
- package/agents/sdd-warden.md +101 -0
- package/package.json +24 -0
- package/skills/align-spec/README.md +20 -0
- package/skills/align-spec/SKILL.md +111 -0
- package/skills/align-spec/scripts/align-spec.mts +187 -0
- package/skills/architect-impl-governance/README.md +46 -0
- package/skills/architect-impl-governance/SKILL.md +45 -0
- package/skills/architect-spec-governance/README.md +48 -0
- package/skills/architect-spec-governance/SKILL.md +59 -0
- package/skills/blast-estimate/README.md +47 -0
- package/skills/blast-estimate/SKILL.md +133 -0
- package/skills/blast-estimate/scripts/blast-estimate.mts +583 -0
- package/skills/builder-impl-governance/README.md +47 -0
- package/skills/builder-impl-governance/SKILL.md +47 -0
- package/skills/builder-spec-governance/README.md +49 -0
- package/skills/builder-spec-governance/SKILL.md +36 -0
- package/skills/check-partition-quality/README.md +22 -0
- package/skills/check-partition-quality/SKILL.md +51 -0
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +336 -0
- package/skills/check-plan-safety/README.md +17 -0
- package/skills/check-plan-safety/SKILL.md +60 -0
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +145 -0
- package/skills/check-project-specs/README.md +19 -0
- package/skills/check-project-specs/SKILL.md +69 -0
- package/skills/check-project-specs/scripts/check-project-specs.mts +217 -0
- package/skills/check-scenario-overlap/README.md +19 -0
- package/skills/check-scenario-overlap/SKILL.md +74 -0
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +249 -0
- package/skills/check-spec-structure/README.md +17 -0
- package/skills/check-spec-structure/SKILL.md +66 -0
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +346 -0
- package/skills/collision-ladder/README.md +18 -0
- package/skills/collision-ladder/SKILL.md +83 -0
- package/skills/collision-ladder/scripts/collision-ladder.mts +657 -0
- package/skills/combat-log-governance/README.md +13 -0
- package/skills/combat-log-governance/SKILL.md +257 -0
- package/skills/concept-index/README.md +13 -0
- package/skills/concept-index/SKILL.md +38 -0
- package/skills/concept-index/scripts/concept-index.mts +245 -0
- package/skills/discover-plans/README.md +16 -0
- package/skills/discover-plans/SKILL.md +74 -0
- package/skills/discover-plans/scripts/discover-plans.mts +212 -0
- package/skills/discover-specs/README.md +15 -0
- package/skills/discover-specs/SKILL.md +76 -0
- package/skills/discover-specs/scripts/discover-specs.mts +396 -0
- package/skills/doctrine-loop/README.md +15 -0
- package/skills/doctrine-loop/SKILL.md +97 -0
- package/skills/formation-loop/README.md +17 -0
- package/skills/formation-loop/SKILL.md +140 -0
- package/skills/gate-validation-governance/README.md +12 -0
- package/skills/gate-validation-governance/SKILL.md +87 -0
- package/skills/impl-producer-governance/README.md +48 -0
- package/skills/impl-producer-governance/SKILL.md +85 -0
- package/skills/init/README.md +27 -0
- package/skills/init/SKILL.md +68 -0
- package/skills/init/scripts/wire-statusline.mts +276 -0
- package/skills/lifecycle-governance/README.md +11 -0
- package/skills/lifecycle-governance/SKILL.md +168 -0
- package/skills/manage/README.md +9 -0
- package/skills/manage/SKILL.md +62 -0
- package/skills/manage-ignore/README.md +19 -0
- package/skills/manage-ignore/SKILL.md +52 -0
- package/skills/manage-ignore/scripts/manage-ignore.mts +294 -0
- package/skills/manage-scenario-bridge/README.md +20 -0
- package/skills/manage-scenario-bridge/SKILL.md +60 -0
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +156 -0
- package/skills/manage-spec-anchors/README.md +18 -0
- package/skills/manage-spec-anchors/SKILL.md +56 -0
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +328 -0
- package/skills/mission-graph/README.md +15 -0
- package/skills/mission-graph/SKILL.md +67 -0
- package/skills/mission-graph/scripts/mission-graph.mts +844 -0
- package/skills/oracle-spec-governance/README.md +45 -0
- package/skills/oracle-spec-governance/SKILL.md +45 -0
- package/skills/ownership-governance/README.md +65 -0
- package/skills/ownership-governance/SKILL.md +104 -0
- package/skills/pause-mission/README.md +18 -0
- package/skills/pause-mission/SKILL.md +112 -0
- package/skills/place-node/README.md +12 -0
- package/skills/place-node/SKILL.md +47 -0
- package/skills/place-node/scripts/place-node.mts +157 -0
- package/skills/plan-retirement/README.md +32 -0
- package/skills/plan-retirement/SKILL.md +90 -0
- package/skills/plan-retirement/scripts/retire-plans.mts +196 -0
- package/skills/plugin-contract-governance/README.md +12 -0
- package/skills/plugin-contract-governance/SKILL.md +112 -0
- package/skills/remediation-governance/README.md +46 -0
- package/skills/remediation-governance/SKILL.md +78 -0
- package/skills/resolve-governances/README.md +18 -0
- package/skills/resolve-governances/SKILL.md +50 -0
- package/skills/resolve-governances/scripts/resolve-governances.mts +515 -0
- package/skills/resolve-tracking/SKILL.md +64 -0
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +213 -0
- package/skills/resume-mission/README.md +12 -0
- package/skills/resume-mission/SKILL.md +53 -0
- package/skills/scaffold-project-spec/README.md +7 -0
- package/skills/scaffold-project-spec/SKILL.md +192 -0
- package/skills/sdd/README.md +7 -0
- package/skills/sdd/SKILL.md +92 -0
- package/skills/solution-producer-governance/README.md +9 -0
- package/skills/solution-producer-governance/SKILL.md +44 -0
- package/skills/spec-format-governance/README.md +73 -0
- package/skills/spec-format-governance/SKILL.md +114 -0
- package/skills/spec-gate/README.md +26 -0
- package/skills/spec-gate/SKILL.md +201 -0
- package/skills/spec-gate/scripts/check-spec-state.mts +601 -0
- package/skills/spec-gate/scripts/check-suite.mts +501 -0
- package/skills/spec-gate/scripts/classify-edit-class.mts +411 -0
- package/skills/spec-producer-governance/README.md +7 -0
- package/skills/spec-producer-governance/SKILL.md +86 -0
- package/skills/spec-structure-governance/README.md +40 -0
- package/skills/spec-structure-governance/SKILL.md +169 -0
- package/skills/ssa-lowering/README.md +26 -0
- package/skills/ssa-lowering/SKILL.md +181 -0
- package/skills/start-mission/README.md +7 -0
- package/skills/start-mission/SKILL.md +115 -0
- package/skills/suite-format-governance/README.md +75 -0
- package/skills/suite-format-governance/SKILL.md +299 -0
- package/skills/suite-format-governance/references/rubric.md +313 -0
- package/skills/touch-set-correction/README.md +16 -0
- package/skills/touch-set-correction/SKILL.md +67 -0
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +418 -0
- package/skills/verify-scenarios/README.md +17 -0
- package/skills/verify-scenarios/SKILL.md +109 -0
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +386 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# builder-spec-governance
|
|
2
|
+
|
|
3
|
+
This is an internal SDD governance about the Builder's bar at the **spec gate**.
|
|
4
|
+
|
|
5
|
+
It answers one question: is this **capability** fully and testably specified? In SDD, a capability's
|
|
6
|
+
contract is written down twice — as a `spec.md` (the decisions it makes, drawn as a **control-flow
|
|
7
|
+
graph**, or CFG) and as a `.feature` suite (one test scenario per branch of that CFG). This bar
|
|
8
|
+
judges whether that contract is complete and checkable *before* anyone builds against it. It judges
|
|
9
|
+
the contract itself, read from the spec plus its suite — not how the document is written, which is a
|
|
10
|
+
different bar
|
|
11
|
+
(`spec-format-governance`).
|
|
12
|
+
|
|
13
|
+
It is one half of a matched pair: this bar asks "is the contract testable and covered?" at the spec
|
|
14
|
+
gate; its sibling **`builder-impl-governance`** asks "does the implementation meet that frozen
|
|
15
|
+
contract?" at the impl gate.
|
|
16
|
+
|
|
17
|
+
## What it requires
|
|
18
|
+
|
|
19
|
+
| Requirement | What it means |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| **Every branch is covered** | Each edge of the capability's CFG has its scenario, and every guard/negative edge is paired with a positive companion. The scenario map is 1:1 in both directions — no orphan scenario, no uncovered edge. |
|
|
22
|
+
| **Every scenario is testable** | Each scenario asserts an observable outcome a check can confirm — a boolean, no "sometimes". A behavior the capability cannot expose cannot be specced. |
|
|
23
|
+
| **A graded subject is still a boolean** | A non-deterministic capability (one whose output varies run to run) still reaches a per-scenario boolean, through a rubric plus a threshold over N runs. The rubric form stays out of the boolean `.feature`, carried as a judge-only `@rubric` scenario. |
|
|
24
|
+
|
|
25
|
+
This is the SDD default for the `builder` spec bar; a plugin may bind its own per artifact-type, and
|
|
26
|
+
this one loads when the registry leaves `builder`/`spec` unbound.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
One merged bar loaded by both faces at the **spec gate**:
|
|
31
|
+
|
|
32
|
+
- **spec-producer:** self-aligns to it while writing the testable suite
|
|
33
|
+
- **cold spec-judge:** grades coverage and testability against it
|
|
34
|
+
|
|
35
|
+
`producer ≠ judge` holds at the agent level — the same bar, two independent readers.
|
|
36
|
+
|
|
37
|
+
## Related governances
|
|
38
|
+
|
|
39
|
+
This bar owns the testability and coverage of the capability's contract. Its neighbors own
|
|
40
|
+
everything around that:
|
|
41
|
+
|
|
42
|
+
- **`builder-impl-governance`** — the other half of the pair: whether the *implementation* meets the
|
|
43
|
+
frozen contract, judged at the impl gate. This bar freezes what must hold; that one verifies it held.
|
|
44
|
+
- **`spec-format-governance`** — the document's prose and section layout. That bar reads the
|
|
45
|
+
`spec.md` as a document; this bar reads the spec + suite as a contract.
|
|
46
|
+
- **`suite-format-governance`** — how the `.feature` suite itself is written, including the 1:1
|
|
47
|
+
scenario-map rule this bar enforces coverage against.
|
|
48
|
+
|
|
49
|
+
Internal SDD governance (`user-invocable: false`). Not triggered by users directly.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: builder-spec-governance
|
|
3
|
+
description: "Partial Skill: invoke by name only"
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
actor: builder
|
|
7
|
+
gate: spec
|
|
8
|
+
compose: union
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Builder-Spec Governance — the testability & coverage bar
|
|
12
|
+
|
|
13
|
+
The **Builder** bar at the **spec gate**: is this **capability** fully and testably specified? Judges
|
|
14
|
+
the capability's contract (read from its spec + suite), not the document's prose — that is
|
|
15
|
+
`sdd:spec-format-governance`. Loaded by both faces. The SDD default for the `builder` spec bar; a
|
|
16
|
+
plugin may bind its own, and this loads when the registry leaves `builder`/`spec` unbound.
|
|
17
|
+
|
|
18
|
+
## The bar
|
|
19
|
+
|
|
20
|
+
- **Every branch of the capability is covered.** Each edge of its control-flow graph (CFG) has its
|
|
21
|
+
scenario, and every guard/negative edge is paired with a positive companion. The **scenario map
|
|
22
|
+
is 1:1** — no orphan scenario, no uncovered edge (`sdd:suite-format-governance`).
|
|
23
|
+
- **Every scenario is testable.** Each asserts an observable outcome a check can confirm — a boolean,
|
|
24
|
+
no "sometimes". A behavior the capability cannot expose cannot be specced.
|
|
25
|
+
- **A graded subject is still a boolean.** For a non-deterministic capability the contract reaches a
|
|
26
|
+
per-scenario boolean through a rubric + threshold over N runs; the rubric form stays out of the
|
|
27
|
+
boolean `.feature`, carried as a judge-only `@rubric` scenario.
|
|
28
|
+
|
|
29
|
+
## Key points (read-check)
|
|
30
|
+
|
|
31
|
+
1. **Every branch of the capability is covered** — every edge has its scenario, guards paired with
|
|
32
|
+
positives, the scenario map 1:1.
|
|
33
|
+
2. **Every scenario is testable** — an observable boolean outcome; behavior the capability cannot
|
|
34
|
+
expose cannot be specced.
|
|
35
|
+
3. **A graded subject still reaches a per-scenario boolean** via rubric + threshold; the rubric stays
|
|
36
|
+
out of the `.feature`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# check-partition-quality
|
|
2
|
+
|
|
3
|
+
Internal SDD engine (`user-invocable: false`). Measures from a project's **git history** how much
|
|
4
|
+
parallel work its layout permits, and compares candidate layouts on the same evidence.
|
|
5
|
+
|
|
6
|
+
**Why it exists.** SDD's scheduler cuts one mission per spec-node, so a layout that scatters a
|
|
7
|
+
capability makes ordinary changes collide and the schedule serializes. That was long asserted rather
|
|
8
|
+
than measured; this produces the number, per project, so a layout decision rests on the project's own
|
|
9
|
+
history instead of doctrine.
|
|
10
|
+
|
|
11
|
+
**The headline is the parallelizable share** — how often two changes from history could run in
|
|
12
|
+
parallel. Every run also reports a shuffled control, because a partition that cannot beat chance
|
|
13
|
+
explains nothing.
|
|
14
|
+
|
|
15
|
+
**One design note worth keeping.** Two more obvious metrics — within-node co-change ratio, and mean
|
|
16
|
+
nodes touched per change — were tried first and **both preferred a layered layout**, because both
|
|
17
|
+
reward a coarser partition for being coarse. They are still printed, labelled as confounded, so the
|
|
18
|
+
mistake is visible rather than repeatable.
|
|
19
|
+
|
|
20
|
+
Runnable standalone; opt-in from `scaffold-project-spec` (detection mode, to inform the strategy
|
|
21
|
+
choice) and the formation loop (the layout-quality signal). Never a gate. Not triggered by users
|
|
22
|
+
directly.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: check-partition-quality
|
|
3
|
+
description: "Partial Skill: invoke by name only — project-spec/partition-quality's engine: measures from git history how much parallel work a layout permits, and compares candidate layouts. Opt-in from scaffold-project-spec and the formation loop, and runnable standalone; not triggered by users directly."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
internal: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Check Partition Quality
|
|
10
|
+
|
|
11
|
+
Measures, from a project's **own git history**, how much parallel work its layout actually permits —
|
|
12
|
+
and compares candidate layouts on the same evidence. The mission scheduler cuts **one mission per
|
|
13
|
+
spec-node**, so two changes touching a shared node must serialize; this reports how often that
|
|
14
|
+
happens.
|
|
15
|
+
|
|
16
|
+
Read-only and advisory: it moves no file, writes nothing, and renders **no verdict**. Layout is the
|
|
17
|
+
owner's call (`sdd:spec-structure-governance` — adoption over purity).
|
|
18
|
+
|
|
19
|
+
## Run it
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node "<skill>/scripts/check-partition-quality.mts" \
|
|
23
|
+
--repo <path> --scope <dir> [--partition <name>]... [--floor N] [--limit N] [--format json]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `--scope` — restrict to a subtree (e.g. `src`, `plugins/sdd/skills`). Files outside it are ignored.
|
|
27
|
+
- `--partition` — repeatable; compare candidates on the same commits. Built in:
|
|
28
|
+
`top-folder` (the capability cut) · `second-folder` (capability/unit) · `role` (artifact role — the
|
|
29
|
+
layered analogue, useful as a contrast) · `single` (the degenerate floor: no parallel work).
|
|
30
|
+
Default compares `top-folder` against `role`.
|
|
31
|
+
- `--floor` — minimum usable multi-file commits before a rate is emitted (default 20).
|
|
32
|
+
|
|
33
|
+
## Reading the output
|
|
34
|
+
|
|
35
|
+
**The headline is the parallelizable share** — of two changes drawn from history, how often they
|
|
36
|
+
*could* run in parallel. Higher is better.
|
|
37
|
+
|
|
38
|
+
**Check the control before believing it.** Every run reports the same metric over a **shuffled**
|
|
39
|
+
partition of identical node sizes. A partition that does not beat its shuffle explains nothing, and
|
|
40
|
+
the run says so.
|
|
41
|
+
|
|
42
|
+
**The diagnostics are confounded — never headline them.** `within-node co-change` and `mean nodes
|
|
43
|
+
touched` both reward a **coarser** partition for being coarse: a single-node partition scores a
|
|
44
|
+
*perfect* 1 on each while permitting zero parallel work. They are printed only so a reader can see
|
|
45
|
+
why they are not used. Two engines built on them would recommend a layered layout.
|
|
46
|
+
|
|
47
|
+
## Boundaries
|
|
48
|
+
|
|
49
|
+
Measurement only — no file is moved, no spec written, no layout approved or rejected. Thin history is
|
|
50
|
+
**reported, never scored**: below the floor it emits no rate rather than a confident number drawn
|
|
51
|
+
from noise. It reads `git log` and nothing else; no node body, no spec frontmatter.
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-partition-quality — project-spec/partition-quality's concrete engine. Measures, from a
|
|
3
|
+
// project's own git history, how much parallel work its layout permits, and compares candidate
|
|
4
|
+
// layouts on the same evidence (see this skill's README.md).
|
|
5
|
+
//
|
|
6
|
+
// THE METRIC IS COLLISION RATE, AND THAT CHOICE IS LOAD-BEARING. The scheduler cuts one mission per
|
|
7
|
+
// spec-node, so two changes touching a shared node must serialize. Collision rate is the share of
|
|
8
|
+
// change pairs that do. The headline is its complement — the parallelizable share.
|
|
9
|
+
//
|
|
10
|
+
// Two obvious alternatives were tried first and BOTH preferred a layered partition (the wrong
|
|
11
|
+
// answer), because both are confounded by node count:
|
|
12
|
+
// - within-node co-change ratio — a coarser partition scores well for being coarse
|
|
13
|
+
// - mean nodes touched per change — a single-node partition scores PERFECTLY while permitting
|
|
14
|
+
// zero parallel work
|
|
15
|
+
// They are computed and reported here as DIAGNOSTICS, explicitly labelled, so that a later reader
|
|
16
|
+
// does not "simplify" the engine back onto one of them. Do not headline them.
|
|
17
|
+
//
|
|
18
|
+
// Read-only: it runs `git log`, writes nothing, and renders no verdict — layout is the owner's call.
|
|
19
|
+
// No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions are exported for
|
|
20
|
+
// node:test; running the file directly drives the CLI.
|
|
21
|
+
|
|
22
|
+
import { execFileSync } from 'node:child_process'
|
|
23
|
+
|
|
24
|
+
/** Below this many usable multi-file commits, a rate would be noise — report, never score. */
|
|
25
|
+
export const DEFAULT_FLOOR = 20
|
|
26
|
+
|
|
27
|
+
export interface Change {
|
|
28
|
+
/** In-scope files touched by one commit. */
|
|
29
|
+
files: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A partition names the node each file belongs to; `undefined` drops the file from the measure. */
|
|
33
|
+
export type Partition = (file: string) => string | undefined
|
|
34
|
+
|
|
35
|
+
export interface Measurement {
|
|
36
|
+
nodes: number
|
|
37
|
+
pairs: number
|
|
38
|
+
collisionRate: number
|
|
39
|
+
parallelizableShare: number
|
|
40
|
+
control: number
|
|
41
|
+
/** Positive when the partition explains more than a shuffle of the same node sizes. */
|
|
42
|
+
marginOverControl: number
|
|
43
|
+
explainsNothing: boolean
|
|
44
|
+
diagnostics: {
|
|
45
|
+
/** CONFOUNDED by node count — a coarser partition scores higher. Never headline. */
|
|
46
|
+
withinNodeCoChangeRatio: number
|
|
47
|
+
/** CONFOUNDED by node count — a single-node partition scores a perfect 1. Never headline. */
|
|
48
|
+
meanNodesTouched: number
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ThinHistory {
|
|
53
|
+
thin: true
|
|
54
|
+
usableCommits: number
|
|
55
|
+
floor: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Reading history ──────────────────────────────────────────────────────────
|
|
59
|
+
// Only multi-file changes inform the measure: a single-file commit contributes no pair, because one
|
|
60
|
+
// file can collide with nothing.
|
|
61
|
+
|
|
62
|
+
export function parseGitLog(stdout: string, inScope: (f: string) => boolean): Change[] {
|
|
63
|
+
const out: Change[] = []
|
|
64
|
+
let cur: string[] | null = null
|
|
65
|
+
for (const line of stdout.split('\n')) {
|
|
66
|
+
const t = line.trimEnd()
|
|
67
|
+
if (t === '') {
|
|
68
|
+
if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] })
|
|
69
|
+
cur = null
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (/^[0-9a-f]{40}$/.test(t)) {
|
|
73
|
+
if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] })
|
|
74
|
+
cur = []
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (cur !== null && inScope(t)) cur.push(t)
|
|
78
|
+
}
|
|
79
|
+
if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] })
|
|
80
|
+
return out.filter((c) => c.files.length > 1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readHistory(repo: string, inScope: (f: string) => boolean, limit = 4000): Change[] {
|
|
84
|
+
const stdout = execFileSync('git', ['-C', repo, 'log', `-${limit}`, '--name-only', '--pretty=format:%H'], {
|
|
85
|
+
encoding: 'utf8',
|
|
86
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
87
|
+
})
|
|
88
|
+
return parseGitLog(stdout, inScope)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── The metric ───────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** The set of nodes one change touches. A change touching none drops out of the measure. */
|
|
94
|
+
function nodesOf(c: Change, p: Partition): Set<string> {
|
|
95
|
+
const s = new Set<string>()
|
|
96
|
+
for (const f of c.files) {
|
|
97
|
+
const n = p(f)
|
|
98
|
+
if (n !== undefined) s.add(n)
|
|
99
|
+
}
|
|
100
|
+
return s
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function collisionRate(changes: Change[], p: Partition): { rate: number; pairs: number } {
|
|
104
|
+
const sets = changes.map((c) => nodesOf(c, p)).filter((s) => s.size > 0)
|
|
105
|
+
let pairs = 0
|
|
106
|
+
let collided = 0
|
|
107
|
+
for (let i = 0; i < sets.length; i++) {
|
|
108
|
+
for (let j = i + 1; j < sets.length; j++) {
|
|
109
|
+
pairs++
|
|
110
|
+
const a = sets[i] as Set<string>
|
|
111
|
+
const b = sets[j] as Set<string>
|
|
112
|
+
for (const n of a) {
|
|
113
|
+
if (b.has(n)) {
|
|
114
|
+
collided++
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { rate: pairs === 0 ? 0 : collided / pairs, pairs }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── The control ──────────────────────────────────────────────────────────────
|
|
124
|
+
// The same measurement over a SHUFFLED partition of identical node sizes. A partition whose rate
|
|
125
|
+
// matches its shuffle explains nothing, and the headline should not be trusted. Deterministic seed:
|
|
126
|
+
// the run must be reproducible.
|
|
127
|
+
|
|
128
|
+
function mulberry32(seed: number): () => number {
|
|
129
|
+
let a = seed >>> 0
|
|
130
|
+
return () => {
|
|
131
|
+
a = (a + 0x6d2b79f5) >>> 0
|
|
132
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
|
133
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
134
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function shuffledControl(changes: Change[], p: Partition, seed = 7): number {
|
|
139
|
+
const files = [...new Set(changes.flatMap((c) => c.files))].filter((f) => p(f) !== undefined).sort()
|
|
140
|
+
const labels = files.map((f) => p(f) as string)
|
|
141
|
+
const rnd = mulberry32(seed)
|
|
142
|
+
for (let i = labels.length - 1; i > 0; i--) {
|
|
143
|
+
const j = Math.floor(rnd() * (i + 1))
|
|
144
|
+
const li = labels[i] as string
|
|
145
|
+
const lj = labels[j] as string
|
|
146
|
+
labels[i] = lj
|
|
147
|
+
labels[j] = li
|
|
148
|
+
}
|
|
149
|
+
const m = new Map(files.map((f, i) => [f, labels[i] as string]))
|
|
150
|
+
return collisionRate(changes, (f) => m.get(f)).rate
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Rename `diagnostics` to `confoundedDiagnostics` on the way out to JSON.
|
|
155
|
+
*
|
|
156
|
+
* The text render carries the warning inline; a JSON consumer sees only keys, so a plain
|
|
157
|
+
* `diagnostics` object hands over two numbers indistinguishable from the headline. Renaming the key
|
|
158
|
+
* beats adding a note field: a consumer cannot read these numbers without typing the word
|
|
159
|
+
* "confounded", whereas a sibling note is trivially skipped.
|
|
160
|
+
*/
|
|
161
|
+
export function toJson([name, m]: [string, Measurement | ThinHistory]): [string, unknown] {
|
|
162
|
+
if (!('diagnostics' in m)) return [name, m]
|
|
163
|
+
const { diagnostics, ...rest } = m
|
|
164
|
+
return [name, { ...rest, confoundedDiagnostics: diagnostics }]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Confounded diagnostics — labelled, never the headline ────────────────────
|
|
168
|
+
|
|
169
|
+
export function withinNodeCoChangeRatio(changes: Change[], p: Partition): number {
|
|
170
|
+
let within = 0
|
|
171
|
+
let total = 0
|
|
172
|
+
for (const c of changes) {
|
|
173
|
+
const ns = c.files.map((f) => p(f)).filter((n): n is string => n !== undefined)
|
|
174
|
+
for (let i = 0; i < ns.length; i++) {
|
|
175
|
+
for (let j = i + 1; j < ns.length; j++) {
|
|
176
|
+
total++
|
|
177
|
+
if (ns[i] === ns[j]) within++
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return total === 0 ? 0 : within / total
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function meanNodesTouched(changes: Change[], p: Partition): number {
|
|
185
|
+
const counts = changes.map((c) => nodesOf(c, p).size).filter((n) => n > 0)
|
|
186
|
+
return counts.length === 0 ? 0 : counts.reduce((a, b) => a + b, 0) / counts.length
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Measure ──────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export function measure(changes: Change[], p: Partition, floor = DEFAULT_FLOOR): Measurement | ThinHistory {
|
|
192
|
+
const usable = changes.filter((c) => nodesOf(c, p).size > 0)
|
|
193
|
+
if (usable.length < floor) return { thin: true, usableCommits: usable.length, floor }
|
|
194
|
+
const { rate, pairs } = collisionRate(usable, p)
|
|
195
|
+
const control = shuffledControl(usable, p)
|
|
196
|
+
const margin = control - rate // lower collision than the shuffle is the improvement
|
|
197
|
+
return {
|
|
198
|
+
nodes: new Set(usable.flatMap((c) => [...nodesOf(c, p)])).size,
|
|
199
|
+
pairs,
|
|
200
|
+
collisionRate: rate,
|
|
201
|
+
parallelizableShare: 1 - rate,
|
|
202
|
+
control,
|
|
203
|
+
marginOverControl: margin,
|
|
204
|
+
explainsNothing: margin <= 0,
|
|
205
|
+
diagnostics: {
|
|
206
|
+
withinNodeCoChangeRatio: withinNodeCoChangeRatio(usable, p),
|
|
207
|
+
meanNodesTouched: meanNodesTouched(usable, p),
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function isThin(m: Measurement | ThinHistory): m is ThinHistory {
|
|
213
|
+
return (m as ThinHistory).thin === true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Built-in candidate partitions ────────────────────────────────────────────
|
|
217
|
+
// Named so a caller can compare "what the tree does now" against "what a capability cut would do"
|
|
218
|
+
// without writing code.
|
|
219
|
+
|
|
220
|
+
export const PARTITIONS: Record<string, (scope: string) => Partition> = {
|
|
221
|
+
/** Top-level folder under the scope — the capability cut for a screaming layout. */
|
|
222
|
+
'top-folder': (scope) => (f) => {
|
|
223
|
+
const rel = f.startsWith(scope) ? f.slice(scope.length).replace(/^\//, '') : undefined
|
|
224
|
+
if (rel === undefined) return undefined
|
|
225
|
+
const seg = rel.split('/')[0]
|
|
226
|
+
return seg === undefined || seg === '' || !rel.includes('/') ? undefined : seg
|
|
227
|
+
},
|
|
228
|
+
/** Second-level folder — a finer cut, for a capability/unit tree. */
|
|
229
|
+
'second-folder': (scope) => (f) => {
|
|
230
|
+
const rel = f.startsWith(scope) ? f.slice(scope.length).replace(/^\//, '') : undefined
|
|
231
|
+
if (rel === undefined) return undefined
|
|
232
|
+
const p = rel.split('/')
|
|
233
|
+
return p.length > 2 ? `${p[0]}/${p[1]}` : undefined
|
|
234
|
+
},
|
|
235
|
+
/** File extension / artifact role — the layered analogue, useful as a contrast candidate. */
|
|
236
|
+
role: (scope) => (f) => {
|
|
237
|
+
if (!f.startsWith(scope)) return undefined
|
|
238
|
+
const n = f.split('/').pop() ?? ''
|
|
239
|
+
const dot = n.indexOf('.')
|
|
240
|
+
return dot === -1 ? n : n.slice(dot + 1)
|
|
241
|
+
},
|
|
242
|
+
/** Everything in one node — the degenerate floor: no parallel work is possible. */
|
|
243
|
+
single: (scope) => (f) => (f.startsWith(scope) ? 'all' : undefined),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Render ───────────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
function pct(n: number): string {
|
|
249
|
+
return `${(n * 100).toFixed(1)}%`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function renderOne(label: string, m: Measurement | ThinHistory): string {
|
|
253
|
+
if (isThin(m)) {
|
|
254
|
+
return ` ${label}: history too thin to measure — ${m.usableCommits} usable multi-file commits, floor ${m.floor}. No rate emitted.`
|
|
255
|
+
}
|
|
256
|
+
const lines = [
|
|
257
|
+
` ${label}`,
|
|
258
|
+
` parallelizable share : ${pct(m.parallelizableShare)} ← the headline`,
|
|
259
|
+
` collision rate : ${pct(m.collisionRate)} over ${m.pairs} change pairs, ${m.nodes} nodes`,
|
|
260
|
+
` shuffled control : ${pct(m.control)} (margin ${m.marginOverControl >= 0 ? '+' : ''}${pct(m.marginOverControl)})`,
|
|
261
|
+
]
|
|
262
|
+
if (m.collisionRate >= 1) {
|
|
263
|
+
lines.push(' ⚠ every change pair collides — this partition permits no parallel work')
|
|
264
|
+
}
|
|
265
|
+
if (m.explainsNothing) {
|
|
266
|
+
lines.push(' ⚠ this partition explains no more than chance — do not trust the headline')
|
|
267
|
+
}
|
|
268
|
+
lines.push(
|
|
269
|
+
` diagnostics (CONFOUNDED by node count — not the headline): within-node co-change ${pct(m.diagnostics.withinNodeCoChangeRatio)}, mean nodes touched ${m.diagnostics.meanNodesTouched.toFixed(2)}`,
|
|
270
|
+
)
|
|
271
|
+
return lines.join('\n')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function render(results: [string, Measurement | ThinHistory][], repo: string, scope: string): string {
|
|
275
|
+
const out = [`check-partition-quality: repo=${repo} scope=${scope || '(whole repo)'}`]
|
|
276
|
+
for (const [label, m] of results) out.push(renderOne(label, m))
|
|
277
|
+
if (results.length > 1) {
|
|
278
|
+
const scored = results.filter(([, m]) => !isThin(m)) as [string, Measurement][]
|
|
279
|
+
if (scored.length > 1) {
|
|
280
|
+
const best = scored.reduce((a, b) => (b[1].parallelizableShare > a[1].parallelizableShare ? b : a))
|
|
281
|
+
out.push(`\nOn this history, "${best[0]}" permits the most parallel work (${pct(best[1].parallelizableShare)}).`)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
out.push('note: a measurement, not a verdict — it moves no file and gates nothing; layout is the owner’s call')
|
|
285
|
+
return out.join('\n')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The effects `main` reaches the outside world through. Dependencies enter at the CLI boundary and
|
|
292
|
+
* nowhere below it: everything under `main` is pure over the history it is handed.
|
|
293
|
+
*/
|
|
294
|
+
export type Context = { readHistory: typeof readHistory }
|
|
295
|
+
|
|
296
|
+
export function main(argv: string[], context: Context = { readHistory }): number {
|
|
297
|
+
let repo = '.'
|
|
298
|
+
let scope = ''
|
|
299
|
+
let floor = DEFAULT_FLOOR
|
|
300
|
+
let limit = 4000
|
|
301
|
+
let format: 'text' | 'json' = 'text'
|
|
302
|
+
const candidates: string[] = []
|
|
303
|
+
for (let i = 0; i < argv.length; i++) {
|
|
304
|
+
const a = argv[i]
|
|
305
|
+
if (a === '--repo') repo = argv[++i] ?? '.'
|
|
306
|
+
else if (a === '--scope') scope = argv[++i] ?? ''
|
|
307
|
+
else if (a === '--floor') floor = Number(argv[++i] ?? DEFAULT_FLOOR)
|
|
308
|
+
else if (a === '--limit') limit = Number(argv[++i] ?? 4000)
|
|
309
|
+
else if (a === '--format') format = (argv[++i] as 'text' | 'json') ?? 'text'
|
|
310
|
+
else if (a === '--partition') candidates.push(argv[++i] ?? '')
|
|
311
|
+
}
|
|
312
|
+
if (candidates.length === 0) candidates.push('top-folder', 'role')
|
|
313
|
+
for (const c of candidates) {
|
|
314
|
+
if (!(c in PARTITIONS)) {
|
|
315
|
+
process.stderr.write(`unknown --partition "${c}" — known: ${Object.keys(PARTITIONS).join(', ')}\n`)
|
|
316
|
+
return 1
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Read ONCE, then measure every candidate over that one history — the comparison is only
|
|
320
|
+
// meaningful if each candidate is scored against the same commits and the same scope.
|
|
321
|
+
const changes = context.readHistory(repo, (f) => (scope ? f.startsWith(scope) : true), limit)
|
|
322
|
+
const results = candidates.map(
|
|
323
|
+
(c) =>
|
|
324
|
+
[c, measure(changes, (PARTITIONS[c] as (s: string) => Partition)(scope), floor)] as [
|
|
325
|
+
string,
|
|
326
|
+
Measurement | ThinHistory,
|
|
327
|
+
],
|
|
328
|
+
)
|
|
329
|
+
if (format === 'json') process.stdout.write(`${JSON.stringify(Object.fromEntries(results.map(toJson)), null, 2)}\n`)
|
|
330
|
+
else process.stdout.write(`${render(results, repo, scope)}\n`)
|
|
331
|
+
return 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
335
|
+
process.exit(main(process.argv.slice(2)))
|
|
336
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# check-plan-safety
|
|
2
|
+
|
|
3
|
+
Internal SDD skill — the concrete guard engine for the **plan brief's safe-to-publish floor**. Scans
|
|
4
|
+
the tracked, portable handoff artifacts under `.agents/plans` (the `*.plan.md` brief + sibling design
|
|
5
|
+
docs) for machine-local references that must never enter git history.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node scripts/check-plan-safety.mts --root . # audit (TOON finding set)
|
|
9
|
+
node scripts/check-plan-safety.mts --root . --check # CI guard (fails on any leak)
|
|
10
|
+
node scripts/check-plan-safety.mts --path <file> --check # check one explicit file
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Flags a **home-directory absolute path** (`/home/<user>/…`, `/Users/<user>/…`, `C:\Users\<user>\…`)
|
|
14
|
+
or a **`$HOME`/`$USER` expansion** — each leaks an OS username and breaks portability. A bare `~/` is
|
|
15
|
+
deliberately not flagged (no username; legitimate in design prose). The plan-brief analog of the
|
|
16
|
+
combat log's floor. Read-only; writes nothing. See [`SKILL.md`](./SKILL.md) for the full contract.
|
|
17
|
+
Not user-invocable.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: check-plan-safety
|
|
3
|
+
description: "Partial Skill: invoke by name only — plan-brief/check-plan-safety's guard engine against machine-local path leaks in tracked plan artifacts — the CI guard, not triggered by users directly."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
metadata:
|
|
6
|
+
internal: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Check Plan Safety
|
|
10
|
+
|
|
11
|
+
The concrete engine for the **plan brief's safe-to-publish floor**. The plan brief
|
|
12
|
+
(`.agents/plans/<cr-ref>.plan.md`) and its sibling design docs are **tracked, portable** handoff
|
|
13
|
+
artifacts — committed at handoff and read by any later session, agent, or checkout. This engine
|
|
14
|
+
guards that floor mechanically: it scans those `*.md` files for **machine-local references** that
|
|
15
|
+
must never enter git history. It carries a self-contained `.mts` script (the repo's node-≥23.6 /
|
|
16
|
+
no-deps convention), and is the plan-brief analog of the combat log's safe-to-publish floor
|
|
17
|
+
(`combat-log-governance`) — the same "never committed: absolute paths, OS usernames" rule, extended
|
|
18
|
+
from the ledger to the brief.
|
|
19
|
+
|
|
20
|
+
## What it flags
|
|
21
|
+
|
|
22
|
+
- **home-abs-path** — a home-directory absolute path: `/home/<user>/…`, `/Users/<user>/…`, or a
|
|
23
|
+
Windows profile `C:\Users\<user>\…`. Leaks the OS username (privacy) **and** resolves on no other
|
|
24
|
+
checkout (portability).
|
|
25
|
+
- **env-home** — a shell expansion of the home dir: `$HOME`, `${HOME}`, `%USERPROFILE%`, `%HOMEPATH%`.
|
|
26
|
+
- **env-user** — a shell expansion of the identity: `$USER`, `${USER}`, `%USERNAME%`.
|
|
27
|
+
|
|
28
|
+
Every finding is **blocking** — a leak is a leak. The fix is to bring the referenced content into
|
|
29
|
+
the repo and reference it **repo-relative** (e.g. copy a `~/.claude/plans/` design doc to
|
|
30
|
+
`.agents/plans/<cr-ref>.design.md`), never to link a machine-local path.
|
|
31
|
+
|
|
32
|
+
### What it deliberately does NOT flag
|
|
33
|
+
|
|
34
|
+
A bare `~/` is **not** a leak — it carries no username and legitimately appears in design prose
|
|
35
|
+
describing home-rooted feature paths (a tool's own `~/.<tool>/` data root). `$HOMEBREW` / `$USERDATA`
|
|
36
|
+
(a different variable that merely starts with `HOME`/`USER`) are not flagged either.
|
|
37
|
+
|
|
38
|
+
## Run the scan
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node "<skill>/scripts/check-plan-safety.mts" [--root .] [--path <file>]... [--check] [--format toon|json]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Default `--root` is the current directory; default `--format` is **TOON** (the token-efficient
|
|
45
|
+
tabular form). Absent `--path`, it scans every `*.md` under `<root>/.agents/plans`.
|
|
46
|
+
- **`--path <file>`** (repeatable) scans an explicit file set instead of the plan dir — used by
|
|
47
|
+
`pause-mission` to check the one brief it is about to commit.
|
|
48
|
+
- **Audit mode** (default) emits the finding set (`file, line, kind, token`) and exits **0**.
|
|
49
|
+
- **`--check`** (CI guard) exits **non-zero** iff any leak is found and **writes nothing** else.
|
|
50
|
+
Wired into `verify:specs` (`node …/check-plan-safety.mts --root . --check`).
|
|
51
|
+
|
|
52
|
+
When `node` is absent, an agent performs the same derivation by hand: grep the plan `*.md` files for
|
|
53
|
+
`/home/`, `/Users/`, `C:\Users\`, `$HOME`, and `$USER`.
|
|
54
|
+
|
|
55
|
+
## Boundaries
|
|
56
|
+
|
|
57
|
+
Read-only — it writes nothing and acts on no finding. It does not fix a leak (the author scrubs it),
|
|
58
|
+
does not check gate legality (`spec-gate`), and does not audit node-shape (`check-spec-structure`).
|
|
59
|
+
It is loaded by the `manage` gateway (Audit & align) for an on-demand scan and by `pause-mission`
|
|
60
|
+
before a checkpoint commit.
|