Repository navigation
RootSign v0.3.1 — sync receipts
One feature, one gap closed. v0.3.0's evidence model could say a record was
missing (INCOMPLETE) or altered (TAMPERED). It had no way to say a
record was observed and refused — that outcome lived in terminal
scrollback and vanished with it. A reviewer examining a spool file later could
not tell "never uploaded" from "uploaded and rejected," which are very
different facts in an investigation.
rootsign-admin sync now writes a sync receipt: the chain-of-custody log
for the upload, kept beside the evidence and never written onto it.
Implements ADR-015. Raised by
issue #37 — the project's
first community design proposal.
What you get
spool/receipts/<session_id>.sync.json written on every real attempt
spool/synced/receipts/<session_id>.sync.json follows the file once accepted
Each attempt records where it went, who ran it, the SHA-256 of the spool file
as replayed, and a per-record outcome:
| Bucket | Meaning |
|---|---|
accepted |
the store took it |
duplicate |
the store already had it (DUPLICATE_EVENT) |
rejected |
refused, with error_code, retryable, and the sequence |
not_attempted |
never presented — solicitation had already halted |
Plus halted_at_sequence and a disposition of fully_accepted, partial
(the store refused something — fix it and re-run) or transport_failed (the
network gave up — try again later).
The buckets partition every record in the file, exactly once. A reviewer
can confirm a receipt accounts for the whole exhibit by arithmetic, without
trusting whoever wrote it. The suite asserts it on every receipt it builds.
Receipts are append-only: re-running after a fix adds attempt 2 rather
than overwriting attempt 1, so the history of how the evidence was handled
survives. --dry-run contacts nothing, observes nothing, and writes nothing.
In the evidence bundle
rootsign export of a session that has a receipt now includes
sync-receipt.json, hashed in manifest.json like every other file, and
verification.json gains a replay block summarizing the latest attempt.
A session with no receipt is byte-identical to what v0.3.0 produced — the
replay key is absent, not null. This is additive under bundle v1.0; there is
no format break and no version bump.
Also in this release
-
syncno longer printsINCOMPLETEfor a rejected record. It prints
REJECTED.INCOMPLETEis the verdict word for "a record is missing," and
a record the store refused is the opposite — observed, and declined.
Conflating them on the command that writes the receipt was the exact
confusion ADR-015 exists to remove. stdout wording only; exit codes and the
verdict enum are unchanged. -
Replay records the whole batch's answers. Fail-fast now stops at the
batch boundary rather than the response boundary. The endpoint answers a
batch index-aligned, and spec §7 rule 3 forbids a rejection from aborting
later elements — so records after a refusal are usually accepted.
Previously those answers were discarded, which under-reported what had
actually been delivered. -
Numeric PII is now redacted — behaviour change (#41).
RedactionConfig
tested rules only against values that were alreadystr, so an SSN, account
number, routing number or NPI arriving as a JSON number passed through
unredacted. Because redaction runs before hashing (ADR-006), the raw value
reached both the hash input and the storedinput_redacted. Matching now
tests the value's string form.The pattern still decides, so this is not blanket redaction by key:
{"ssn": 42}survives, and{"count": 123456789}survives. Containers
still recurse; a value whose string form cannot be produced fails closed.Consequence worth noting before you upgrade: a payload carrying numeric
PII now hashes differently than it did under v0.3.0, because the hash is
taken over the redacted payload. Records already stored are unaffected —
this changes what newly recorded actions hash to. If you compare a digest
captured under v0.3.0 against a v0.3.1 re-run of the same payload, expect a
mismatch; that is this fix, not tampering.FinancialPIIConfigandHealthcarePIIConfigare the most affected, since
their patterns (account_number,routing_number,npi, plusssnand
credit_cardfrom the standard set) are pure-digit and so match exactly the
fields most likely to be serialized as numbers.
Not in this release
- No wire change. Receipts are client-side artifacts. The ingest spec is
untouched apart from a note recording thatPREDECESSOR_REJECTEDwas
considered and rejected (see below). - No receipt signing. Receipts are content-hashed and ride under the
bundle manifest. Signing remains ADR-014 Decision 5's deferral. - No cross-implementation fixture vectors. Issue #37's broader ask —
vectors independent implementations can run — is tracked separately. The
schema frozen here is its prerequisite. replay-pendingdoes not write receipts yet. The writer lives in a
shared module so that command can adopt it without duplication.
A design note worth recording
The drafted amendment proposed a PREDECESSOR_REJECTED error code, on the
premise that a store which refused sequence N cannot accept N+1. The spec
says the opposite. §7 rule 3 forbids a rejection from aborting later batch
elements, and §8.3 forbids rejecting a record for a prev_action_hash the
store has never seen — because doing so "would erase the only evidence that
anything was lost."
So no cascade exists for such a code to name, and adding it would have meant
overturning the strongest evidence-preservation rule in the spec. It was
withdrawn. Fail-fast is kept for a different and better reason: ADR-013
Decision 4's resume contract wants delivery to stay a prefix, so the spool
file alone can answer "where does the next run start."
Upgrading
Nothing to do. No configuration, no migration, no schema change. The first
rootsign-admin sync after upgrading starts writing receipts; spool files
written by earlier versions replay exactly as before.