ed and strreplace verify a matched span hasn't gone stale before
writing to it, and journal every write so undo can restore it exactly.
Neither of those protects a file from being touched by something else
entirely — a raw str_replace, sed, a hand-edit, or an agent reaching
for a generic editing tool instead of repoman's own. That gap was closed
by chance exactly once: mid-session, a file already under repoman's
journal was edited via a different tool entirely, and the only reason it
didn't ship broken was a human happening to review the diff afterward.
Provenance is the mechanism that catches it reliably, every time, instead
of by luck.
Every repoman-mediated write to a journal-tracked file — ed apply,
sub, append, prepend, insert, undo, and strreplace's own
substitution — records the sha256 of that file's full content at the
moment of the write, in .ed-journal.json's file_provenance map
(path -> {hash, at}). This isn't tied to any one transaction: a
transaction can be evicted from the journal's rolling window while the
file it touched is still current and still worth checking, and undo
(which removes a transaction rather than adding one) still needs
somewhere to record the hash of the content it just restored.
repoman provenance check recomputes each tracked file's current
hash and compares it against what's recorded. A mismatch means the file
changed since repoman last touched it — there is no other way for the
hash to diverge:
$ repoman provenance check
PROVENANCE CHECK OK (14 file(s) tracked)
$ vim docs/TRACKING.md # a hand-edit, outside repoman entirely
$ repoman provenance check
ERROR provenance-mismatch: docs/TRACKING.md changed outside repoman since 2026-09-09T02:54:51Z -- recorded 4bad60b1b60c..., now 26ee0dd9f7c5...
PROVENANCE CHECK FAIL: 1 mismatch(es)
A tracked file that no longer exists at all is reported distinctly, not folded into the same message:
ERROR provenance-missing: gone.txt no longer exists (last known-good hash recorded 2026-09-09T02:54:51Z)
v1 scope, deliberately: journal-tracked files only, no git integration. A file repoman has never written to has no provenance recorded and is never a reason to refuse anything — this check has nothing to compare it against.
check alone only catches drift after the fact, on demand. The write
paths themselves — ed apply, insert, sub, append/prepend,
undo, and strreplace's single read funnel — now refuse to touch a
file with a live provenance mismatch, before anything is written:
$ repoman ed apply sample.txt:0-9:92e78d0b --with "should not land"
REFUSED: sample.txt was edited outside repoman since 2026-09-09T02:54:51Z -- run `repoman provenance check` for details. Nothing written.
This closes a gap apply's own SpanHash staleness check structurally
cannot: SpanHash verifies the matched span's own 64-byte context
window hasn't moved since find — and a fresh find/apply handle
taken on already-tampered content matches that tampered content just
fine, so SpanHash never fires. Provenance checks against the last
repoman-recorded state instead, which is exactly what a freshly-taken
handle can't see. Two write paths had no staleness protection at all
before this: sub's --expect N only ever guarded occurrence count,
never whether the file had changed since anything was recorded; and
append/prepend have no matched-span check by design, since they
target a true file boundary rather than existing text.
Gating every write path on provenance closed the only documented
recovery path along with the mismatch itself — redoing the same edit
through repoman used to re-sync the hash, but that redo is itself a
write, so it was being refused by the very mechanism it was meant to
satisfy. repoman provenance sanction <file> --reason "..." is the
way out: it re-syncs the recorded hash to the file's current
on-disk content directly, without requiring the edit to be redone.
$ repoman provenance sanction docs/TRACKING.md --reason "manual fix during an incident drill, reviewed and accepted"
SANCTIONED: docs/TRACKING.md -- accepted for this regression fixture
$ repoman provenance check
PROVENANCE CHECK OK (14 file(s) tracked)
$ repoman ed apply docs/TRACKING.md:0-40:a1b2c3d4 --with "normal editing again"
applied at docs/TRACKING.md:0: "..." -> "normal editing again"
--reason is mandatory — this is a deliberate override of a safety
check, and the audit trail is the entire point, not an optional
courtesy. sanction refuses in two other cases, both distinctly
worded: a path with no mismatch currently on record (nothing to
sanction — not a silent no-op), and a path that no longer exists
(there's no current content to certify; restore the file first, or
address its absence some other way).
No git integration — provenance only knows what repoman itself wrote,
not what a git checkout or a merge landed. No sanction/override
beyond re-syncing the hash — there's no separate audit log of past
sanctions beyond the single reason recorded alongside the current
hash. relcore wiring is done (T-03): relcore now runs a
provenance pre-flight unconditionally on every release, immediately
alongside badcode, before any release.steps entry, including on
--resume — an unsanctioned out-of-band edit blocks the release the
same way a badcode hit does, with no bypass beyond sanctioning it
first. See repoman relcore --help and this project's own
docs/TRACKING.md (T-03) for the details. Badcode hits themselves also
carry a provenance annotation now (T-04): each match reports whether
the file it was found in currently matches repoman's own journal
record, has never been touched by repoman at all, or was tracked once
and has since diverged — a distinction no adjacent secret-scanning
tool can make, since none of them own the editing layer.