Blast Estimate
The concrete engine for blast-estimate — a read-only derivation that works out how much of the
project a Mission could disturb, instead of trusting the hand-typed guess. Given a Mission's
touch-set (the work areas it names) and the project corpus, it computes a blast level
(low / medium / high) from three measured inputs and lines that level up against the Mission's
hand-typed declared blast (agrees / under-called / over-called). The blast-estimate node
of the SDD project spec (repo-only) carries the full contract and vocabulary.
The three inputs
- count — how many of the touch-set's areas resolve to a known work area in the corpus, plus coverage: whether the touch-set covers every work area of a touched project. Together these are breadth — absolute reach and relative reach.
- centrality (dependency fan-in) — for each resolved area, how many other work areas hold at
least one file that references it, measured across the area's full root set —
implementation files count exactly as spec prose does. The
maxacross the resolved areas is used. See The reference matcher below: a bare id alone is not how real prose references an area. - sensitivity — whether a resolved area is marked in the opt-in
.agents/sdd/sensitive-paths.toml— asensitive = [ "id", ... ]string array, parsed line-anchored and lenient exactly asmanage-spec-anchorsparsesanchors = [ … ], so a leading comment, a trailing comment, or a neighbouring key are all ordinary TOML and parse. A file with nosensitivearray at all is malformed and fails loud. Only an absent file (ENOENT) is benign — no area is sensitive, not an error. Every other read failure fails loud — unparseable, unreadable (permissions), or not a regular file — returningcomputed: nulland anerrornaming the file. The narrow ENOENT test is deliberate: swallowing the rest into "nothing marked" fails in the dangerous direction, silently under-calling blast on exactly the areas a project marked as needing care. An unreadable marking is not evidence of no markings.
The arithmetic is bucketed and deliberately simple (the rubric fixes only the ordering properties, not the exact numbers):
- breadth =
max(countScore, coverageScore)— reach measured two ways, whichever says more:countScore: 1→0, 2-3→1, 4+→3 (absolute reach — touching many areas is broad even in a large project the touch-set nowhere near covers)coverageScore: 3 iff the touch-set covers every work area of a touched project holding ≥2 work areas, else 0 (relative reach — a 3-area project touched entirely is project-wide; the barrier agreement must hold at every project size ≥2, not only at 4+)
- centrality: 0→0, 1-2→1, 3-6→2, 7+→3 (a genuine hub: touching an area a large share of the project leans on is project-scale reach even at count 1). Calibrated against real fan-in, which spans roughly 0–17 on a corpus of this size.
- sensitivity: any marked area →+2
Sum the three, then score>=3 → high, score>=1 → medium, score==0 → low. A touch-set that
resolves to zero known areas (including the empty touch-set) computes unknown — never low.
The ≥2 work areas guard on coverage is load-bearing, and it is keyed per project, not per
corpus: a 1-area project inside a larger multi-project corpus is still never project-wide. The
contract states the precondition itself — the project-wide scenario requires a project holding more
than one work area, and "a lone work area is its whole project but is not project-wide reach" pins
the 1-area answer to low for a lone area with no fan-in and no marking. Coverage is reach
relative to a project, and a project of one has none to cover, so coverage never fires there and
breadth rests on absolute count alone — centrality and sensitivity still score, so a lone area
that is central or marked computes medium or high.
Two exclusions are structural, not just documented: there is no compatibility/breaking-change input at all (no seam for it to enter the score), and centrality is measured fan-in only — a work area's name never enters the computation.
Work-area recovery — declared layouts, never a path shape
Node recovery is not this engine's to invent: it reuses touch-set-correction's pure
fileToNode(path, layouts) over discoverLayouts' declared ProjectLayout[] (the same
cross-skill reuse collision-ladder does). This is load-bearing — a work area spans multiple
declared roots, a spec root and an impl root:
project "sdd" -> roots [".agents/specs/sdd", "plugins/sdd/skills"]
.agents/specs/sdd/mission-graph/README.md -> sdd/mission-graph
plugins/sdd/skills/mission-graph/SKILL.md -> sdd/mission-graph # SAME node, two roots
A node's file set is every file under any of its roots, so fan-in measures the project's real
lean on an area rather than spec-prose cross-reference. estimateBlast takes layouts injected
(fileToNode is pure, so tests construct them as fixtures); the CLI sources them from
discoverLayouts, overridable with --layout.
The reference matcher
Fan-in counts the forms the corpus actually uses to name an area — a bare id is what a touch-set
and the ledger write, not how prose references anything. For node <p>/<cap>:
| form | example |
|---|---|
| bare id | sdd/spec-gate |
| skill-style ref | sdd:spec-gate |
| path under any declared root, at any depth | plugins/sdd/skills/spec-gate/, .agents/specs/sdd/authoring/spec-gate/ |
| relative sibling link (same project only) | ../spec-gate/, ../../authoring/spec-gate/ |
Path forms are derived from the project's declared roots, never hardcoded, so a re-rooted project
keeps working; the any-depth segment matters because a node's spec can sit nested under its root. A
relative link carries no project, so it is only counted within the same project — otherwise two
projects sharing a capability name (manage, design) would cross-credit each other.
This stays mention-based and cheap by design. Real produced/consumed symbol dependency is
ssa-lowering / collision-ladder territory — the boundary is deliberate. Only the resolved areas
are scored, so a one-area touch-set costs one matcher pass over the corpus rather than one per area.
Run it
node "<skill>/scripts/blast-estimate.mts" --root <repo> --touch-set a,b,c \
[--layout 'sdd:.agents/specs/sdd,plugins/sdd/skills']... \
[--declared low|medium|high|unknown] [--format toon|json]
--rootdefaults to.(the repo root the layouts' roots and.agents/sdd/sensitive-paths.tomlresolve against).--layoutis repeatable (<project>:<root1>,<root2>, the same shapetouch-set-correctionaccepts). Omit it to auto-discover viadiscover-specs.--touch-setis a comma-separated list ofproject/capabilityids.--declaredis optional; omitted orunknownlines up asno-declared— the computed level still returns on its own.- Default output is TOON;
--format jsonemits the fullEstimateResult:resolved,unresolved(surfaced, never dropped),computed,reasons(count,maxFanIn,sensitiveAreas,projectWide), andlineUp.
Boundaries
Read-only: it composes a corpus scan and the touch-set it is handed, and returns an estimate — it
writes no file and mutates no store. It does not judge compatibility/breakage (a separate
dimension), rank by surface location, decide HITL vs AFK, render a self-clear-or-escalate verdict (it
only modulates a conductor's judgment), produce a touch-set (it consumes one — pre-work declared,
or touch-set-correction's post-work corrected one), or reimplement node recovery (it imports
fileToNode; it never reinvents or shells out to it).