All posts

Releases

Blast Radius: what else did that change touch?

Shipmoor 0.9.0 adds shipmoor blast, a change-blast-radius query over a committed link graph. It reports which acceptance obligations became suspect because their realizing code moved, and which code became suspect because an obligation moved while the code did not. Every link is authored by a human or minted from causal evidence when an obligation graduates to the deterministic floor. No lexical inference, ever, and the sparse graph that policy produces is reported as a gap rather than filled with a guess.

Blast Radius: what else did that change touch? cover image

A coding agent changes app/login.py. The diff is clean, the tests are green, the review finds nothing. Nobody notices that AC-04 — the acceptance obligation that code was written to satisfy — still describes the old behavior.

Nothing in that story is a bug in the ordinary sense. There is nothing to catch in the change, because the defect is not in the change. It is in the relationship between the change and everything the change was supposed to stay consistent with.

Shipmoor 0.9.0 adds shipmoor blast, which asks one question about a change: what else does it touch?

The problem is that the relationship is not recoverable

A reviewer looking at a two-file diff has no way to know which obligations, which checks, and which doc sections used to line up with those files. That knowledge existed once — at the moment somebody wrote the code to satisfy the promise — and then it evaporated.

Every tool that has tried to recover it after the fact has done so by guessing. Match the obligation’s words against file names. Embed both and compare. Ask a model whether these look related.

We are not doing that, and the reason is worth being blunt about.

No lexical inference, ever

This is the shortest and most important thing about the feature.

Every link is authored by a human, or minted from causal evidence when an obligation graduates to the deterministic floor, or it does not exist. Nothing in Blast Radius compares an obligation’s words to a file’s name. There is no fuzzy matching, no embedding similarity, no “these look related” heuristic anywhere in the graph.

That policy has a visible cost: a repo that has adopted neither authoring nor Claim Check has a sparse graph.

We paid that cost on purpose, and we paid it once already. An earlier attempt in the 0.8.0 line composed evidence by finding lexical bridges between artifacts. It worked beautifully on the case it was built against and did nothing at all on the next one, because the bridge it depended on was an accident of that fixture’s vocabulary. We reverted it and wrote the prohibition into the design as a standing law.

A guessed link is worse than a missing one. It produces a suspect the reader cannot trust and cannot act on, and it teaches people to ignore the output — which costs you every true positive the tool will ever produce. A missing link shows up as a gap, and the gap is the authoring demand.

What a run looks like

$ shipmoor blast
Blast radius  working tree vs HEAD · 2 files changed
  suspect 1 · rerun advised 0 · ungoverned 1 · in sync 0 · silent 0

  suspect       ac-01.realizes.login · AC-01 realizes app/login.py · code moved, obligation unchanged · minted (claim-check graduation)
  ungoverned    config/limits.py · changed with no links

exit 0 · suspects at warn (set blast.suspect_threshold: fail to gate) · report .shipmoor/blast.v1.json

Every stored link is pinned with the content identity of both endpoints. Both match their pins, the link is silent. Exactly one moved, it is suspect, with the stale side named. Both moved in the same change, it reads in sync and the run re-pins automatically.

That is the whole classifier. No ranking, no severity, no confidence score. A suspect is a suspect.

Note the last field on the suspect line. Every rendered line carries its provenance: authored, minted (claim-check graduation), or tag join for a derived edge. A reader should never have to guess what kind of evidence a claim rests on, and a suspect from a hand-authored link is a different claim from one a run minted.

Direction is a property of the diff

The interesting half is the one nobody builds.

Code moved, obligation unchanged is the expected direction: someone changed the code that realizes a promise without touching the promise.

Obligation moved, code unchanged is the other one: someone sharpened what the system promises and nothing downstream moved. Either the code already satisfied the sharper wording, or it does not and nobody noticed.

There is one code path for both. Which direction you see is decided entirely by what the diff touched. There is nothing to configure and no second mechanism to keep in sync.

Identity is content, not meaning

A comment-only edit changes the bytes, so it changes the identity, so it suspects.

That will annoy you the first time. It is also the direct consequence of the law above. The moment a comparator starts deciding which edits really changed the promise, it is making a semantic judgment — and there is no honest way to explain to a reader why one edit counted and another did not.

So the relief valve is not a smarter comparator. It is a human decision, recorded:

$ shipmoor blast --repin ac-01.realizes.login
re-pinned  ac-01.realizes.login · code app/login.py sha256:41c7… (was sha256:12b2…)
recorded in .shipmoor/links.yaml

