- Add tools/validate.py: validates packages, and with --correction runs proposed corrections through the gate against a package without writing anything (spec/11 §11.4). The gate now refuses a proposal that fails the correction schema, rule correction.schema. - tools/load.py validates every package before loading, and with --bundle --built-from builds a scienting-bundle/1 with the FTS5 search index and bundle_meta (spec/13 §13.5). Search rows are defined per unit (sentence, cell, equation outside a sentence, figure-text) with the parent kind as context. - Roster keys carry a role, and a key with the role owner may revoke any receipt (spec/08 §8.7, spec/20); roster schema, store table and the fixture roster updated. The checker signs a trial revocation to prove the rule both ways. - A qa: receipt raises the nodes it sampled except those in its errors (spec/08 §8.2); the example paper's fidelity counts become 27 F3, 13 F2, 4 F1 and 1 F0. - The destination chooses the keyed scheme, not the text policy: hmac-team between the team's machines, hkdf-pages outside the team (spec/05 §5.8, spec/12 §12.5). - A TextQuoteSelector must repeat the anchor's plain quote and is absent for hashed or keyed anchors (spec/18 §18.3). - Add fixtures/records/suspect.json, a scienting-suspect/1 record. - Oracle: hashes are kept incrementally and checked after every correction against a full recomputation; load and write refuse unsafe manifest paths; Ed25519, HKDF and schema validation come from the cryptography and jsonschema packages; canonical JSON uses the built-in encoder with an exact RFC 8785 fallback. - The tools need Python 3.14 with cryptography and jsonschema; the py310 CI environment is dropped and the workflow runs the default only. - Resolve every [CHECK] note in the docs, READMEs and context; add CONTRIBUTING.md; every tool answers --help and refuses unknown options. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
9.0 KiB
8. Fidelity, receipts and spot-checks
8.1 Levels of fidelity
| Level | Meaning |
|---|---|
| F0 converted | One reading, not corroborated. |
| F1 resolved | Every suspect in the node was answered against the page image and passed the gate. |
| F2 corroborated | An independent witness agrees, by match_hash and complex keys, or the text is born digital, as a Typst or LaTeX source is. |
| F3 proofread | A named person compared the node with the page image, and a valid receipt they signed lists the node at its current node_hash. |
Levels apply to leaves.
The pipeline records F0, F1 or F2 in a leaf's fidelity; F3 comes only from receipts.
A correction that changes a leaf drops it back: the gate's crop evidence makes it F1 at most, until it is corroborated or proofread again.
8.2 Levels
A leaf's level is the highest of its recorded fidelity and the levels of every valid receipt (8.6) that lists it at its current node_hash.
A receipt at a lower level never lowers a node; only a revocation, a failed spot-check or a correction does.
A spot-check's qa: receipt (8.8) raises the nodes it sampled, except those its errors name.
A document reports, in document.json, the lowest level among its leaves and the count at each level.
8.3 Receipts
A paper has thousands of nodes, and nobody signs thousands of times, so people sign receipts: one signature over a Merkle root of everything reviewed in a sitting.
| Property | Required | Holds |
|---|---|---|
format |
Yes | scienting-receipt/1. |
id |
Yes | The record ID (4.6). |
doc |
Yes | The document key. |
rev |
Yes | The revision reviewed. |
level |
Yes | F1, F2 or F3. |
scope |
Yes | all, section:<id>, page:<n>, targeted:<name> (such as targeted:lode), or qa:<receipt id> for a spot-check. |
nodes |
Yes | [[id, node_hash], …], sorted by ID without repeats. |
records |
No | [[kind, id, hash], …] for other things signed in the sitting: annotation, digitization, correction, layer or checklist. |
root |
Yes | The Merkle root (8.5). |
evidence |
Yes | What the reviewer looked at: {kind, sha256, node?, page_fp?, box?, note?} with kind crop, witness or source-file, or {kind: "person", by}. A regenerated crop is different evidence. |
by |
Yes | The signer's roster key. |
at |
Yes | When it was signed. |
qa |
For qa: scopes |
The spot-check (8.8). |
export_of |
No | The receipt this one re-issues under a keyed scheme. |
keyed |
No | The scheme its values are keyed under, when it leaves the team. |
signature |
Yes | {scheme, key, value} (8.4). |
A receipt lists nodes, records, or both.
A correction invalidates a receipt for the corrected node only, because the node's node_hash no longer matches; the rest still counts.
A receipt shared outside the team cannot carry plain node hashes, which would leak short texts, and its signature covers them.
So an export re-issues it: a new receipt with export_of, values under the export's key, signed by the original reviewer or a team key with the export purpose.
8.4 Record hashes and signatures
A signed record's record hash is SHA-256 of the canonical JSON of the record without id and signature.
Its id is the 13-character letter-first encoding of that hash.
Its signing payload is the canonical JSON of {"format": <the record's format>, "hash": <the record hash>}.
| Scheme | value |
|---|---|
ed25519 |
The RFC 8032 signature of the payload, in standard Base64 with padding. |
ssh |
An SSH signature of the payload in the namespace scienting, armored. |
gpg |
A detached, armored OpenPGP signature of the payload. |
sigstore |
A Sigstore bundle for the payload, as JSON text. |
signature.key is the signer's roster key and equals by.
Self-hosted deployments sign with team keys or a private Sigstore deployment; the public Sigstore instance, whose log permanently records each signer's identity, is for public layers only.
signatures.jsonl verifies one receipt and the RFC 8032 test vector.
8.5 Merkle roots
A receipt's items are ["node", id, node_hash] for each listed node, in order, followed by its records.
The root follows RFC 6962: a leaf hash is SHA-256 of the byte 0x00 and the item's canonical JSON; an interior hash is SHA-256 of 0x01 and the two child hashes; a list of n > 1 items splits after the largest power of two less than n.
RFC 6962 has since been replaced by RFC 9162, which defines the same tree.
A root over one item is that item's leaf hash; a receipt with no items is invalid.
The tree allows a proof that one node was covered without revealing the others; merkle.jsonl gives roots for one to seven items.
8.6 Receipt status
A receipt is:
- invalid when its root, ID or signature does not verify, or its signer's key was not valid when it was signed (
registered_at≤at<revoked_at); - otherwise revoked when a valid revocation names it, or names its signer with a period that contains its
at; - otherwise suspended when a valid spot-check of it found an error;
- otherwise valid.
Only valid receipts set levels. A suspended receipt stays suspended; its reviewer reviews the sitting again and signs a new receipt, which needs its own spot-check.
8.7 Revocations and the roster
A revocation, scienting-revocation/1, names one receipt, or a signer and a period {from, to}, with a reason, and is signed like a receipt.
It is valid when its signature verifies and its signer is the revoked receipt's signer, the signer it names, or a key whose role in the roster is owner.
The roster, scienting-roster/1, lists the keys that may sign: each with an id, a person, a role, a scheme, its public key (Base64 for Ed25519), its purposes (sign, spot-check, export), registered_at and revoked_at.
The role is the holder's role in the team that keeps the roster, owner, editor, reader or guest, as in a project (20).
Because the roster carries it, a package that carries its roster carries everything needed to judge its revocations.
A package MAY carry the roster its receipts need as roster.json, so its receipts verify without the store.
The roster is meant to be the one a store verifies against; see the rationale.
8.8 Spot-checks
For each F3 receipt, a second reviewer checks a sample of its nodes and signs a qa: receipt.
A seed the reviewer controls could be ground, by varying the receipt until the sample falls on careful work, so the seed is a signature nobody can steer:
- The seed is the checker's Ed25519 signature over the canonical JSON of
{"format": "scienting-spot-check/1", "receipt": <the original's record hash>}, made with a key registered in the roster with thespot-checkpurpose before the original was signed. Ed25519 signatures are deterministic, so the checker cannot choose it, and the reviewer cannot predict it. - The sample size is the smaller of n and the larger of
minimumand ⌈rate× n⌉, for the original's n nodes; the defaults are a rate of0.05and a minimum of 10. - Rank the original's node IDs by the hexadecimal SHA-256 of the seed's 64 bytes followed by the ID's UTF-8, then by ID, and take the first ones.
The qa object records of, key, seed (Base64), rate, minimum, the sample sorted by ID, and the errors found, each {node, kind?, note?}.
The qa: receipt's nodes are the sampled nodes at the hashes the original lists.
Anyone can recompute the sample; spotcheck.jsonl does.
8.9 Reviewer error bounds
One sample proves little; what someone relying on the work needs is a bound on each reviewer's error rate across all their samples.
With k errors in n sampled nodes, the bound is the one-sided Clopper–Pearson upper confidence bound at confidence c, by default 0.95: the rate p at which the probability of at most k errors in n is 1 − c, or 1 when k = n.
It is reported to six decimal places, rounded up.
With no errors the bound is about 3/n, so zero errors in 300 sampled nodes bounds a rate below 1%; bounds.jsonl gives cases.
A QA report, scienting-qa-report/1, states for each reviewer who signed a delivery the nodes sampled, the errors found, the confidence, the method clopper-pearson and the bound.
8.10 Targeted F3
The target set of a document is every node that an extract, a dataset value, a confirmed claim-link, a draft quotation or a pinned citation anchors, plus every display equation, plus every sentence with a digit in a text or math run.
Digits in cite, xref and fnmark runs do not count.
A project sets the digit rule to all, anchored-sections (only sentences in a section that holds an anchored node) or none.
Neighbors shown for context are not in the set, and nothing new comes to rest on a node below F3 without the node joining the set.