UW Verification Receipt 1.1
Media type: application/vnd.uwmd.receipt+jsonSchema: schemas/uw-receipt.schema.jsonDefining RFC: 0016 (accepted 2026-08-09) Status: normative for receipt version 1.1
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.
1. Purpose and assurance boundary
A verification receipt is a small, detached JSON document stating which underwriting record was checked, which deterministic calculation pack ran against it, what that pack produced, and which policy set was applied. It is bound to the record by a canonical digest and MAY be signed.
A receipt lets a party who did not run the calculation confirm, offline and without the original tool, that a stated set of numbers follows deterministically from a stated set of inputs.
A verified receipt attests exactly two things:
- the record's canonical financial content is unchanged since issuance; and
- the stated outputs follow deterministically from that content under the named pack and policy set.
It attests nothing about whether the inputs are true, complete, sourced from genuine documents, or reasonable. A record asserting a fabricated NOI can carry a perfectly valid receipt.
Implementations MUST NOT present a verified receipt using language that implies the underwriting is correct, complete, audited, or approved. Interfaces surfacing receipt status MUST make the distinction in this section available to the reader rather than displaying an unqualified checkmark. This raises the language in UW_LITE_SPEC.md §9 to a requirement on every consumer.
2. Detachment
A receipt is a standalone document. It MUST NOT be embedded in the record it describes. Embedding would break the Tier-2 byte-preservation invariant, would make the record's digest depend on a value derived from that digest, and would prevent a third party from issuing a receipt about a record they must not modify.
Transport is out of scope for this version; the object is transport-neutral. The conventional sidecar spelling is <record>.receipt.json.
3. Document shape
{
"receipt_version": "1.1",
"subject": {
"representation": "uw-lite-markdown",
"representation_version": "1.0",
"canonicalization": "uw-lite-financial",
"canonicalization_version": "1.1",
"digest": "sha256:f930ebf20c9b0cc1159b65cc653f50a728fa07858080be5f6592ab307953d216"
},
"computation": {
"pack": "org.uwmd.pack.multifamily",
"pack_version": "1.0.0",
"engine": "@uwmd/core",
"engine_version": "1.2.0",
"protocol_version": "1.3.0",
"rule_set": "3.0",
"results": [
{ "calc_id": "dscr", "value": 1.2824864235841738, "unit": "x", "computed": true },
{ "calc_id": "ltv", "value": 0.6, "unit": "%", "computed": true },
{ "calc_id": "cash_on_cash", "value": null, "unit": "%", "computed": false }
],
"results_digest": "sha256:…"
},
"policy": {
"policy_set": "builtin",
"policy_set_version": "1.0",
"validation": { "errors": 0, "warnings": 2 }
},
"issued_at": "2026-08-09T00:00:00Z",
"issuer": "uwmd-cli@2.3.0",
"signature": null
}The JSON Schema is normative for structure; this document is normative for behavior.
policy.validation counts the validation result's errors and warnings. Format §5.2 deal flags are not issues and are not counted (RFC 0109); an institution escalation does not apply, since receipt issuance loads no institution configuration.
3.1 Canonicalizations
subject.canonicalization names the canonical form the digest covers. This specification defines two:
| Name | Applies to | Definition |
|---|---|---|
uw-lite-financial | uw-lite-markdown | UW_LITE_SPEC.md §6 financial canonical form |
uw-envelope-semantic | uwx-markdown | RFC 8785 canonicalization of the Document Envelope semantic value (envelope minus generated_at, generator, semantic_digest) |
Both bind to a semantic form rather than raw bytes. Lite receipts bind the financial canonical form. Structured envelope receipts also cover annotations, inline metadata, superseded history and retained section prose. Cosmetic serialization changes preserving that canonical value do not change its digest. RFC0089 coordinates the structured subject with Envelope2.0 and canonicalization_version: "2.0"; Lite's financial canonicalization remains separately versioned and scoped. Source projection performs complete occurrence accounting without importing full carrier schema admission; receipt issuance retains its own validation-count/issuance rules. Protocol §V.13 reuses the envelope subject for computation-independent document authentication; that detached artifact is not a computation receipt.
subject.digest MUST be SHA-256 over the exact UTF-8 bytes of the named canonical form, serialized as sha256:<lowercase hex>.
3.2 Results
RFC 0123: an applicable builtin calc_id asserts its selected rule-set dictionary definition. A producer using another definition MUST use another id; a starter pack cannot redefine standard meaning. Pack-less verification follows §5.1.
computation.results MUST contain every calc the named pack declares as an output, and MUST NOT contain any value the pack did not compute. A receipt is not a place to carry hand-entered numbers.
The calc engine reports "the record lacks these inputs" as a successful evaluation to null. A receipt MUST NOT let that read as a computed value: such an output is recorded with "computed": false and "value": null. When computed is false, value MUST be null.
Results SHOULD be ordered by calc_id so re-issuance is byte-stable.
computation.results_digest is SHA-256 over the RFC 8785 canonicalization of the results array. It exists to detect accidental corruption cheaply, and never authorizes skipping recomputation (§5).
3.3 Signature
signature, when present, uses the wire shape RFC 0010 defines for block signatures (algorithm, key_id, value) and covers the RFC 8785 canonicalization of the receipt with signature set to null. Key distribution and revocation are out of band.
4. Issuance
An issuer MUST NOT emit a receipt for a document whose parse produced any error-severity issue. This extends the existing UW_LITE_SPEC.md §5 refusal to every representation.
Issuance is total: an implementation either returns a complete receipt or raises a typed error. It MUST NOT emit a partial, caveated, or provisional receipt.
An issuer MUST NOT name a pack that declares no version, and MUST NOT name a pack that declares no calculations.
An issuer MUST compute and validate under the rule set the record's declared Format version names (Protocol §XII.5) and state it at computation.rule_set (§5.2.2).
Validation errors and warnings are counted and recorded, but do not block issuance — a receipt describes what the policy set found, and a record that trips a threshold is still a record whose arithmetic can be attested.
Re-issuance stability (invariant). Re-issuing a receipt over an unmodified record MUST reproduce the same subject.digest and the same results. Only issued_at may differ.
5. Verification
A verifier MUST report exactly one of three states, and MUST NOT collapse the third into either of the others:
verified— the digest matches, recomputation reproduces every stated quantized result exactly, and the signature (if present) validates.failed— the digest, a result, or the signature disagrees.unverifiable— the verifier lacks the named pack, pack version, engine, key, or a canonicalizable record, and therefore cannot decide. Absence of evidence is not a negative result.
A verifier MUST recompute rather than trust results_digest.
5.1 Precedence
Verifiers MUST apply these in order:
- Record not canonicalizable (parse errors, undetectable representation) →
unverifiable(RCP-09). There is nothing to compare against. - Digest mismatch →
failed(RCP-01), unless the receipt'scanonicalization_versionalso differs from the one this verifier applies, in which case →unverifiable(RCP-10); see §5.5. Absent that, the mismatch is decisive: the record's financial content changed after issuance, and no later check can rehabilitate it. 2a. A referenced input resolved and its digest disagrees →failed(RCP-12); see §10. Checked here, above the indeterminate group, because a reference the verifier holds and can compare is real evidence, and ordering it later would let an unknown pack mask a mutated observation set. 2b. The receipt names a rule set other than the one its record declares →failed(RCP-13); see §5.2.2. Like the digest, this compares the receipt with its own subject, so it is decided before the indeterminate group and before any signature check or recomputation. - Input/backend preflights → unverifiable for unavailable referenced inputs (RCP-11) or a signature without a backend (RCP-08). Preserve issue order: unresolved inputs, pack availability/version when applicable, then backend. RCP-05/06 alone MUST NOT prevent dictionary recomputation under RFC 0123.
- Authenticate first (RFC 0088). If a signature is supplied, verify it exactly once over the canonical signing payload before any financial recomputation or recomputation-refusal verdict. Invalid signature or malformed Unicode signing input → failed/RCP-01, with no recomputed output. Malformed signing input does not reach the backend; backend exceptions retain their transport behavior. Unsigned receipts gain no authentication claim. 4a. Recomputation refusal → failed/RCP-03 with recomputed:[], after successful authentication when signed. This remains a refusal even with a different engine identity; the finite-disagreement carve-out below does not apply.
- Recomputation disagreement →
failed(RCP-02coverage,RCP-03value), unless the issuing engine identity also differs, in which case →unverifiable(RCP-07); see §5.3. results_digestdoes not recompute →failed(RCP-04).- If pack availability/version still prevents proof of complete named-pack coverage, return unverifiable/RCP-05 or RCP-06 with the actual dictionary subset in recomputed. Otherwise → verified.
Malformed wire values and source/subject/held-input preflights remain earlier than authentication. A supplied signature authenticates before any financial recomputation or refusal, including when a pack is unavailable. No backend means RCP-08 and no recomputed values; invalid signatures remain RCP-01.
Dictionary result identity (RFC 0123). For the selected rule set and asset class, an applicable builtin calc_id asserts its dictionary definition, selectors, prerequisites, dependencies and reported-output quantization. Alternate definitions MUST use another id. A Verifier MUST recompute every such stated result without depending on the named starter pack, including when its version differs. Resolve dependency closure internally; expose only stated result ids, never unstated intermediates or invented record inputs. Compare value, computed status and unit exactly. Deterministic refusal remains RCP-03; finite disagreement retains the RCP-03/RCP-07 engine-identity rule. A real same-engine dictionary disagreement MUST NOT be masked by RCP-05/06. Recompute results_digest over the complete stated list independently of pack presence. Agreement of visible dictionary results cannot prove named-pack output coverage; pack-only or unknown coverage remains unverifiable. A known matching pack retains exact declared-output coverage RCP-02 and evaluation of its remaining pack-only outputs. No wire field or tolerance is added.
5.2 Tolerance
Receipt results are the calculation's reported outputs, quantized once at its effective round_to under Protocol §VIII.5. Numeric and non-numeric results MUST agree by exact equality; there is no additional absolute or relative tolerance. Stated and recomputed results MUST also agree on computed and unit. Result digests remain over those full-precision reported outputs. The historical 1e-6 × max(1, |stated|, |recomputed|) comparison is superseded; a verifier MUST NOT accept a different result merely because it falls within that old bound.
A document's metric statement is a different comparison boundary: RFC 0079 and Protocol §VIII.5.1 compare the unrounded dictionary result at the statement's source precision, subject to its dictionary minimum and capped at round_to. This does not reduce the precision of issued receipt results or digests. No producer display string sets either comparison's precision.
5.2.1 Protocol version
computation.protocol_version is OPTIONAL and states the UW Protocol version the issuer computed under. An issuer conforming to 1.3.0 or later SHOULD populate it.
It exists because engine_version cannot answer the question a verifier actually has — "are these values quantized per §VIII.5?" An engine version only means something to a reader who knows that engine's release history, and a third-party issuer's history is exactly what a verifier does not have. A protocol version is the one identifier every conforming issuer shares.
Absence means unstated, not non-conforming: a receipt issued before the field existed cannot retroactively claim a version. A verifier MUST NOT treat absence as a failure.
RFC 0124 requires an explicit kept uw_version in Lite subjects too. A missing, null, malformed or unsupported declaration cannot canonicalize (RCP-09). It is not filled from uw_lite_version or the receipt. A valid subject whose receipt names another rule set retains failed/RCP-13 before pack/backend uncertainty.
5.2.2 Rule set
Added by RFC 0111 D5.computation.rule_set is REQUIRED and names, by Format version, the rule set the issuer computed and validated under (Protocol §XII.5). It is always the subject record's declared uw_version; for a UW Lite record that declares none, it is the Format version the Lite bridge compiles to (3.0).
A verifier selects the rule set from the record, never from the receipt, and reports the one it applied (rule_set in its verification result). If the receipt names a different rule set, the verdict is failed with RCP-13: the receipt misstates its own subject, and its results do not attest a computation under the rules that record is verified by. Unlike §5.3 and §5.5 there is no unverifiable carve-out: the rule set is fixed by the record, not by either side's implementation. A record whose declared version names no rule set the verifier keeps does not canonicalize (RCP-09, §5.1 step 1).
5.3 Engine-identity mismatch
Resolves the open question in RFC 0016. If recomputation disagrees and the verifier's engine identity differs from the receipt's, the verdict is unverifiable with RCP-07, not failed. The disagreement cannot be attributed to the record, and reporting it as a failure would blame a document for an engine upgrade. If the engine identities match, a disagreement is failed.
Engine identity is the pair (computation.engine, computation.engine_version), and a verifier MUST compare both. A version string is only ordered within one engine's release history: 2.1.0 of an unrelated implementation is not a build of this engine that happens to be newer, so treating a bare version match across two different engine values as "same engine" would report a genuine cross-engine disagreement as failed and blame the record for it.
An engine-identity difference alone, with results agreeing, is not an issue: determinism across engines and versions is the expected case, and reporting it would train users to ignore the state.
5.4 Issue codes
| Code | Verdict contribution | Meaning |
|---|---|---|
RCP-01 | failed | Subject digest disagrees, or the signature did not validate |
RCP-02 | failed | Stated results do not cover exactly the pack's declared outputs |
RCP-03 | failed | A stated result disagrees with its quantized recomputation |
RCP-04 | failed | results_digest does not recompute over the stated results |
RCP-05 | unverifiable | The named pack is unknown to this verifier |
RCP-06 | unverifiable | The verifier holds a different version of the named pack |
RCP-07 | unverifiable | Results disagree and the engine identity also differs |
RCP-08 | unverifiable | A signature is present and no backend is available |
RCP-09 | unverifiable | The record could not be canonicalized for comparison |
RCP-10 | unverifiable | Digests disagree and the canonicalization version also differs |
RCP-11 | unverifiable | A referenced input is not available to this verifier |
RCP-12 | failed | A referenced input resolved, but its digest disagrees |
RCP-13 | failed | The receipt names a rule set other than the one its record declares (§5.2.2) |
COMP-ROLLUP-DISAGREES | failed | A stated composite aggregate disagrees with recomputation (§11) |
5.5 Canonicalization-version mismatch
Added by RFC 0025. A digest is only evidence about a record when both sides computed it under the same rules. If the subject digests disagree andsubject.canonicalization_version differs from the version this verifier applies for that canonicalization, the verdict is unverifiable with RCP-10, not failed.
This is the same carve-out §5.3 makes for engine identity, one step earlier in the precedence and for the same reason: a verifier must not report that a document was tampered with when the only thing that changed is its own arithmetic. The concrete case is UW Lite canonicalization 1.1, which fixed percent scaling (UW_LITE_SPEC.md §4.1) and so moved the digest of every Lite document containing a percent that does not divide exactly — while the documents themselves stayed byte-identical.
Two limits keep the carve-out narrow. It applies only when the digests already disagree: a version difference with matching digests is not an issue at all, and verification proceeds normally, which is the common case since most documents are unaffected by any given rules change. And it is keyed on the version differing, not on it being older — a verifier cannot assume it holds the newer rules, and MUST NOT attempt to re-canonicalize under the receipt's version unless it actually implements it.
A verifier that implements multiple canonicalization versions MAY recompute under the receipt's stated version and, if that digest matches, continue to the remaining checks rather than stopping at RCP-10.
canonicalization_version is versioned independently of representation_version: the rules for producing canonical bytes can change without the source grammar changing, and an implementation MUST NOT stamp one field from the other.
6. Staleness
A Tier-2 editor SHOULD treat any existing receipt as stale once a write lands, because the canonical digest changes. Presenting a stale receipt as current is the principal way to misuse this feature.
7. Conformance
Conforming implementations are exercised by conformance/receipts/:
issue/— record plus expected receipt (withissued_atstubbed).verify/— record plus receipt plus expected verdict, covering a clean verify, a record mutated after issuance, a result that disagrees with recomputation, and a receipt naming an unknown pack.refuse/— a document with parse errors and an assertion that issuance refuses rather than emitting an unsigned or caveated receipt.
Two invariants are asserted without a frozen baseline: re-issuance over an unmodified record reproduces the same subject.digest and the same results (§4), and a verifier presented with any receipt returns one of exactly the three verdicts in §5.
8. Relationship to RFC 0010
RFC 0010 signed blocks attest authorship and non-tampering of a block — who wrote it, unchanged since. A receipt attests correctness of a computation over a whole record — these outputs follow from these inputs under this pack. The two compose but answer different questions, and neither substitutes for the other. A fully signed record can still carry arithmetic no pack would produce.
9. Reference implementation
packages/uwmd-verify/src/receipts.ts implements unsigned issuance and verification (re-exported by @uwmd/core). Signature creation and validation live outside both libraries so they stay free of cryptographic dependencies; a verifier without a backend returns unverifiable for a signed receipt.
CLI: uwmd receipt issue <file> and uwmd receipt verify <file> <receipt.json>.
10. Input provenance (receipt 1.1)
Established by RFC 0022 §3; amended by RFC 0021 §6.
10.1 Why one section rather than two
A receipt attests that stated outputs follow from stated inputs. When an input is not in the subject record — a market-data observation set, or a child record inside a composite — the receipt must name it, or the computation is not reproducible against the inputs actually used.
RFC 0021 and RFC 0022 were accepted the same day and both needed this. They share one section with a source discriminator rather than two parallel lists, because the verifier's handling is identical in both cases: resolve the reference, compare the digest, and keep "I cannot find it" distinct from "it does not match". That distinction is the reason this is a normative structure and not a comment field.
10.2 Shape
{
"inputs_provenance": [
{
"source": "market_data",
"document_id": "md:phx-multifamily:2026-Q2",
"as_of": "2026-06-30",
"digest": "sha256:<64 lowercase hex>"
}
]
}inputs_provenanceis OPTIONAL. A receipt over a record that consumed no external input omits it entirely. Absence means the issuer stated no external input; it never implies non-conformance.source,document_id, anddigestare REQUIRED on each entry.digestMUST matchsha256:[0-9a-f]{64}.as_ofis OPTIONAL at this layer, because not every source has a vintage. Amarket_dataentry always carries one in practice:market-data-v1refuses to parse withoutas_of, so an entry lacking one could not have been produced from a conforming observation set.document_idMUST be unique within the array. Two entries for one id would make the digest comparison depend on iteration order.sourceis an open union.market_data(RFC 0022) andchild_record(RFC 0021) are registered; an implementation encountering an unrecognized source MUST carry it and report it as unresolvable (RCP-11) rather than rejecting the receipt or ignoring the entry.- An issuer MUST NOT derive these entries by guessing what a record might have used. The host supplies them, because only the host knows what it resolved against; a fabricated entry would make a receipt claim an input it never used.
- An issuer SHOULD emit entries sorted by
document_id, so re-issuance over an unchanged record reproduces identical bytes regardless of the order the host listed them in (§7's re-issuance invariant).
10.3 Verification
A verifier holds some set of referenced artifacts. For each entry:
| Verifier state | Verdict contribution |
|---|---|
| Holds it, digest matches | no issue; continue |
| Holds it, digest differs | failed (RCP-12) |
| Does not hold it | unverifiable (RCP-11) |
A verifier that holds none of the named references reports unverifiable. It MUST NOT report verified, because it has not checked something the receipt says the computation depended on — and it MUST NOT report failed, because an absent reference set is not evidence of tampering. This is the same three-state discipline §5 already applies to unknown packs and to signed receipts with no backend.
A verified receipt over a record that consumed market data means: the stated outputs follow deterministically from these inputs, and these particular observations were the ones used. It does not mean the observations are accurate, current, representative, or that the provider is competent.
10.4 Versioning
receipt_version MUST be "1.1"; no other receipt version is admitted. Input provenance is optional. The receipt version is independent of representation, canonicalization, engine and Protocol versions. The mismatch verdicts RCP-06/07/10 remain; a verifier unable to check the named pack, engine or canonicalization reports unverifiable, not verified.
11. Rollup receipts
Added by RFC 0021 §6. Amends §10 rather than forking a parallel section, and does not bump receipt_version again — see §10.4.
11.1 What a rollup receipt is
A composite record states aggregate figures over its children. It does not compute them in the calc engine: the Tier-3 sandbox has no iteration and no array indexing, and RFC 0019 reached exactly that wall for mixed_use components. A portfolio total over N assets is the same problem at a different scale.
Instead the verifier evaluates a fixed, closed, non-extensible aggregation vocabulary over the named children. Nothing here lets a module or a document author introduce a new aggregation, and nothing here makes the Tier-3 evaluator iterate. If a general aggregation primitive ever lands, it supersedes this section rather than sitting beside it.
{
"aggregates": [
{
"id": "portfolio_noi",
"fn": "sum",
"over": "noi_model.net_operating_income",
"members": ["deal:parkview", "deal:riverside"],
"value": 1049823
}
]
}The permitted fn values are exactly sum, count, min, max, and weighted_average. weight_by is REQUIRED for weighted_average and MUST NOT appear otherwise. members MUST NOT name a member twice — a member counted twice inflates a sum, and a portfolio total is exactly the figure nobody re-checks by hand.
11.2 Two-stage verification
- Child stage. Each named member's own receipt is verified — its record digest is unchanged and its pack outputs recompute. This is existing §5 behaviour applied per child.
- Parent stage. Each aggregate is recomputed by applying
fnto theoverpath of exactly the members it names, and compared to the statedvalue.
Verdict precedence:
| Condition | Verdict |
|---|---|
Any child failed | failed |
Any child unverifiable, or a named member's value unavailable | unverifiable |
| A stated aggregate disagrees with recomputation | failed (COMP-ROLLUP-DISAGREES) |
| Otherwise | verified |
The child stage is decided before any arithmetic. A total computed over a child whose own receipt failed must not be reported as agreeing, even when the numbers happen to add up.
A member whose value cannot be read is unevaluable, never zero. Summing what is present would produce a confident portfolio figure over a portfolio the verifier never saw, which is the failure mode this section exists to prevent. count is not exempt: counting members whose values could not be read would report a count over a set the verifier did not actually see.
A weighted_average whose weights sum to zero is failed, not unverifiable. The verifier holds every value it needs; weights summing to zero make the stated average an assertion that cannot be well-defined, which is a defect in the record rather than a gap in the verifier.
11.3 Relationship to §10
A rollup receipt SHOULD also record each child in inputs_provenance with source: "child_record". The two mechanisms answer different questions and compose: §10 detects that a child changed since issuance (RCP-12), while §11 recomputes whether the stated aggregate follows from the children. A receipt carrying both catches a mutated child cheaply, by digest, before any aggregate arithmetic runs.
11.4 Assurance boundary
Restated because a portfolio figure is exactly the kind of number people over-trust. A verified rollup means the stated total follows deterministically from those child records as they stand. It does not mean the children are complete, that the portfolio contains every asset it should, that the members named are the right members, or that any input is true.