The re-pin edits the committed store and appends a re-pin event to the entry’s history, so the judgment “this refactor did not change the promise” is itself reviewed in the pull request, with the old and new identity both visible. The alternative — silencing a suspect by editing a hash — would put the most consequential decision in the whole feature outside review.

A --repin of a link that is not suspect is a stated no-op. One whose endpoint was deleted is refused rather than recorded as a hollow success: a deleted endpoint has no identity to pin.

The graph builds itself, if you let it

Link supply grows two ways, and only two ways.

You author them. An entry in .shipmoor/links.yaml naming an obligation and the code paths that realize it. You write the endpoints; the run writes the pins.

# shipmoor links.v1 store. Committed means approved; edits are reviewed in the PR.
links:
  - id: ac-01.realizes.login
    kind: realizes
    item: AC-01
    code:
      - app/login.py
    provenance: authored

Claim Check runs mint them. When an obligation graduates to the deterministic floor — decided satisfied through a bound check — that round’s change surface is causal evidence for which code realizes it, and the run mints a realizes pin labeled minted.

The minting rules are narrow. Post-verdict and floor-only: a judge outcome never mints, and neither does an underspecified obligation. Authored entries outrank minted ones and are never overwritten. And the verdict is unaffected — minting on, off, or failing yields byte-identical verdicts and exit codes, apart from the disclosed notice.

So a repo’s graph is thin on day one and thickens with every round it runs, as a by-product of gating changes you were already gating. That accumulation is the point. The repo gets permanently easier to reason about, and nobody had to schedule a traceability project.

The store is repo state

.shipmoor/links.yaml sits beside the acceptance set and is governed by the same mechanic: committed means approved.

Two consequences, both deliberate. Blast never scaffolds the store — an engine-created store would be an approval nobody made, so a repo with no store gets one honest line naming where links come from. And blast never writes the acceptance file; the obligation side of every link is read-only to the feature.

Gaps: the other half of refusing to guess

Because the tool will not invent a link, it has to be good at telling you where one is missing.

$ shipmoor blast --gaps
Gaps  4 lists · honest buckets disclosed

  changed code with no links      1
    - config/limits.py
  obligations with no check       1
    - AC-01
  obligations with no realizes    0
  documented surface unmatched    not checked (this list arrives with docs verification)

  not checkable: 0

Every list prints its count including zero, because an omitted list and an empty one are indistinguishable to a reader.

The fourth is the exception, and it is the detail I would point at if you only look at one thing. Doc sections are compared at whole-file granularity today; section-level anchors arrive with docs verification. So that list is disclosed as not checked rather than rendered as 0. Printing a zero there would be the single most misleading number the feature could produce, because you would reasonably conclude your documentation is fully governed. An empty list claims the run looked.

Advisory by default

shipmoor blast exits 0 even when it finds suspects.

A feature whose whole premise is a graph you have to build cannot start by failing the builds of every repo that has not built one yet. Adoption has to be able to start with reading the output.

When you want the ratchet, it is one key:

schema_version: 1
blast:
  suspect_threshold: fail

From then on, a pull request that edits an obligation without touching its realizing code fails, with the suspect link as the witness.

Unresolvable links, gaps, and rerun advisories never affect the exit code under any threshold — a repo with one stale path in its store should not fail every build over a link that has nothing to do with the change under review. And an unrecognised threshold value is a loud error rather than a silent fall back to the default. A gate that can be turned off by a typo is not a gate.

What is deferred, and said out loud

Doc sections are whole-file today. The documented-surface gaps list is disclosed rather than counted. Fix-packet actions, verdict integration, and CI and IDE surfaces arrive with the loop composition, where a suspect becomes something your agent is handed rather than something you read.

All of that is stated in the product’s own output, not just here. A tool that hides its own edges has already started lying to you about the easy things.

Try it

Today, shipmoor blast is unlimited during the first 30 days of Pro and in paid Pro. Free includes five Blast Radius invocations per calendar month, independent from the Claim Check, Review, and Test Evidence allowances.

shipmoor blast --gaps .

Start there, on a repo you know well. The gaps list on a codebase you understand is the fastest way to find out whether your acceptance set says what you think it says.

Full documentation: Blast Radius on docs.shipmoor.dev.

0.9.0 also carries a substantial latency program for Code Review and Claim Check. That has its own post.

Contact sales

Our team can help with custom support, team rollouts, and self-hosted deployments. Or to get started now, explore our self-serve plans.