stage-receipt/0.0-preview, beside its Internet-Draft
(draft-saha-stage-receipts-00, posted 8 September 2026 — a draft, not a standard). Nothing here is a released
implementation or a stable schema. It is published now so that the sample records and the
reference verifier that accompany it can be checked by anyone, today, with standard tools and
no trust in the publisher. The reference pipeline that emits these records is in preparation;
the verifier ships open.
One record per stage. Digests over exact bytes. A chain a stranger can recompute. The format behind the ten-record diagram, set out for someone who has to implement it — or check it — without asking us anything.
A digest is only meaningful if the byte recipe is stated. "The hash of the document" is not a claim. "SHA-256 over the stored bytes of artifact X, 1,482,331 bytes" is. Every recipe on this page therefore answers the same three questions: which bytes, produced how, hashed with what.
The algorithm is SHA-256 throughout — one algorithm per record set, named once in the
format field, because per-record algorithm choice is an invitation to downgrade games. A digest
is written as sha256: followed by 64 lowercase hex characters.
The store-then-digest law. Normalise once, at write time — then digest exactly what you stored, and never re-normalise on read. If a verifier has to "prepare" bytes before hashing them, the preparation is part of the instrument and must be pinned like one.
A stage record is a JSON object with thirteen required top-level fields. A verifier fails a record that lacks any of them. Nothing is inferred and nothing is defaulted: an absent field is absent.
| Field | What it must contain |
|---|---|
format | The format identifier, stage-receipt/0.0-preview. Names the algorithm and the canonical form for the whole record. |
run_id | Identifier of the run this stage belongs to. Every record in one chain shares it. |
stage | { index, name } — position in the chain and the stage's name. The index is a decimal string. |
prev | The digest of the previous record's exact file bytes, or null for the first stage. This is the chain link. |
time | { started, ended } — RFC 3339 timestamps that carry their zone. A timestamp without its zone is half a timestamp and is refused. |
instrument | { id, kind, version, config_digest, manifest_digest, rederivable } — the tool that ran this stage, named and pinned. A record whose instrument cannot be identified is invalid; the conforming producer behaviour is refusal, not a blank field. |
inputs | List of { name, digest, bytes, media_type, trust_class }. Each digest resolves to an earlier record's output or to a source in custody — never a prose reference. |
outputs | Same shape. What this stage produced, by digest. |
assertions | What the stage claims about its work, with a constants object naming every transform constant the claims depend on. A relation that holds only under an unstated constant tests the constant, not the relation. |
outcome | { class, note? } — one of ok, refused, error. A refusal is a result. An error carries status, body and origin; an error without its body is a status code wearing an explanation. |
coverage | { declared_stages, emitting_stages, completeness, boundaries } — the stages the pipeline declares, the stages that actually emitted, and whether they agree. completeness is complete or incomplete, never absent; complete asserted while a declared stage is missing from the emitting list is a failure, not a warning. |
emission | { policy, gaps } — the policy under which records were emitted (the reference is fail-closed) and any declared gaps. |
anchor | { state, reason? } — anchored or unanchored. Absence is a failure: unanchored is a state, not a silence. See §6. |
Every input and output declares one of operator-authored,
model-generated, or externally-sourced. This does not make
externally-sourced content safe; it makes it visible, which is the most a record format
can honestly promise to any reader downstream — human or model.
Absent, declared-none, and recorded are three distinct states and must
round-trip distinguishably. An empty inputs list is a declaration; a missing
inputs field is a defect. A failed measurement is not a measured zero.
Three things on this page each have exactly three outcomes, and they are not the same three.
A stage's outcome.class is ok / refused / error
— what the stage did. A verification stage's per-claim result is supported /
contradicted / couldn't-check — what the evidence says about one claim. A
verifier's report on a record is PASS / FAIL / NOT RUN, with reason —
what a stranger could establish. Collapsing any of them to two is how a system ends up reporting
a pass for something it never examined.
A record's digest is taken over its file bytes, so the file's bytes must be the canonical serialisation of the object they encode — not equivalent to it, identical. The canonical form, in full:
500.00 as a float has already lost the distinction between "500.00" and "500", and float re-serialisation is not stable across languages.NaN, no Infinity, no -0.prev.import hashlib, json
def canonical(obj) -> bytes:
# refuse any JSON number, refuse a self-digest, then:
return json.dumps(obj, sort_keys=True, ensure_ascii=False,
separators=(",", ":"), allow_nan=False).encode("utf-8")
def digest(obj) -> str:
return "sha256:" + hashlib.sha256(canonical(obj)).hexdigest()
jq -S sorts keys but reformats numbers and appends a newline — use it to
inspect, never to produce. The reference canonicaliser ships with the samples as a
~40-line script whose only job is to answer "is this file its own canonical form?"
The envelope says what is digested. These recipes say which bytes, for each kind of thing a stage can touch.
Hash the file exactly as stored. No transformation of any kind.
sha256sum FrameworkAgreement_2025.pdf # Linux / macOS certutil -hashfile file.pdf SHA256 # Windows python -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" f.pdf
Normalise once at write time — UTF-8, LF line endings, no BOM — write to the store, then hash the stored bytes with R1. The record may note the normalisation so a verifier who receives the text through a lossy channel (email, copy-paste) knows how to reconstitute the exact bytes. CRLF is the first cause of false mismatches; kill it at write time, never at verify time.
A chunk's own digest is SHA-256 over the UTF-8 bytes of the chunk text (R2 rules). Its
location is a pair of offsets into the parent's stored bytes. State the offset unit
in the record. This reference uses byte offsets into the parent's stored UTF-8; code-point
offsets are equally valid but must be declared. A record that does not name its offset unit
cannot be checked. Verify: parent_bytes[start:end] must hash to the chunk digest.
Serialise as canonical JSON (§2) and hash those bytes. Arrays stay in the stage's declared order — chunk sequence, rank order — because order is part of what the stage asserts.
Leaves are item digests in manifest order. Pair adjacent leaves, hash the concatenation of the two raw 32-byte digests, repeat to the root. State the odd-leaf rule in the record (this reference: promote the odd leaf unchanged). One published root commits to every leaf, and any single item is verifiable with a log-sized proof path.
The record is a JSON object; serialise it in canonical form including its
prev field, hash the bytes, and that digest is what the next record's
prev cites and what the manifest lists. Editing any historical record changes its
digest, which breaks every later record's link. Tampering surfaces as a broken chain, not as an
argument.
Hash the raw bytes as sent and as received — before parsing, before re-serialisation, before pretty-printing. The JSON you parsed is not the JSON you received; only the received bytes are evidence. Capture at the transport boundary.
The chain head (or a Merkle root over many) is published somewhere the operator does not control the history of, on a cadence: a commit or tag in a public repository, an OpenTimestamps attestation, a transparency-log entry. The anchor proves the record existed by that moment — which is what turns "our clock said 14:02" into "the world can bound when this record was made." §6 states what anchoring does and does not establish.
Keys, credentials, personal data. The record carries digests of artifacts, and artifacts must be reviewed as publishable before their digests are anchored — because an anchored digest of a secret is a commitment you can never unwind. A value inside a published preimage is frozen the moment the first hash is published.
Not every stage can be replayed, and the format refuses to pretend otherwise. The instrument
carries rederivable, and the diagram carries the same fact as two badges:
RECEIPT · re-derivable The stage can be run again from its recorded inputs under its pinned instrument and must produce the same output bytes. Conversion with a pinned tool, chunking under a declared rule, retrieval against a frozen index, prompt assembly, a verification pass — these are receipts.
DECLARED · bytes frozen The stage cannot be replayed — a call to a vendor-hosted model, an external API answering from world-state, a non-deterministic generation — so the record freezes exactly what went in and what came out, digests both, and says on its face that it cannot promise repeatability. A declaration is weaker than a receipt and is labelled as weaker. A record that quietly implies replayability it does not have is the defect this distinction exists to prevent.
The format is stage-agnostic: any pipeline declares its own stages in
coverage.declared_stages. The table below is the ten-stage retrieval pipeline of the
home-page diagram — one worked example, not a required shape. The sample
chain published beside this page is a four-stage subset of it.
| Stage | Beyond the envelope, the record must carry | Recipes | Promise |
|---|---|---|---|
| S0 Source custody | file digest, byte length, depositor, acquisition time | R1 | receipt — identity |
| S1 Conversion | tool publicly obtainable at pinned version; in and out digests | R1 in → R2 out | receipt |
| S2 Cleaning | a census: every exclusion by page, span, and reason code; totals — present even when zero | R2 out; census R4 | receipt |
| S3 Chunking | per-chunk digests; manifest of (parent digest, start, end, seq); offset unit named | R3; R4 | receipt |
| S4 Embedding / index | model identity; weights digest if local; snapshot id and digest | R1 / R4 | receipt pinned local weights · declared hosted API |
| S5 Retrieval | query bytes digest; snapshot id; the full ranked list with scores as decimal strings | R2 query; R4 results | receipt vs the frozen snapshot |
| S6 External call | endpoint identity; wall-clock moment; request and response wire digests; provider signature if offered | R7; R4 if summarised | declared world-state |
| S7 Prompt assembly | digest of every ingredient — template, chunks, tool responses — and of the assembled prompt | R2; ingredient list R4 | receipt |
| S8 Generation | prompt digest; model id, version, parameters; output digest — and nothing implying repeatability | R2 out | declared non-deterministic instrument |
| S9 Verification | per claim: span, target parent digest, offsets, outcome, reason if it couldn't be checked; the three-outcome totals and how they were derived | R3 checks; report R4 + R6 | receipt |
Closure check — run before checking any single offset. Every digest cited anywhere must resolve to a record in the chain; every record must be reachable walking back from the final answer's record; the walk must end at S0. A record set that fails closure has steps off the books, and no per-claim result can rescue it.
A run publishes its records beside a manifest: the ordered list of
{ index, stage, file, digest }, a chain_head equal to the digest of
the last record, the run identifier, and the manifest's own anchor state. A verifier checks
that each record's prev equals the digest of the preceding record's exact bytes
and that the head matches the last one.
A hash proves a record is consistent with itself, never that it is the record that was made. A chain that verifies establishes that these records are consistent with each other. It does not establish that they are the records that were produced at the time: an operator holding every copy can rewrite one and recompute every downstream link. Only anchoring to a commitment the operator cannot rewrite converts consistency into originality — and the anchor cadence is the tamper window.
That is why the format requires the anchor state to be declared on every record, and why a
conforming verifier, on any record marked unanchored, must emit NOT RUN —
originality with the reason stated. A verifier that lets a green chain imply originality is
lying by omission. The reference verifier prints, for every unanchored sample:
[NOT-RUN] originality -- record is declared unanchored;
this verifier can establish consistency, not originality
Anchoring, chaining, and signing are each opt-in and off by default in any implementation of this format. A record that declares itself unanchored is conforming; a record that omits the declaration is not.
git clone -c core.longpaths=true and autocrlf off.1.0842 becoming 1.08420000000000005. Only canonical bytes (§2) are hashable; only wire bytes (R7) are evidence.unanchored and says so; the verifier reports originality as NOT RUN with
that reason.A sample chain, its manifest, the reference canonicaliser, a ~140-line reference verifier, and ten rejection vectors a conforming verifier must refuse — each with the reason it must be refused — are published at oneproof.dev/samples/ (11 September 2026). Three commands check the whole thing:
sha256sum receipts/*.json MANIFEST.json # our digests, your tool python3 canonicalize.py receipts/*.json # is each file its own canonical form? python3 verify.py MANIFEST.json # the chain, offline
A conforming verifier requires no network, no key, no account, no licence, and no live third party; is implementable from the specification and the vectors alone; and never mutates what it checks. A format with no rejection vectors has not specified anything — it has described a happy path and left every implementer to guess at the edges. An implementation claiming conformance must refuse all ten, and must refuse them for the stated reason; refusing the right file for the wrong reason is a failing test that happens to look green.
One verifier checking one format is a claim. Two independent verifiers agreeing on the same records is evidence. If you write one, publish your results against the samples and the vectors — including every place where you and the reference disagree. A disagreement between two verifiers is the most valuable bug report this format can receive, because it means the specification is ambiguous and the ambiguity was found before anyone depended on it.
sha256sum / certutil for R1–R3 · python hashlib +
json.dumps(sort_keys=True, separators=(',',':')) for §2 and R4–R6 · transport-level
capture for R7 · a public repository, OpenTimestamps, or a transparency log for R8. No new
cryptography anywhere. The method is the discipline of which bytes, not new primitives.