oneproof.dev
home · prove · stage records
Preview. This page specifies a record format, 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.

Stage records

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.

§0The one rule everything else serves

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.

§1The record envelope — every stage, no exceptions

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.

FieldWhat it must contain
formatThe format identifier, stage-receipt/0.0-preview. Names the algorithm and the canonical form for the whole record.
run_idIdentifier 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.
prevThe 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.
inputsList 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.
outputsSame shape. What this stage produced, by digest.
assertionsWhat 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.

Trust classes

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.

Three states that must not collapse

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 different three-way splits — kept apart on purpose

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.

§2The canonical form — specified as runnable code

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:

  1. UTF-8, no byte-order mark.
  2. JSON, object keys sorted by Unicode code point.
  3. No whitespace outside string literals.
  4. No JSON numbers. Every numeric value — counts, offsets, indices, scores, byte lengths — is a decimal string. A format that serialises 500.00 as a float has already lost the distinction between "500.00" and "500", and float re-serialisation is not stable across languages.
  5. No NaN, no Infinity, no -0.
  6. The file contains exactly the canonical bytes and nothing else — no trailing newline.
  7. A record never contains its own digest. A record cannot contain the proof of its own final state; the digest lives in the chain manifest and in the next record's 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?"

§3Digest recipes — one per kind of artifact

The envelope says what is digested. These recipes say which bytes, for each kind of thing a stage can touch.

R1 · File bytes — binary artifacts (PDF, image, wheel, model weights)

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

R2 · Text artifacts — converted text, cleaned text, prompts, answers

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.

R3 · Chunks and spans — parts of a digested parent

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.

Two conformance levels, and the record says which. The stored-span shape above is the format's full profile. A pipeline may instead record containment lineage: each chunk names its parent, and the chunk text is verifiably a substring of the parent's stored blocks under the chunker's declared join — with spans produced as a derived quantity at check time, that derivation itself receipted with three outcomes (unique / ambiguous, with the occurrence count / not found). The reference pipeline currently implements the derived-span level; the diagram on the home page shows it. A record conforms at either level provided it declares which. A claim of stored spans that the bytes do not back is worse than either.

R4 · Manifests — set-valued outputs (chunk sets, ranked results, a census)

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.

R5 · Merkle root — large sets, external anchoring

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.

R6 · The record itself — and therefore the chain

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.

R7 · Wire bytes — external calls, request and response

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.

R8 · Anchoring — what makes the timestamps testify

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.

R9 · What never gets digested into anything — secrets

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.

§4Receipts and declarations

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.

§5Per stage — the worked example

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.

StageBeyond the envelope, the record must carryRecipesPromise
S0 Source custodyfile digest, byte length, depositor, acquisition timeR1receipt — identity
S1 Conversiontool publicly obtainable at pinned version; in and out digestsR1 in → R2 outreceipt
S2 Cleaninga census: every exclusion by page, span, and reason code; totals — present even when zeroR2 out; census R4receipt
S3 Chunkingper-chunk digests; manifest of (parent digest, start, end, seq); offset unit namedR3; R4receipt
S4 Embedding / indexmodel identity; weights digest if local; snapshot id and digestR1 / R4receipt pinned local weights · declared hosted API
S5 Retrievalquery bytes digest; snapshot id; the full ranked list with scores as decimal stringsR2 query; R4 resultsreceipt vs the frozen snapshot
S6 External callendpoint identity; wall-clock moment; request and response wire digests; provider signature if offeredR7; R4 if summariseddeclared world-state
S7 Prompt assemblydigest of every ingredient — template, chunks, tool responses — and of the assembled promptR2; ingredient list R4receipt
S8 Generationprompt digest; model id, version, parameters; output digest — and nothing implying repeatabilityR2 outdeclared non-deterministic instrument
S9 Verificationper claim: span, target parent digest, offsets, outcome, reason if it couldn't be checked; the three-outcome totals and how they were derivedR3 checks; report R4 + R6receipt

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.

Exhibit B · on the home pageOne question, ten records — the diagram this table describes, with each stage's badge, digest, and what it admits it cannot re-derive.

§6The chain, the manifest, and the limit of a green chain

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.

§7The five classic ways a digest lies

  1. CRLF / BOM drift. A Windows checkout rewrites line endings; the hash mismatches on text that looks identical. Fix at write time (R2). Verifiers clone with git clone -c core.longpaths=true and autocrlf off.
  2. JSON re-serialisation. Same data, different bytes — key order, whitespace, 1.0842 becoming 1.08420000000000005. Only canonical bytes (§2) are hashable; only wire bytes (R7) are evidence.
  3. Hashing the parsed, not the received. A pretty-printed API response digests differently from what arrived. Capture at the boundary.
  4. Unstated offset units. Bytes, code points, and UTF-16 units silently disagree the moment text leaves ASCII. Name the unit in the record.
  5. Self-reported time. A timestamp without an anchor is testimony. Anchor the chain head; let the batch bound the clock.

§8What this format does not yet do

Non-linear topology. The chain as specified is linear. Fan-out, fan-in, retries as distinct attempts, branches and loops are an open requirement and are not represented in the samples. This is the largest hole, and it is named rather than cropped out of the picture.
Anchoring. Declared, required, not implemented in the reference. Every sample is unanchored and says so; the verifier reports originality as NOT RUN with that reason.
Signatures. Sequenced behind actor identity; not present.

§9Check it yourself — standard tools, no network, no key

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 — publish at oneproof.dev/samples/ on 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.

The toolbox — nothing bespoke

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.