Not yet generally available. Blast Radius ships in Shipmoor CLI 0.9.0, which is still in the release process. This page describes shipped behavior on the release candidate; the command is not on the stable channel yet.
The truth table
Every stored link is compared endpoint by endpoint against its recorded pins.
| Obligation endpoint moved | Code endpoint moved | Resolvable | Classification |
|---|---|---|---|
| no | no | yes | silent |
| no | yes | yes | suspect: code moved, obligation unchanged |
| yes | no | yes | suspect: obligation moved, code unchanged |
| yes | yes | yes | in sync, auto re-pinned, disclosed |
| any | any | no | unresolvable with reason; never suspect; never a threshold input |
That is the whole classifier. There is no ranking, no severity, no confidence score. A suspect is a suspect.
Direction is a property of the diff
The two suspect rows are the same mechanism read in two directions. One code path serves both, and there is no second mechanism and nothing to configure.
Code moved, obligation unchanged. Someone changed the code that realizes a promise without touching the promise. Either the promise is now wrong, or the change did not affect it. Both are worth a human deciding.
Obligation moved, code unchanged. 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.
Which direction you see is decided entirely by what the diff touched.
Reading the screen
$ 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
Headline counts first, then one line per finding, then the exit.
Findings are grouped the way the screen above groups them, and within a group they keep committed store order — the order you wrote them in, not an order the tool chose. Nothing is re-sorted by an implicit notion of importance.
Every line carries its provenance
A reader should never have to guess what kind of evidence a claim rests on.
| Label | Means |
|---|---|
authored | A human wrote this link and committed it. |
minted (claim-check graduation) | A Claim Check run minted it from causal evidence, carrying the mint’s own note. |
tag join | A derived verifies edge, joined on the obligation’s expected check tag. |
This is the trust-through-disclosure ruling applied to every rendered line. A suspect that came from a hand-authored link and a suspect that came from a minted one are different claims, and the screen says which is which.
The counts cannot disagree
--terse condenses the same counts to one line for a CI log, and --json emits the whole report. All three read the same numbers. The renderer holds no logic: every count it prints is the length of a list it was handed.
Re-pinning: the two ways a pin ever moves
Both are visible in the committed diff. There is no third way, and nothing moves a pin silently.
Automatic, when both endpoints moved together
That is the definition of in sync: the promise and the code changed in the same change, so the run re-pins and says so.
No prompt, no flag. If you changed both sides, you have already made the decision the re-pin records. The store write is minimal-diff, touching only that entry’s pins.
Explicit, when a one-sided change did not alter the promise
A pure refactor, say. You accept that in writing:
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. The judgment “this refactor did not change the promise” is itself reviewed in the pull request, with the old and new identity both visible.
That is the point of recording it. The alternative — a suspect you silence by editing a hash — would put the most consequential decision in the whole feature outside review.
--repin is repeatable: one link id per occurrence.
What --repin refuses to do
| Situation | Behavior |
|---|---|
| The link is not currently suspect | A stated no-op. In sync or silent links have nothing to accept. |
| The link’s endpoint was deleted | Refused, not recorded. A deleted endpoint has no identity to pin, and recording a hollow success would be a lie in the store. |
| The link id does not exist | A usage error naming the id. |
Dry-running a re-pin
--no-write makes any re-pin a dry run: the intended change is computed and disclosed, and the file on disk is untouched.
shipmoor blast --repin ac-01.realizes.login --no-write
The same flag suppresses the automatic in-sync re-pin and the report file, so a --no-write run is guaranteed to leave the working tree exactly as it found it.
Unresolvable is not suspect
A link whose endpoint cannot be resolved is not classified. It lands in the disclosed not-checkable bucket carrying its reason, and it never trips the exit-code threshold under any value.
Treating an unknown as a finding would mean a repo with one bad path in its store fails its build for a reason that has nothing to do with the change under review. See Troubleshooting for each reason and its fix.
Next
- Configuration and exit semantics — turning suspects into a gate.
- Gaps — what has no links at all.