wai.media.contribution: who contributed what, computed from the process (CO1) #403

Merged
dcharlot merged 6 commits from wai/contribution into main 2026-10-21 19:02:22 -04:00
19 changed files with 8707 additions and 12 deletions

View file

@ -122,6 +122,10 @@ on:
# ai_usage_verify.py reads that table, and the registry's wire identifiers test reads its
# sections.
- 'wai/extensions/ai-usage.md'
# tests/contribution.rs requires this extension's §11 to list the codes the reference
# reports, contribution_verify.py reads that table, and the registry's wire identifiers
# test reads its sections.
- 'wai/extensions/contribution.md'
- 'wai/wai-web/**'
# The audio provenance check below reads the corpora above with this tool.
- 'wai/tools/wai_audio_provenance_check.py'
@ -794,7 +798,8 @@ jobs:
# e2ee_verify.py (the end-to-end encrypted rooms corpus), live_watermark_verify.py (the
# forensic-watermark corpus), live_ads_verify.py (the live-ads corpus), c2pa_live_verify.py
# (the live-provenance corpus), edge_verify.py (the deriving-edge corpus),
# synthesis_verify.py (the synthesis corpus) and ai_usage_verify.py (the AI usage corpus).
# synthesis_verify.py (the synthesis corpus), ai_usage_verify.py (the AI usage corpus) and
# contribution_verify.py (the contribution-records corpus).
# Each exits non-zero on any failure or any difference, and the step fails with it.
# One platform: what they check is the reading of the text, not the architecture.
independent-verifiers:
@ -1176,6 +1181,28 @@ jobs:
(cd wai-rs && cargo test --no-default-features --features ai_usage --test ai_usage)
python3 -I live-conformance/ai_usage_verify.py live-conformance/usage
python3 -I claim-conformance/claim_verify.py consumption live-conformance/usage/vectors.json
# wai.media.contribution (extensions/contribution.md): the reference module's unit tests
# (every proof of the steps' tree walks to its root, the copying operations follow their
# sample mappings, a map has one encoding) and tests/contribution.rs (the committed corpus
# regenerates byte for byte from its fixed seeds and no other file is there, wai-rs gives
# every case its stated outcome and statements, §11 lists exactly the reference's codes,
# each of which the corpus gives, and the corpus generates with the model file of
# wai.generate.spectral's conformance vector); then the corpus's second implementation,
# contribution_verify.py, written from the extension's text and sharing no code with
# wai-rs, which evaluates every pinned operation, runs the generator in its own arithmetic
# after checking it against the generator's conformance vector, computes every map, share
# and C2PA statement itself, reads origin claims with synthesis_verify.py and every record
# with WAI's strict Ed25519 rule and BLAKE3 from claim-conformance, requires every mutated
# copy of the corpus to fail, and drops each of its own checks in turn and requires the
# corpus to catch the weakened verifier; standard library only, run isolated (-I).
- name: contribution records · wai-rs and contribution_verify.py
working-directory: wai
shell: bash
run: |
set -euo pipefail
(cd wai-rs && cargo test --no-default-features --features contribution --lib contribution::)
(cd wai-rs && cargo test --no-default-features --features contribution --test contribution)
python3 -I live-conformance/contribution_verify.py live-conformance/contribution
# wai.live.e2ee (extensions/confidentiality.md): the corpus's second implementation,
# e2ee_verify.py, written from the extension's text and the documents it reads and
# sharing no code with the reference (joule-moq's secure_objects and joule-moq-mls,

View file

@ -6,6 +6,51 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### `wai.media.contribution`: who contributed what, computed from the process
- **The extension** ([contribution](extensions/contribution.md)) registers the record
`wai.media.contribution`. A tool signs a **session** naming its parties: people by Ed25519 key
(`human`, `audience`), each countersigning its own membership (`wai-membership`), and models
by digest. A person cannot be enrolled as a maker or a director of any sample without that
signature (`party_not_countersigned`).
- **Steps.** Every act is a signed step (`wai-step`) in a hash chain, signed by the person who
performed it or by the tool for a model: `author`, `generate`, `candidates`, `select`, `edit`,
`inpaint`, `transform`, `compose` and `reject`; `direct` is reserved for direction tracks.
Transforms and compositions evaluate pinned operations by name: deriving-edge's crop,
rotation, mirror, overlay and grid composition, synthesis's upscale and denoise, and one new
per-sample colour table, `wai-contribution:lut`. The exact generator is `wai.generate.spectral`.
- **The authorship map**, computed by re-executing each act's dependency rule: for every pixel
of the output, `made_by` (whose act produced its value) and `directed_by` (whose prompt, mask,
selection, parameters or arrangement conditioned it). A mixing filter takes the union of what
it mixes; a selection adds the selector everywhere; a composition takes each layer where it is
visible. Encoded with a palette and run-length rows, in one encoding, and bound to the output's
samples by its digest. Shares are exact sample counts per party for each of the two sets,
never one blended figure; an estimator's share rides beside them with its class.
- **Unknown is never zero.** An attested step's unpinned sample mapping leaves its samples
`unknown`, and a verifier marks samples from steps it does not hold `undisclosed`; neither is
ever credited to a party, and a stated map that credits an unknown sample is refused
(`unknown_attributed`).
- **What a verifier checks**: the chain and every step's inclusion in the record's Merkle root;
that an edit or an inpainting changed nothing outside its region, byte for byte
(`outside_region_changed`); that a selection is a member of its committed set
(`selection_not_in_set`) and that every candidate regenerates from its seed
(`regeneration_mismatch`); that each transform and composition recomputes
(`recompute_mismatch`); that the map recomputes (`map_mismatch`); and that the output's origin
claim is synthesized wherever a model made a sample (`origin_understated`).
- **Selective disclosure.** Steps are disclosed with inclusion proofs and prompts are salted
commitments; the outcome's tier says what was reached: `recomputed`, `checked` or `stated`.
- **Mappings.** One C2PA 2.4 actions statement per session, computed where every step is
disclosed (`c2pa.created` from an empty canvas, then one action per step behind the output,
with its software agent, its IPTC digital source type and its changed region); identity
assertions and live tracks, informative.
- **WAI decides no legal question.** The record states facts a reader applies its own rule to.
- **Conformance.** `live-conformance/contribution/`, written from fixed seeds by
`wai_contribution_vectors gen`, every reason of §11 refused at least once; `tests/contribution.rs`; and the second
implementation, `live-conformance/contribution_verify.py`, which runs the generator in its own
arithmetic after checking it against the generator's conformance vector, catches every
mutated copy of the corpus, and is caught by the corpus with each of its checks dropped in
turn. Audio maps by sample interval and video maps by frame are not part of this revision.
### `wai.ai.usage`: AI usage preferences bound to the bytes, and the agent's consumption claim
- **The extension** ([ai-usage](extensions/ai-usage.md)) registers the record `wai.ai.usage`.

View file

@ -44,8 +44,9 @@ Every registered string has one kind.
`wai.live.metering`, `wai.live.steering`, `wai.live.moderation`, `wai.live.e2ee`,
`wai.live.watermark`, `wai.live.ads` and `wai.live.provenance`; timed text,
`wai.text.timed`; the deriving edge's record, `wai.edge.derivation`; the origin claims of
media, `wai.media.synthesis`; AI usage preferences and consumption claims, `wai.ai.usage`).
It names no payload format.
media, `wai.media.synthesis`; AI usage preferences and consumption claims, `wai.ai.usage`;
the contribution records of media made by people and models together,
`wai.media.contribution`). It names no payload format.
A derivation or record string is not a payload capability, so it is not
dispatchable (SPEC §4): dispatch never takes an envelope's capability that names
@ -211,6 +212,7 @@ and a role are fixed once registered.
| `wai.live.room` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.live.steering` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.live.watermark` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.media.contribution` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.media.synthesis` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.mesh.draco` | payload | registered | volumetric | standalone | royalty-free | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR |
| `wai.mesh.glb` | payload | registered | volumetric | standalone | royalty-free | standard-unclassified | — | §5 Interactive, temporal & computed content |
@ -364,12 +366,15 @@ and a role are fixed once registered.
| `urn:wai:event:1` | scheme | defined | live-events §8.4 |
| `urn:wai:event:1` | catalog | defined | live-events §8.1 |
| `wai` | catalog | defined | moq-streaming-format §3 |
| `wai-authorship-map` | document-kind | defined | contribution §7.3 |
| `wai-c2pa` | catalog | defined | live-provenance §10.2 |
| `wai-contribution` | document-kind | defined | contribution §6 |
| `wai-derivation` | document-kind | defined | deriving-edge §5.1 |
| `wai-detection` | document-kind | defined | synthesis §7.3 |
| `wai-key-status` | document-kind | defined | jwp-receipts §7.9 |
| `wai-label-policy` | document-kind | defined | synthesis §10.1 |
| `wai-marking` | document-kind | defined | synthesis §7.2 |
| `wai-membership` | document-kind | defined | contribution §3.2 |
| `wai-module-grant` | document-kind | defined | jwp-receipts §7.9 |
| `wai-module-grants` | document-kind | defined | http-delivery §8.4 |
| `wai-origin` | document-kind | defined | synthesis §3.1 |
@ -377,6 +382,8 @@ and a role are fixed once registered.
| `wai-receipt-index` | document-kind | defined | http-delivery §8.2 |
| `wai-receipts` | catalog | defined | moq-streaming-format §4.1 |
| `wai-review` | document-kind | defined | synthesis §9.1 |
| `wai-session` | document-kind | defined | contribution §3.1 |
| `wai-step` | document-kind | defined | contribution §4.1 |
| `wai1` | codecs-prefix | defined | http-delivery §4.1 |
| `wai1` | fourcc | provisional | http-delivery Appendix B |
| `wai2` | codecs-prefix | defined | http-delivery §4.1 |

View file

@ -596,8 +596,9 @@ kind:
`wai.live.metering`, `wai.live.steering`, `wai.live.moderation`, `wai.live.e2ee`,
`wai.live.watermark`, `wai.live.ads` and `wai.live.provenance`, and timed text,
`wai.text.timed`; an edge's derivation record, `wai.edge.derivation`; the origin claims of
media, `wai.media.synthesis`; and AI usage preferences and consumption claims, `wai.ai.usage`),
which names no payload format.
media, `wai.media.synthesis`; AI usage preferences and consumption claims, `wai.ai.usage`; and
the contribution records of media made by people and models together,
`wai.media.contribution`), which names no payload format.
Only payload, companion and refinement capabilities have a media class, a
fidelity tier, a replicate class and a role. A derivation or record string is not a payload
@ -1161,6 +1162,27 @@ statements it saw and its grant; a verifier checks it against the objects, their
the room, and refuses a use they disallow (`usage_disallowed`), and a relay or gateway MAY require
claims before it serves an agent: [`extensions/ai-usage.md`](extensions/ai-usage.md).
And for **who contributed what** to media people and models make together —
`wai.media.contribution`: a tool signs a session naming its parties, people by key (each
countersigning its own membership, so no one is enrolled without its signature) and models by
digest, and every act is a signed step in a hash chain whose steps are the leaves of a Merkle
tree: a person authors or edits samples inside a region, a model generates, proposes candidates
or inpaints a region a person marked, a party selects one candidate, transforms a state with a
pinned derivation (deriving-edge's crop, rotation and mirror, synthesis's upscale and denoise, a
per-sample colour table), composes layers, or rejects an output. Each act has a dependency rule,
so re-executing the steps gives every sample of the output the parties whose act produced its
value (`made_by`) and those whose choices conditioned it (`directed_by`): an authorship map
computed, not declared, encoded with a palette and run-length rows and bound to the output's
samples by its digest. A verifier checks that an edit or an inpainting changed nothing outside
its region, that a selection is a member of its committed set and that every candidate
regenerates from its seed under `wai.generate.spectral`, that each transform recomputes, that the
map recomputes, and that the output's origin claim is synthesized wherever a model made any
sample. A creator may disclose some steps with inclusion proofs and keep prompts as salted
commitments; the outcome is `recomputed`, `checked` or `stated`, the shares are exact sample
counts per party, and a sample no disclosed, pinned rule fixes is `unknown` or `undisclosed`,
never credited to anyone. WAI decides no legal question: the record states facts a reader applies
its own rule to: [`extensions/contribution.md`](extensions/contribution.md).
### Payloads of the WAI-native integer codecs
This revision pins a payload format to each WAI-native integer codec whose row named

View file

@ -0,0 +1,777 @@
# WAI Extension: Contribution Records
> Status: Draft. Registers the record `wai.media.contribution`: who contributed what to a piece
> of media made by people and models together, and how much, computed from the process that
> made it. A tool signs a **session** naming its parties: people, each by a key and each
> countersigning its own membership, and models, each by its digest. Every act in the session
> is a signed **step** in a hash chain: a person authors or edits samples, a model generates,
> proposes candidates or inpaints a marked region, a party selects one candidate, transforms a
> state with a pinned derivation, composes layers, or rejects an output. Each act has a
> **dependency rule**, and re-executing the steps gives, for every sample of the output, the
> parties whose act produced its value (`made_by`) and the parties whose choices conditioned it
> (`directed_by`): the **authorship map**, computed rather than declared. A verifier checks
> what the steps say: that an edit or an inpainting changed no sample outside its region, that a
> selected output is a member of the set it was chosen from and that each candidate regenerates
> from its seed, that a transform recomputes, that the map recomputes, and that the output's
> origin claim ([synthesis](synthesis.md)) is at least what the map implies. The steps are the
> leaves of a Merkle tree, so a creator can disclose some of them with inclusion proofs; a
> prompt is a salted commitment until its creator opens it; and the outcome says which **tier**
> the disclosure reaches: `recomputed`, `checked` or `stated`. A sample whose label no disclosed,
> pinned rule fixes is **unknown** or **undisclosed**, never attributed to anyone, and the
> shares are sample counts per party, never one blended figure. No new transport. The reference
> implementation is the `contribution` module of `wai-rs` (feature `contribution`); the corpus
> is [`live-conformance/contribution/`](../live-conformance/contribution/), and its second
> implementation is `live-conformance/contribution_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision, each pinned to the version named: the C2PA Technical
> Specification, version 2.4 (April 2026), §10.2.3.2, §18.2, §18.15.1, §18.15.2 and §18.15.4;
> the Creator Assertions Working Group's Identity Assertion, version 1.2 (ratified 15 December
> 2025), §5.1, §5.1.2, §6.1 and §8; the IPTC Digital Source Type vocabulary as released on
> 2024-10-23; arXiv 2609.07711, §3.4; and *Copyright and Artificial Intelligence, Part 2:
> Copyrightability*, a report of the United States Copyright Office (January 2025), its table
> of contents and its Part V. A section number of one of those texts in this extension is that
> version's.
## 1. Scope
Process-level provenance that captures who contributed what, and to what extent, within a
co-creation session and across sessions, is named as an open research direction in arXiv
2609.07711 §3.4. WAI's derivations are exact, so a record of the process can be re-executed to
the artifact, and the map of who made each sample can be computed from it.
In scope: the records (§2) and the session with its parties (§3); the steps, the acts and their
pinned operations (§4); the chain of steps and its Merkle tree (§5); the contribution record
(§6); the authorship map, its dependency rules, its encoding and its shares (§7); disclosure
and the privacy of process (§8); agreement with an origin claim and the mappings onto C2PA
actions, identity assertions and live tracks (§9); verification (§10).
This revision maps still images of `RGB8` samples in full. Audio maps by sample interval and
video maps by frame are not part of it: a session's `media` is `image` (§3.1), and a later
revision registers the other media with their canonical buffers.
Out of scope, each a slot a later revision fills:
- **Legal questions.** WAI decides none. A registration authority, a collective or a court
applies its own rule, case by case, to facts. The cited report takes up, in turn, assistive
uses of AI systems, prompts, expressive inputs, and modifying or arranging AI-generated
content (its Part II), and concludes that outputs may be protected in whole or in part where a
person was able to determine the expressive elements they contain, and that prompts alone are
unlikely to suffice (its Part V). A contribution record states, sample by sample, which party's act produced
each value and which parties' choices conditioned it, and how much of that a verifier
recomputed. It does not say which of those facts make anyone an author anywhere. Registering
a work, and contacting any authority about it, are its owner's decisions.
- **Tooling and presentation** of the map (later revisions' tools).
- **Aggregating an audience's choices** into one act: an `audience` party may act alone in this
revision (§3.1), and a step on behalf of many is a later revision's.
- **Direction tracks**: the act `direct` is reserved (§4.2) and refused until registered.
- **Attribution through conditioning**: a generation conditioned on an earlier state, and what
an inpainting reads outside its region, attribute nothing here. `generate` and `candidates`
take no input in this revision (§4.2).
Relationship to the core spec: `wai.media.contribution` is a **record** ([SPEC](../SPEC.md)
§5): signed records a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
Relationship to [synthesis](synthesis.md): an origin claim says what an object is; a
contribution record says how it was made and by whom, sample by sample. The record names the
output's origin claim, and a verifier holds the two to each other (§9.1). This extension takes
synthesis §2's encodings and its two integer derivations by name.
Relationship to [deriving-edge](deriving-edge.md): a transform or a composition of this
extension evaluates one of that extension's operations (§4.2 there) by its name, its parameters
and its arithmetic, over states of a session rather than over delivered objects, with no
envelope written. A deriving edge's record chains published objects; a session's steps chain the
states of one piece of work.
## 2. Records
Every record of this extension is a JSON object (RFC 8259) under the encodings of
[jwp-receipts](jwp-receipts.md) §2.4.3, read as synthesis §2 reads a record: strict JSON, no
member named twice, at most 32 levels of nesting, byte strings in lowercase hexadecimal,
integers unsigned and at most 2^53 − 1, no other number, no boolean, **terms** and
**date-times** as defined there. Each record has exactly the members its section lists, no
member missing and none added, at any level, and a `kind` naming it; one that does not is
`record_malformed`. So is a member outside the values or ranges its section gives.
A signed record carries `signer_pubkey` (32 bytes, an Ed25519 key of RFC 8032) and `sig`
(64 bytes):
```
sig = Ed25519(signer_key, "wai:contribution\x01" || JCS(record without "sig"))
link_hash = BLAKE3("wai:contribution-link\x01" || JCS(record))
```
JCS is RFC 8785. The signature is verified strictly ([SPEC](../SPEC.md) §7 *Signatures*): a key
that fails step 1 or 2 is `bad_key`, and a signature that fails a later step `bad_signature`. A
record is named by its link hash wherever another names it. `issued_at` holds the instant its
signer signed it, self-asserted.
A **state** is a picture a step produces: `width` × `height` pixels as the `RGB8` buffer of
[reconstruction-binding](reconstruction-binding.md) §2 (row-major, three bytes a pixel, no row
padding), each side at least 1 and the buffer at most 2^26 bytes. Its **samples digest** is
SHA-256 (FIPS 180-4) over the buffer, and a state is named by it and by the step that produced
it. A pixel is addressed (*x*, *y*), *x* to the right and *y* down from the top-left. A
**sample** of the map is a pixel.
## 3. The session
### 3.1 The session record
The tool a session runs in signs a **session** record, `kind` `"wai-session"`:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-session"` |
| `session` | 32 B hex | fresh random bytes naming the session |
| `tool` | term | the tool, as a C2PA `softwareAgent` names it (§9.2) |
| `media` | string | `image` |
| `parties` | array | 1 to 64 parties, each named by its position from 0 |
| `issued_at` | date-time | when the tool signed it |
| `signer_pubkey`, `sig` | | §2: the **tool's key** |
A party is one of:
- `{"role": "human", "key"}`: a person, by the Ed25519 key it signs with (32 bytes; as a
`did:key` it is the same key);
- `{"role": "audience", "key"}`: a person in an audience, by its key;
- `{"role": "model", "digest", "type"}`: a model, by its prior's digest (SPEC §3.1), or, for
a generator that reads one model file and names it by SHA-256 (SPEC §5 *Payload of
`wai.generate.spectral`*), by that file's SHA-256; and `type`, `trained` or `algorithmic`
as synthesis §3.1 has a model's type.
A human or an audience party is a **keyed** party. No key appears twice, no digest twice, and
no keyed party's key is the tool's (`record_malformed`), so the tool's signature never stands
for a person's.
### 3.2 Membership
A keyed party **countersigns** its membership: it signs a **membership** record, `kind`
`"wai-membership"`, over the session as the tool signed it:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-membership"` |
| `session` | 32 B hex | the link hash of the session record |
| `party` | integer | its own position among the parties |
| `issued_at` | date-time | when it signed it |
| `signer_pubkey`, `sig` | | §2: the party's key |
A membership whose `session` is not the session's link hash, whose `party` is not a keyed
party, or whose signer is not that party's key is `membership_invalid`. A keyed party with no
membership is `party_not_countersigned`: no person is enrolled as a party of a session, and so
as a maker or a director of any sample (§7), without its own signature over the session's
parties. A model party signs nothing; the tool signs its acts (§4.1).
## 4. Steps
### 4.1 The step record
Each act is a **step**, `kind` `"wai-step"`:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-step"` |
| `session` | 32 B hex | the link hash of the session record |
| `seq` | integer | the step's position in the session, from 0 |
| `prev` | 32 B hex or `null` | the link hash of step `seq` − 1; `null` for step 0 |
| `party` | integer | the party that performed the act (the **performer**) |
| `act` | string | §4.2 |
| `inputs` | array | 0 to 16 states, each `{"step", "samples"}`: the `seq` of the step that produced it, below this step's `seq`, and its samples digest; none twice |
| `output` | object or `null` | what the act produced (§4.2) |
| `region` | array or `null` | for `edit` and `inpaint`, 1 to 256 rectangles `{"x", "y", "w", "h"}`, `w` and `h` at least 1, each within the output (`x` + `w` ≤ `width`, `y` + `h` ≤ `height`); the **region** is their union. `null` for every other act |
| `params` | object | §4.3 |
| `check` | string | `replicate` or `attest` (§4.4) |
| `issued_at` | date-time | when it was signed |
| `signer_pubkey`, `sig` | | §2: the performer's key for a keyed performer, the tool's key for a model |
An `output` is `{"samples", "width", "height"}`, one state; for `candidates` it is `{"set",
"width", "height"}`, a set of 2 to 16 states of one size by their samples digests, none twice;
for `reject` it is `null`.
A step whose `session` is not the session's link hash is `session_mismatch`; whose `party`, or a
party its `params` name, is not a party of the session, `party_unknown`; whose performer's role
is not one its act admits (§4.2), `party_role_mismatch`; and whose signer is not the one this
section names, `signer_mismatch`.
### 4.2 Acts
| act | performer | inputs | output | region | what it introduces |
|---|---|---|---|---|---|
| `author` | `human` | none | one state | none | every sample: the person's own |
| `generate` | `model` | none | one state | none | every sample |
| `candidates` | `model` | none | a set | none | every sample of every member |
| `select` | any party | one: a member of a `candidates` step's set | one state: the input | none | nothing |
| `edit` | `human` | one | one state of the input's size | required | the region's samples: the person's own |
| `inpaint` | `model` | one | one state of the input's size | required | the region's samples |
| `transform` | any party | one | one state | none | nothing |
| `compose` | any party | 1 to 16 | one state | none | the canvas fill (§4.5) |
| `reject` | any party | one | none | none | nothing |
| `direct` | reserved | | | | |
- **author**: samples a person made enter the session: a drawing, a recording, typed text set
as an image.
- **generate**: a model produces a state.
- **candidates**: a model produces a set of states, committed by their digests and, where the
generator is pinned, by their seeds.
- **select**: a party picks one member of a committed set. Its output is that member.
- **edit**: a person replaces samples inside the region with samples it made.
- **inpaint**: a model fills the region a party marked.
- **transform**: a derivation, with parameters a party chose, maps one state to another.
- **compose**: a party places layers on a canvas, or tiles them in a grid.
- **reject**: a party discards a state. The state stays committed, so a verifier sees what was
discarded as well as what was kept.
- **direct**: reserved for direction tracks. A step whose `act` is `direct` is `act_reserved`.
A step that breaks the table's inputs, output or region is `record_malformed`; an input of a
`select` that is not a member of a `candidates` step's set, or a `select` whose output is not its
input, is `selection_not_in_set` (§10.2 step 6).
### 4.3 Parameters
A step's `params` has exactly these members:
| act | `params` |
|---|---|
| `author` | `{"source"}`: what the person made, as synthesis §3.2 has a capture state it: `digitalCapture`, `computationalCapture`, `screenCapture` or `digitalCreation` |
| `generate` | `{"generator", "prompt", "prompt_by", "seed", "steps"}` |
| `candidates` | `{"generator", "prompt", "prompt_by", "seeds", "steps"}` |
| `inpaint` | `{"generator", "prompt", "prompt_by", "mask_by", "seed", "steps"}` |
| `select`, `edit`, `reject` | `{}` |
| `transform`, `compose` | `{"derivation", "params"}`: a term naming the derivation, and an object, its parameters |
- `generator`: a term naming what generated: `wai.generate.spectral` (§4.5) or another.
- `prompt`: the salted commitment to the prompt (§8.1), 32 bytes, or `null` for none.
- `prompt_by`: the party that wrote the prompt; `null` exactly when `prompt` is.
- `mask_by`: the party that marked the region.
- `seed`, `steps`: integers or `null`; `seeds`: an array of one integer per member of the set,
in the set's order, or `null`.
### 4.4 The check
A step's `check` says whether every sample it carries forward is fixed by a pinned rule:
- `replicate`: every sample the act carries from its inputs recomputes from them under an
operation this revision pins (§4.5), and every sample a model introduces regenerates under a
pinned generator. Samples a person introduces (`author`, an `edit`'s region) are that person's
own, the source the chain starts from, as a capture is in synthesis §3.2: there is nothing to
recompute, and the signature says who made them.
- `attest`: a step that is not `replicate`: what it produced rests on its signer's statement.
| act | `check` |
|---|---|
| `author`, `edit`, `select`, `reject` | `replicate` |
| `generate`, `candidates`, `inpaint` | `replicate` when `generator` is `wai.generate.spectral`; `attest` otherwise |
| `transform` | `replicate` when `derivation` is one §4.5 pins for it; `attest` otherwise |
| `compose` | `replicate` when `derivation` is `wai.derive.overlay_composition` or `wai.derive.grid_composition`; `attest` otherwise |
A step whose `check` is not its act's is `check_mismatch`. A `replicate` step's parameters are
those §4.5 gives its operation; a `generate`, `candidates` or `inpaint` step of
`wai.generate.spectral` has a `seed` (each of its `seeds`) from 0 to 2^32 − 1 and `steps` from 1
to 64, and its output's sides are each at most 4096 with `width` · `height` at most 2^23, the
ranges SPEC §5 *Payload of `wai.generate.spectral`* gives an instruction object. Otherwise
`params_invalid`. An `attest` step's `params` under `derivation` is any object.
### 4.5 The pinned operations
Each operation computes its output from its inputs' samples and its parameters, and defines,
for each output pixel, the input pixels it reads: its **sample mapping**, which the map follows
(§7.2).
Transforms:
- `wai.derive.crop` `{"x", "y", "w", "h"}`, `wai.derive.rotation` `{"quarter_turns"}` and
`wai.derive.mirror` `{"flip"}`: deriving-edge §4.2's, its parameters and its arithmetic
exactly. Each output pixel reads the one input pixel that section copies to it.
- `wai-synthesis:upscale2` and `wai-synthesis:denoise3`, `{}`: synthesis §4.1 and §4.2. Output
pixel (2*x*, 2*y*) of the upscale reads in(*x*, *y*); (2*x* + 1, 2*y*) reads it and
in(*x*₁, *y*); (2*x*, 2*y* + 1) reads it and in(*x*, *y*₁); (2*x* + 1, 2*y* + 1) reads those
and in(*x*₁, *y*₁), with *x*₁ and *y*₁ as there. Each output pixel of the denoise reads the nine
clamped pixels of its neighbourhood.
- `wai-contribution:lut` `{"table"}`, `table` 768 bytes: out(*x*, *y*) byte *c* = `table`[256 ·
*c* + in(*x*, *y*) byte *c*], for *c* 0, 1, 2. Each output pixel reads the input pixel at its
own position. It is a value transform: a colour grade, a tone curve, a levels adjustment. No
other WAI text pins a per-sample colour mapping, so this extension pins this one.
Compositions, over the step's inputs in its order:
- `wai.derive.overlay_composition` `{"width", "height", "fill", "at"}` and
`wai.derive.grid_composition` `{"columns", "rows"}`: deriving-edge §4.2's. A pixel an input
covers reads the pixel of the last input that covers it; a pixel of an overlay's canvas that no
input covers reads nothing and holds the `fill`.
Generation, for `generate`, `candidates` and `inpaint`:
- `wai.generate.spectral`: the reference generator SPEC §5 *Payload of `wai.generate.spectral`*
names, `generate(model, prompt, seed, steps, h, w)`, with the performer's model file (whose
SHA-256 is the party's `digest`), the opened prompt's text (§8.1), the empty string where
`prompt` is `null`, and `h` and `w` the output's. A `candidates` member at position *k* of the
set is made with `seeds`[*k*]. An `inpaint` makes the picture at the input's size and copies its
samples inside the region; outside the region, the output is the input.
A parameter the inputs cannot satisfy (a crop outside its input, grid inputs of different
sizes, an `at` entry missing) is `params_invalid` when the inputs' sizes are known (§10.2 step
6).
## 5. The chain and its tree
### 5.1 The chain
A session's steps are numbered from 0 by `seq`, and each names the link hash of the one before
it in `prev`: a hash chain, so no step can be removed, inserted or reordered without breaking
every link after it. A step whose `seq` is not below the record's `steps` (§6), a `seq` given
twice, a step 0 whose `prev` is not `null`, a later step whose `prev` is `null`, or a `prev`
that is not the link hash of step `seq` − 1 where the verifier holds that step, is
`chain_broken`.
### 5.2 The tree
The steps are the leaves of a binary Merkle tree, built as jwp-receipts §2.2 builds a group's,
over the steps' link hashes in `seq` order, with domains of this extension's own so that the
root of a session's steps can never be read as a group's:
```
leaf(step) = BLAKE3("wai:contribution-leaf\x01" || link_hash(step))
node(left, right) = BLAKE3("wai:contribution-node\x01" || left || right)
```
At each level with an odd number of nodes the last is duplicated; the root is the single node
left. The duplicated last leaf also walks to the root from the position past it, so the record
states the number of leaves, `steps`, and §5.1 refuses a `seq` that is not below it: no step can
stand at a position the tree does not have.
A disclosed step carries an **inclusion proof**: its siblings from the leaf level upward,
exactly ⌈log₂ `steps`⌉ of them (none for one step). It is walked as jwp-receipts §3 step 5
walks an object's proof, with the leaf index the step's `seq` and the direction bits the bits of
`seq`: at level *i*, bit *i* of `seq` set means the current node is the right child. A proof of
another length, or one that does not reach the record's `root`, is `inclusion_failed`.
## 6. The contribution record
The tool signs the **contribution** record, `kind` `"wai-contribution"`:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-contribution"` |
| `session` | 32 B hex | the link hash of the session record |
| `steps` | integer | the number of steps, at least 1 |
| `root` | 32 B hex | the root of the steps' tree (§5.2) |
| `output` | object | the final output: `{"step", "samples", "width", "height", "content_hash"}`, the `seq` of the step that produced it (below `steps`), its samples digest and size, and the content hash (jwp-receipts §2.1) of the object it was published as, or `null` |
| `map` | 32 B hex | the digest of the authorship map (§7.3) |
| `origin` | 32 B hex or `null` | the link hash of the output's origin claim (synthesis §3.1); `null` only with `content_hash` `null` too, or where the tool signs none |
| `estimates` | array | 0 to 16 estimated shares (below) |
| `issued_at` | date-time | when the tool signed it |
| `signer_pubkey`, `sig` | | §2: the tool's key |
A record that is not the session's is `session_mismatch`; one not signed by the tool's key,
`signer_mismatch`; one whose `origin` is not `null` while `content_hash` is, `record_malformed`.
An **estimate** is `{"estimator", "class", "party", "basis", "share_ppm"}`: a term naming the
method, a term naming its class (the family of methods it belongs to, such as an
information-theoretic one), a party (`party_unknown` otherwise), `made_by` or `directed_by`, and
the share it estimates in parts per million, at most 1 000 000. An estimate is the tool's
statement with its class, as an energy figure carries its class: a verifier reports it as
stated, beside the counts of §7.4 and never in place of them, and never adds it to them.
## 7. The authorship map
### 7.1 Labels
Every sample of the output has a **label**: two sets, `made_by`, the parties whose act produced
the sample's value, and `directed_by`, the parties whose choices conditioned it (a prompt, a
mask, a selection, a transform's parameters, an arrangement). A set holds parties, and may hold
one of two markers:
- `unknown`: no pinned rule fixes where the sample's value came from, even with every step
disclosed: an `attest` transform or composition does not pin its sample mapping;
- `undisclosed` (in a verifier's computed map only, never in a stated one): the sample comes
from a state whose step the verifier does not hold.
A marker is never a party and is never counted as one: **unknown is never zero**. A sample a
rule cannot attribute is reported as such, not credited to anyone.
### 7.2 Dependency rules
The map of a state is computed from its step and its inputs' maps. The map of an input whose
step the verifier does not hold has every sample `({undisclosed}, {undisclosed})`. For a step
performed by party *a*:
| act | label of each sample of the output |
|---|---|
| `author` | `({a}, {})` |
| `generate`; each member of `candidates` | `({a}, P)`, *P* the set of `prompt_by`, where it is not `null` |
| `edit` | inside the region `({a}, {})`; outside it, the input's label at the same pixel |
| `inpaint` | inside the region `({a}, P ∪ {mask_by})`; outside it, the input's label at the same pixel |
| `select` | the input's label at the same pixel, with *a* added to `directed_by` |
| `transform`, `replicate` | the union, over the input pixels its sample mapping reads (§4.5), of their `made_by`, and of their `directed_by` with *a* added |
| `compose`, `replicate` | at a pixel an input covers, that input pixel's label with *a* added to `directed_by`; at a canvas pixel no input covers, `({a}, {a})`: the fill is the arranger's |
| `transform` or `compose`, `attest` | `({unknown}, {unknown})` |
| `reject` | no output |
So a geometric transform carries each label through its exact sample mapping; a filter that
mixes pixels takes the union of what it mixes; a value transform keeps `made_by` and adds its
chooser to `directed_by`; a selection keeps the chosen candidate's map and adds the selector
everywhere; a composition takes each layer's map where the layer is visible. A model's samples,
generated or inpainted, are `made_by` that model whether or not its generator is pinned: the act
places them, and the step's status (§10.3) says whether they were regenerated. An `edit`'s or an
`inpaint`'s region takes nothing from the samples it replaced: they no longer condition it.
Where the verifier does not know an input's size (its step is not held), the sample mapping
cannot be followed; a `transform` or `compose` then gives every output sample `({undisclosed},
{undisclosed, a})`, and a `select` gives the input's label as above. The output's map is the map
of the state the record's `output` names: the one its `step` produced.
### 7.3 Encoding
The map is a JSON object under §2's encodings, `kind` `"wai-authorship-map"`, unsigned and
bound by its digest:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-authorship-map"` |
| `output` | 32 B hex | the samples digest of the output |
| `width`, `height` | integer | the output's size |
| `palette` | array | the distinct labels, each `{"made_by", "directed_by"}`, in the order each first occurs in row-major order |
| `rows` | array | `height` rows, each an array of runs `[i, n]`: `n` ≥ 1 pixels with palette label `i`, left to right, summing to `width`; adjacent runs never share a label |
A set is an array of party positions in ascending order, then `"unknown"` where it holds that
marker; a stated map holds no `"undisclosed"`. Every party is a party of the session. A map
that is not one JSON text under §2's encodings, or that breaks any of these rules, is
`map_malformed`: each map has one encoding.
```
map digest = BLAKE3("wai:contribution-map\x01" || JCS(map))
```
The record names the map by its digest, and the map names the output's samples digest and
size, so the map is bound to the samples it describes. A map whose digest is not the record's
`map`, or whose `output`, `width` or `height` is not the record's output's, is `map_unbound`.
### 7.4 Shares
The **shares** of a map are exact sample counts: the number of samples; for each party, the
number of samples whose `made_by` holds it, and separately the number whose `directed_by` holds
it; and, for each of the two sets, the number of samples holding `unknown` and the number holding
`undisclosed`. A sample counts once for each party its set holds, so a sample two parties made
counts for both. The counts are never combined into one figure: what `made_by` and
`directed_by` mean is a reader's to weigh under its own rule.
## 8. Disclosure
### 8.1 Salted commitments
A prompt, and any other text a party gives a step, is carried as a salted commitment:
```
commitment = SHA-256(salt || UTF-8(text))
```
with `salt` 32 fresh random bytes for each commitment. A short or guessable prompt cannot be
recovered by hashing candidates: the salt is not disclosed with the step. An **opening**
`{"commitment", "salt", "text"}` discloses one: the salt (32 bytes) and the text (a string).
An opening whose salt and text do not hash to its commitment is `opening_mismatch`.
### 8.2 What each disclosure gives
A creator discloses some or all of the steps, each with its inclusion proof, and some or all of
the states, the openings of the prompts and the model files a pinned generator reads. The tier
of the outcome (§10.3) follows from what is disclosed:
| disclosed | tier |
|---|---|
| every step behind the output, every state they read and write, the openings of their prompts and the model files of their pinned generators, no `attest` step behind the output | `recomputed` |
| as above with an `attest` step behind the output, or with a state, an opening or a model file withheld | `checked`: what was held was checked, the rest is committed |
| some steps, with their proofs: the others committed by the root | `checked`, and the samples from undisclosed steps `undisclosed` |
| the record and the map, no step | `stated`: the signed map is held, nothing recomputed |
A withheld prompt stays private: its commitment binds the step to it, and the step's map does
not depend on its text. A withheld step stays private too: its leaf is in the root, so it cannot
be replaced later, and the samples it produced are `undisclosed`, never credited to anyone.
## 9. Agreement and mappings
### 9.1 The origin claim
Where the record names an origin claim (synthesis §3.1) the verifier holds, read by synthesis
§2 and §3.1:
- its subject is the object `output.content_hash` (`origin_mismatch`);
- where any sample of the stated map is `made_by` a model party, its `class` is `synthesized`
(`origin_understated`): an object any part of which a model generated or inpainted is not
captured, reconstructed or assistive, as synthesis §3.2's lattice has it.
### 9.2 The C2PA actions
Where every step is disclosed, the record gives one C2PA statement, `{"actions": A}`, written
in JCS: `A` is the actions of the output's manifest (C2PA 2.4 §18.15.1), one per step behind the
output, in `seq` order, after a first `c2pa.created` action:
- first: `{"action": "c2pa.created", "digitalSourceType":
"http://c2pa.org/digitalsourcetype/empty", "softwareAgent": {"name": T}}`, `T` the session's
`tool`: a session is begun on an empty canvas in a tool, and C2PA 2.4 §18.15.2 asks for
`c2pa.created` first with that source type;
- then, for each step, the action below with `"softwareAgent": {"name": N}`, `N` the tool for a
keyed performer and `tag:transaction.science,2026:wai/model/` followed by the digest in
lowercase hexadecimal for a model (synthesis §6's model identifier), a generator-info map
(C2PA 2.4 §10.2.3.2) with its `name`:
| step | action | `digitalSourceType` (the IPTC term of) | `changes` |
|---|---|---|---|
| `author` | `c2pa.edited` | its `source` | none |
| `generate`, `candidates` | `c2pa.edited` | `trainedAlgorithmicMedia` for a trained model, `algorithmicMedia` otherwise | none |
| `inpaint` | `c2pa.edited` | `compositeWithTrainedAlgorithmicMedia` for a trained model, `algorithmicMedia` otherwise | the region |
| `edit` | `c2pa.drawing` | `digitalCreation` | the region |
| `transform`: crop | `c2pa.cropped` | none | none |
| `transform`: rotation, mirror | `c2pa.orientation` | none | none |
| `transform`: upscale2 | `c2pa.resized` | `algorithmicallyEnhanced` | none |
| `transform`: denoise3 | `c2pa.enhanced` | `algorithmicallyEnhanced` | none |
| `transform`: lut | `c2pa.adjustedColor` | none | none |
| `transform`: `wai.derive.scaling` | `c2pa.resized` | none | none |
| another `transform`, a `compose` | `c2pa.edited` | none | none |
| `select` | none: it changes no sample | | |
An action's members are written in the order JCS gives them. `changes` (C2PA 2.4 §18.15.4.6) is
one region map (§18.2): `[{"region": R}]`, `R` one spatial range for each rectangle, in the
step's order, `{"type": "spatial", "shape": {"type": "rectangle", "unit": "pixel", "origin":
{"x": X, "y": Y}, "width": W, "height": H}}`, the coordinates as integers (a manifest in CBOR
writes them as the floats §18.2.3 types them). The IPTC term is
`http://cv.iptc.org/newscodes/digitalsourcetype/` followed by the code. The trained models'
`c2pa.ai-disclosure` assertions are the origin claim's (synthesis §6). A rejected state, and a
branch of the session the output does not come from, give no action: they did not make the
asset. Embedding the statement in a manifest, and signing it, is C2PA's.
### 9.3 Identity assertions (informative)
A keyed party may give the output's manifest an identity assertion (Identity Assertion 1.2
§5.1, label `cawg.identity`) whose referenced assertions include the actions assertion of §9.2,
as §6.1 there recommends for an actor who performed those actions, under a credential of §8
there. The roles of §5.1.2 there (`cawg.creator`, `cawg.contributor`, `cawg.editor`, …) are the
party's own statement of its relationship to the asset; this extension does not derive one
from the shares. The party's key in the session is an Ed25519 key, not a credential type of §8
there, and the relation between the two is the party's to state.
### 9.4 Live tracks (informative)
A record covers one still. Over [live-provenance](live-provenance.md), a manifest that covers
such a still (a picture a room shares, a frame made in a session and then streamed) can name the
record by its link hash in an entity-specific assertion of the kind that extension's §5 defines
for the stream, the group and the take; the record, its session, its memberships, its disclosed
steps and its map are then offered by the carriage that offers the track's receipts, as
deriving-edge §6 offers a chain's records. Per-frame maps of a live track are a later
revision's.
## 10. Verification
### 10.1 Inputs
A verifier is given: the session record; its memberships, in a list; the contribution record;
the disclosed steps, in a list, each with its inclusion proof; the authorship map; the openings
it holds, in a list; the states it holds, each a buffer, named by its samples digest; the model
files it holds, each named by its SHA-256; the origin claims it holds, in a list; and a
**policy** of one member:
| member | values | default | meaning |
|---|---|---|---|
| `tier` | `stated`, `checked`, `recomputed` | `stated` | the lowest tier it accepts (`tier_not_met`) |
### 10.2 Steps of verification
The verifier runs these steps and stops at the first refusal, which it reports with its reason
and its place: `["session"]`, `["session", "party", i]`, `["membership", i]`, `["record"]`,
`["step", i]`, `["map"]`, `["map", x, y]`, `["opening", i]`, `["origin", i]` or `["policy"]`,
each *i* a position in its list (a party's among the parties) and (*x*, *y*) a pixel.
1. **Reading** (§2, §3, §4.1–§4.3, §6, §7.3, §8.1): the session, the memberships, the record,
the steps, the map and the openings, each in order, read and their signatures verify; the
origin claims read and verify by synthesis §2 and §3.1. `record_malformed`, `bad_key`,
`bad_signature`, `act_reserved`, `map_malformed`. A step whose `act` is `direct` is
`act_reserved` once its members are read, whatever they hold. A proof that is not an array
of 32-byte strings is `record_malformed`.
2. **Openings** (§8.1): `opening_mismatch`.
3. **Membership** (§3.2): each membership in order (`membership_invalid`), then each keyed
party in order (`party_not_countersigned`).
4. **The record** (§6): `session_mismatch`, `signer_mismatch`, and each estimate's party
(`party_unknown`).
5. **Each step**, in the order given: `session_mismatch`, `party_unknown`,
`party_role_mismatch`, `signer_mismatch` (§4.1); `check_mismatch` and `params_invalid`
(§4.4); `chain_broken` (§5.1); `inclusion_failed` (§5.2).
6. **Each step against the states**, in `seq` order, each by these checks in order:
1. each state its output names that the verifier holds is `width` · `height` · 3 bytes
(`samples_mismatch`);
2. each input whose step the verifier holds is that step's output, or a member of its set
(`input_unknown`); for a `select`, instead: its output is its input, and where the
verifier holds the input's step, that step is a `candidates` step and the input is a
member of its set (`selection_not_in_set`);
3. for an `edit` or an `inpaint` whose input's step is held, that step's output has this
step's output's size (`region_invalid`);
4. for a `replicate` `transform` or `compose` whose inputs' sizes are known (the verifier
holds each input's step): its parameters hold over them (§4.5, `params_invalid`), and
the size they give is the output's (`recompute_mismatch`);
5. the act's own check, where the verifier holds what it reads:
- `edit`, `inpaint`: a held buffer of the input that is not of the output's size is
`region_invalid`; with the input's and the output's samples held, every pixel outside
the region is the input's, byte for byte (`outside_region_changed`);
- `generate`, `candidates`, `replicate`: with the model file and the prompt's opening
(where `prompt` is not `null`) held, each state regenerates (§4.5): the samples digest of
what the generator makes is the state's (`regeneration_mismatch`);
- `inpaint`, `replicate`: with the model file, the prompt's opening and the output's
samples held, every pixel inside the region is the generator's picture's
(`regeneration_mismatch`);
- `transform`, `compose`, `replicate`: with every input's samples held, the operation
evaluated over them gives the output's size and samples whose digest is the output's
(`recompute_mismatch`).
7. **The output** (§6): where the verifier holds the step `output.step`, that step's output is
one state with the record's samples digest and size (`output_mismatch`); a held buffer of the
output's digest is `width` · `height` · 3 bytes (`samples_mismatch`).
8. **The map** (§7): the map is bound (`map_unbound`). The verifier computes the output's map by
§7.2 from the steps it holds and compares it with the stated map, pixel by pixel in row-major
order, `made_by` then `directed_by`. Where the computed set holds `undisclosed`, the parties
and the `unknown` it holds besides are in the stated set; otherwise the two sets are equal.
At the first pixel where that fails: `unknown_attributed` where the computed set holds
`unknown` and the stated set does not (a sample no pinned rule fixes, credited to a party);
otherwise `map_mismatch`.
9. **The origin claim** (§9.1): `origin_mismatch`, `origin_understated`, at the claim's place.
10. **Policy**: an outcome below the policy's `tier` is `tier_not_met`.
### 10.3 Statuses and tiers
Each step the verifier holds has a **status**:
- `attested`: its `check` is `attest`;
- `checked`: its `check` is `replicate`, the verifier holds the step of each of its inputs, and
each check of step 6.5 its act has run (an `author` and a `reject` have none, a `select`'s is
step 6.2's against the held `candidates` step, and an `edit` needs both of its states held,
an `inpaint` its states, its model file and its opening);
- `not-checked`: otherwise.
The steps **behind the output** are the step `output.step` and, recursively, the step of each
input of a step behind the output. The outcome's **tier**:
- `recomputed`: every step behind the output is held, and `checked`;
- `stated`: the verifier holds no step;
- `checked`: otherwise.
### 10.4 What an accepted case states
- `tier` (§10.3);
- `session` and `record`: the link hashes of the session record and the contribution record;
- `steps`: each held step in `seq` order, its `seq`, `act`, `party`, `check` and `status`;
- `committed`: the number of steps not held;
- `computed` and `stated`: the shares (§7.4) of the computed map and of the stated map, each
`samples`; `made_by` and `directed_by`, each a list of `{"party", "samples"}` for the parties
with a count above zero, in ascending order of party; and `made_by_unknown`,
`made_by_undisclosed`, `directed_by_unknown` and `directed_by_undisclosed`;
- `estimates`: the record's, as stated;
- `origin`: `agree` where the claim the record names was held and checked, `not-checked` where
it was not held, `null` where the record names none;
- `c2pa`: the statement of §9.2 as its JCS text, where every step is held; otherwise `null`.
## 11. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `record_malformed` | §2, §3.1, §4.1, §4.2, §4.3, §6, §8.1, §10.2 step 1 |
| `bad_key` | §2 |
| `bad_signature` | §2 |
| `act_reserved` | §4.2 |
| `map_malformed` | §7.3 |
| `opening_mismatch` | §8.1 |
| `membership_invalid` | §3.2 |
| `party_not_countersigned` | §3.2 |
| `session_mismatch` | §4.1, §6 |
| `party_unknown` | §4.1, §6 |
| `party_role_mismatch` | §4.1 |
| `signer_mismatch` | §4.1, §6 |
| `check_mismatch` | §4.4 |
| `params_invalid` | §4.4, §4.5, §10.2 step 6 |
| `chain_broken` | §5.1 |
| `inclusion_failed` | §5.2 |
| `samples_mismatch` | §10.2 steps 6 and 7 |
| `input_unknown` | §10.2 step 6 |
| `selection_not_in_set` | §4.2, §10.2 step 6 |
| `region_invalid` | §10.2 step 6 |
| `outside_region_changed` | §10.2 step 6 |
| `regeneration_mismatch` | §4.5, §10.2 step 6 |
| `recompute_mismatch` | §4.5, §10.2 step 6 |
| `output_mismatch` | §10.2 step 7 |
| `map_unbound` | §7.3 |
| `map_mismatch` | §10.2 step 8 |
| `unknown_attributed` | §7.1, §10.2 step 8 |
| `origin_mismatch` | §9.1 |
| `origin_understated` | §9.1 |
| `tier_not_met` | §10.1 |
## 12. Conformance
[`live-conformance/contribution/`](../live-conformance/contribution/) holds the corpus, in one
file, `vectors.json`, written from fixed seeds by `wai_contribution_vectors gen`. It holds the
model file of `wai.generate.spectral` its sessions generate with, and the **vectors**: each a
case of §10.1, its outcome and, accepted, everything §10.4 states; refused, its reason and its
place. The exact generator of the corpus is `wai.generate.spectral`, with the model file of its
conformance vector (`wai-rs/tests/vectors/generate/generate_spectral_model.json`). It has:
- **Accepted, `recomputed`**: a person's drawing, a collaborator's brush edit inside a region,
and a crop, with a captured origin claim that agrees; an inpainting by the pinned generator
inside a region one person marked, from a prompt another wrote, regenerated, with a
synthesized origin claim that agrees; three candidates, each regenerated from its seed, one
selected by an audience member, one rejected, then a colour table applied; a generated layer
composed over a drawing and a fill; and a chain of transforms (rotation, mirror, upscale,
denoise, crop), each recomputed.
- **Accepted, `checked`**: an inpainting by a trained model the tool attests, its outside
region checked; an attested scaling whose output is `unknown`; the candidates session with its
generating step undisclosed, its samples `undisclosed`; and the same with every step disclosed
but the prompts withheld, so no candidate is regenerated.
- **Accepted, `stated`**: the record and the map, no step; and a record that attaches an
estimate with its class.
- **Refused**: each reason of §11 at least once, each a control of one defect, and a second
control for each check that has a second place in §10.2: among them a sample outside an
edit's region changed, a selected output not in its set, a candidate and an inpainted region
that do not regenerate, a map that does not recompute, a gap in the chain and a `prev` that
skips its predecessor, a step by a party the session does not have, a person enrolled without
countersigning, an attested region credited to a person instead of `unknown`, an origin claim
of a lower class than the map implies, and a prompt commitment that does not open.
A conforming verifier gives every vector the outcome, the reason, the place and the statements
the corpus states. `wai_contribution_vectors verify` and the test `tests/contribution.rs` check
that the file regenerates byte for byte, that `wai-rs` gives every vector its outcome, and that
§11 lists exactly the reference's codes. `live-conformance/contribution_verify.py` is the second
implementation, written from this text: it shares no code with `wai-rs`; it computes every map
itself, evaluates every pinned operation and runs the generator in its own arithmetic, checking
that arithmetic first against the generator's conformance vector
(`wai-rs/tests/vectors/generate/sha256.json`); it reads origin claims with the synthesis
corpus's second implementation (`synthesis_verify.py`), verifies every signature with WAI's
strict rule (`claim-conformance/_ed25519.py`) and every link hash with BLAKE3 written from its
specification (`claim-conformance/_blake3.py`); it mutates copies of the corpus and requires
every copy to fail; and it drops each of its own checks in turn and requires the corpus to catch
the weakened verifier.
## 13. Security and privacy considerations
- **What is checked and what is stated.** A step is signed by the party that performed it, or by
the tool for a model. The samples a person introduces are that person's statement: WAI checks
everything that happened to them afterwards, not whether the person made them without a model.
An `author` step's `source` says what the person made, as a capture's claim does. A model's
samples from an `attest` generator are the tool's statement; from a pinned generator they are
regenerated, so the step proves exactly what was generated, and that no person painted it.
- **The region.** An edit or an inpainting is checked, not claimed, to have changed nothing
outside its region: a verifier holding the two states compares every pixel. What a model's
fill read from outside the region conditions it, and is not attributed in this revision.
- **Taste as evidence.** A `candidates` step commits every candidate by digest and seed, and a
`reject` keeps what was discarded committed, so a verifier that regenerates the set sees what
was chosen over what.
- **Withholding.** A creator who discloses part of a session cannot change the rest later: the
record's root commits to every step. A withheld step's samples are `undisclosed` and a
withheld prompt is a salted commitment; neither is ever credited to a party. A creator who
withholds can only lower the tier the outcome reaches.
- **Enrolment.** A person is a party only by countersigning the session, so a tool cannot credit
a person, or blame one, without that person's signature over the whole list of parties. A
model party signs nothing; its acts are the tool's statements about it.
- **Not a judgment.** A record states facts sample by sample; it ranks no contribution, and its
counts are exact where a rule fixes them and marked where none does. An estimate is the tool's,
with its class, and is never mixed into the counts. What any of it means for authorship is
decided elsewhere, by others, under their own rules.
- **Privacy of process.** Prompts are committed with a salt; a session names parties by keys and
digests, never by name; the states are digests unless disclosed. A disclosed state is a
picture, and a creator discloses what it chooses.

View file

@ -20,6 +20,7 @@ and the independent verifier of each, beside it.
| [`c2pa-live/`](c2pa-live/) | [live-provenance](../extensions/live-provenance.md) (`wai.live.provenance`) | `c2pa_live_verify.py` |
| [`synthesis/`](synthesis/) | [synthesis](../extensions/synthesis.md) (`wai.media.synthesis`) | `synthesis_verify.py` |
| [`usage/`](usage/) | [ai-usage](../extensions/ai-usage.md) (`wai.ai.usage`) | `ai_usage_verify.py` |
| [`contribution/`](contribution/) | [contribution](../extensions/contribution.md) (`wai.media.contribution`) | `contribution_verify.py` |
**Provenance.** Generated here: `wai_live_events_vectors gen` writes `events/`,
`wai_live_room_vectors gen` writes `rooms/`, `wai_capture_take_vectors gen` writes
@ -29,12 +30,15 @@ writes `recordings/`, `wai_timed_text_vectors gen` writes `captions/` and
`steering/`, `wai_live_moderation_vectors gen` writes `moderation/`,
`wai_live_watermark_vectors gen` writes `watermark/`, `wai_live_ads_vectors gen` writes
`ads/`, `wai_live_provenance_vectors gen` writes `c2pa-live/`, `wai_synthesis_vectors gen`
writes `synthesis/` and `wai_ai_usage_vectors gen` writes `usage/`, each from records authored
in code, from fixed seeds (`events/README.md`, `rooms/README.md`, `takes/README.md`,
writes `synthesis/`, `wai_ai_usage_vectors gen` writes `usage/` and
`wai_contribution_vectors gen` writes `contribution/`, each from records authored in code, from
fixed seeds (`events/README.md`, `rooms/README.md`, `takes/README.md`,
`compose/README.md`, `recordings/README.md`, `captions/README.md`, `metering/README.md`,
`steering/README.md`, `moderation/README.md`, `watermark/README.md`, `ads/README.md`,
`c2pa-live/README.md`, `synthesis/README.md`, `usage/README.md`); `c2pa-live/` cuts its media
from the packaging corpus's AV1 floor. `joule_moq_e2ee_vectors gen` (in
`c2pa-live/README.md`, `synthesis/README.md`, `usage/README.md`, `contribution/README.md`);
`c2pa-live/` cuts its media from the packaging corpus's AV1 floor, and `contribution/` generates
its pictures with `wai.generate.spectral` and the model file of that capability's conformance
vector. `joule_moq_e2ee_vectors gen` (in
`joule-code/joule-code-rs/crates/joule-moq-mls`) writes `e2ee/vectors.json` from fixed seeds,
and `e2ee/` also holds the test vectors RFC 9605 and `draft-ietf-moq-secure-objects-01`
publish, as printed (`e2ee/README.md`). One recorded record: `metering/relay-record.wmh` and
@ -60,4 +64,5 @@ python3 live_watermark_verify.py watermark
python3 c2pa_live_verify.py c2pa-live
python3 synthesis_verify.py synthesis
python3 ai_usage_verify.py usage
python3 contribution_verify.py contribution
```

View file

@ -0,0 +1,40 @@
# live-conformance/contribution — contribution records
The corpus of [`extensions/contribution.md`](../../extensions/contribution.md) §12, in one
file, `vectors.json`: the model files its sessions generate with (`models`, each its `sha256`
and its `file` as text), and the **vectors**, each a case of §10.1 and what a verifier gives it.
A case holds its `policy` (`tier`); the `session` record and its `memberships`, the
contribution `record`, the disclosed `steps` (each its `step` text and its inclusion `proof`, a
list of siblings in hexadecimal), the authorship `map`, the prompt `openings`, each as a JSON
text; the `states` it holds, each an `RGB8` buffer in hexadecimal, named by its SHA-256; the
`models` it holds, each by the SHA-256 of a file in `models`; and the `origins` it holds, each
an origin claim of [synthesis](../../extensions/synthesis.md) §3.1 as a JSON text.
**Contribution equivalence.** For each vector, a conforming verifier reaches the outcome it
states:
- `refuse`: the `reason` and where, `at`: `["session"]`, `["session", "party", i]`,
`["membership", i]`, `["record"]`, `["step", i]`, `["map"]`, `["map", x, y]`,
`["opening", i]`, `["origin", i]` or `["policy"]`;
- `accept`: the `statements` of §10.4: the `tier`; the link hashes of the `session` and the
`record`; each disclosed step's `seq`, `act`, `party`, `check` and `status`; the number of
steps `committed` and not disclosed; the shares of the `computed` and the `stated` map, each
its `samples`, its `made_by` and `directed_by` counts per party, and the counts of `unknown`
and `undisclosed` samples of each set; the record's `estimates` as stated; the `origin`
agreement (`agree`, `not-checked` or `null`); and the C2PA statement of §9.2 as its JCS text
(`c2pa`), or `null` where a step is not disclosed.
A verifier that reaches another outcome, reason, place or statement does not conform.
**Provenance.** `wai_contribution_vectors gen` writes `vectors.json` from records authored in
code: every key is SHA-256 of the corpus's label and an index, every drawn picture and brush
stroke is SHA-256 output of a label, every generated picture is made by `wai.generate.spectral`
with the model file of its conformance vector (`wai-rs/tests/vectors/generate/
generate_spectral_model.json`, held here byte for byte in `models`), every time is a constant,
and Ed25519 signing is deterministic, so the file is the same bytes on every machine. Every
state is named by the SHA-256 of its `RGB8` samples.
```
python3 -I ../contribution_verify.py .
```

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -784,6 +784,20 @@
"edition": null,
"registered": "2026-10-20"
},
{
"capability": "wai.media.contribution",
"kind": "record",
"status": "registered",
"media": null,
"role": null,
"tier": null,
"licence": null,
"replicate": null,
"sets": [],
"spec_section": "§5 Interactive, temporal & computed content",
"edition": null,
"registered": "2026-10-21"
},
{
"capability": "wai.media.synthesis",
"kind": "record",
@ -2347,6 +2361,13 @@
"section": "§3",
"status": "defined"
},
{
"name": "wai-authorship-map",
"kind": "document-kind",
"document": "extensions/contribution.md",
"section": "§7.3",
"status": "defined"
},
{
"name": "wai-c2pa",
"kind": "catalog",
@ -2354,6 +2375,13 @@
"section": "§10.2",
"status": "defined"
},
{
"name": "wai-contribution",
"kind": "document-kind",
"document": "extensions/contribution.md",
"section": "§6",
"status": "defined"
},
{
"name": "wai-derivation",
"kind": "document-kind",
@ -2389,6 +2417,13 @@
"section": "§7.2",
"status": "defined"
},
{
"name": "wai-membership",
"kind": "document-kind",
"document": "extensions/contribution.md",
"section": "§3.2",
"status": "defined"
},
{
"name": "wai-module-grant",
"kind": "document-kind",
@ -2438,6 +2473,20 @@
"section": "§9.1",
"status": "defined"
},
{
"name": "wai-session",
"kind": "document-kind",
"document": "extensions/contribution.md",
"section": "§3.1",
"status": "defined"
},
{
"name": "wai-step",
"kind": "document-kind",
"document": "extensions/contribution.md",
"section": "§4.1",
"status": "defined"
},
{
"name": "wai1",
"kind": "codecs-prefix",

View file

@ -625,6 +625,13 @@ wai:wai.live.watermark a wai:Capability ;
wai:specSection "§5 Interactive, temporal & computed content" ;
wai:registeredOn "2026-10-20"^^xsd:date .
wai:wai.media.contribution a wai:Capability ;
rdfs:label "wai.media.contribution" ;
wai:kind wai:kind-record ;
wai:status wai:status-registered ;
wai:specSection "§5 Interactive, temporal & computed content" ;
wai:registeredOn "2026-10-21"^^xsd:date .
wai:wai.media.synthesis a wai:Capability ;
rdfs:label "wai.media.synthesis" ;
wai:kind wai:kind-record ;

View file

@ -298,6 +298,15 @@ derive_edge = ["key_discovery", "video_int", "png_still", "dep:jpeg-encoder"]
# the review's tier, and the label obligations of a player's signed log. SHA-256, BLAKE3,
# Ed25519 and the key log only; no new dependency.
synthesis = ["live_room"]
# Contribution records (extensions/contribution): `wai.media.contribution` — a tool's session of
# parties (people by key, each countersigning its membership; models by digest), the signed hash
# chain of steps (author, generate, candidates, select, edit, inpaint, transform, compose, reject)
# in a Merkle tree for selective disclosure, the authorship map computed by re-executing each
# act's dependency rule (made_by and directed_by per sample, unknown and undisclosed never
# credited), the checks a verifier makes (regions, selections, regeneration, recomputation), and
# the agreement with the output's origin claim. Reuses synthesis (its records and derivations)
# and generate (the pinned generator its exact steps regenerate with); no new dependency.
contribution = ["synthesis", "generate"]
# AI usage preferences and consumption claims (extensions/ai-usage): `wai.ai.usage` — the usage
# statement (draft-ietf-aipref-vocab-08's Structured Field form, the TDM Reservation Protocol's
# properties and the Training and Data Mining Assertion's entries, verbatim) in an envelope's
@ -765,6 +774,11 @@ name = "wai_synthesis_vectors"
path = "src/bin/wai_synthesis_vectors.rs"
required-features = ["synthesis"]
[[bin]]
name = "wai_contribution_vectors"
path = "src/bin/wai_contribution_vectors.rs"
required-features = ["contribution"]
[[bin]]
name = "wai_live_watermark_vectors"
path = "src/bin/wai_live_watermark_vectors.rs"
@ -1493,6 +1507,11 @@ name = "synthesis"
path = "tests/synthesis.rs"
required-features = ["synthesis"]
[[test]]
name = "contribution"
path = "tests/contribution.rs"
required-features = ["contribution"]
[[test]]
name = "live_watermark"
path = "tests/live_watermark.rs"

View file

@ -0,0 +1,74 @@
//! Conformance corpus for contribution records (`live-conformance/contribution/`, extensions/contribution.md §12).
//!
//! wai_contribution_vectors gen <corpus-dir> write vectors.json from fixed seeds
//! wai_contribution_vectors verify <corpus-dir> every file regenerates byte for byte, no other
//! file but README.md is there, and every case
//! gets the outcome it states
//!
//! The generator and this verifier are one implementation. The corpus's second
//! implementation is `live-conformance/contribution_verify.py`, written from the extension's
//! text and sharing no code with this binary.
use std::collections::BTreeMap;
use std::path::Path;
use wai::contribution::corpus;
fn main() {
let a: Vec<String> = std::env::args().collect();
match (a.get(1).map(String::as_str), a.get(2)) {
(Some("gen"), Some(d)) => {
if let Err(e) = std::fs::create_dir_all(d) {
eprintln!("create {d}: {e}");
std::process::exit(1);
}
for (name, body) in corpus::generate() {
let p = Path::new(d).join(&name);
if let Err(e) = std::fs::write(&p, body) {
eprintln!("write {}: {e}", p.display());
std::process::exit(1);
}
}
println!("wrote {d}");
}
(Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))),
_ => {
eprintln!("usage: wai_contribution_vectors gen|verify <corpus-dir>");
std::process::exit(2);
}
}
}
fn verify(dir: &Path) -> i32 {
let mut files = BTreeMap::new();
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(e) => {
eprintln!("read {}: {e}", dir.display());
return 1;
}
};
for e in entries.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
if name == "README.md" {
continue;
}
match std::fs::read_to_string(e.path()) {
Ok(s) => {
files.insert(name, s);
}
Err(err) => {
eprintln!("read {name}: {err}");
return 1;
}
}
}
let generated: BTreeMap<String, String> = corpus::generate().into_iter().collect();
let same = files == generated;
println!("{} every file regenerates byte for byte from its seeds, and no other file is there", if same { "PASS" } else { "FAIL" });
let fails = corpus::verify(&files);
for f in &fails {
println!("FAIL {f}");
}
println!("{} every case gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" });
(!same || !fails.is_empty()) as i32
}

View file

@ -225,6 +225,7 @@ pub const CAP_LIVE_PROVENANCE: &str = "wai.live.provenance";
pub const CAP_EDGE_DERIVATION: &str = "wai.edge.derivation";
pub const CAP_MEDIA_SYNTHESIS: &str = "wai.media.synthesis";
pub const CAP_AI_USAGE: &str = "wai.ai.usage";
pub const CAP_MEDIA_CONTRIBUTION: &str = "wai.media.contribution";
/// One row per string SPEC §5 registers that is a payload, companion or record:
/// `(string, SPEC section, edition, registered)`.
@ -318,6 +319,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[
(CAP_LIVE_PROVENANCE, S_INTERACTIVE, None, "2026-10-21"),
(CAP_MEDIA_SYNTHESIS, S_INTERACTIVE, None, "2026-10-21"),
(CAP_AI_USAGE, S_INTERACTIVE, None, "2026-10-21"),
(CAP_MEDIA_CONTRIBUTION, S_INTERACTIVE, None, "2026-10-21"),
(CAP_AUDIO_IAMF, S_IMMERSIVE, None, "2026-06-17"),
(CAP_AUDIO_AC4, S_IMMERSIVE, None, "2026-06-17"),
(CAP_AUDIO_IVAS, S_IMMERSIVE, None, "2026-06-17"),
@ -374,6 +376,7 @@ const RECORDS: &[&str] = &[
CAP_EDGE_DERIVATION,
CAP_MEDIA_SYNTHESIS,
CAP_AI_USAGE,
CAP_MEDIA_CONTRIBUTION,
];
/// A named identifier SPEC uses that has the shape of a capability string but is
@ -534,6 +537,7 @@ const LIVE_ADS: &str = "extensions/live-ads.md";
const LIVE_PROVENANCE: &str = "extensions/live-provenance.md";
const DERIVING_EDGE: &str = "extensions/deriving-edge.md";
const SYNTHESIS: &str = "extensions/synthesis.md";
const CONTRIBUTION: &str = "extensions/contribution.md";
const fn wire(name: &'static str, kind: WireKind, document: &'static str, section: &'static str,
status: WireStatus) -> WireIdentifier {
@ -545,8 +549,8 @@ const fn wire(name: &'static str, kind: WireKind, document: &'static str, sectio
/// (jwp-receipts §7), of packaging, of live events, of live rooms, of capture binding, of
/// live composition, of live recording, of timed text, of live metering, of energy steering,
/// of live moderation, of confidentiality, of forensic watermark sessions, of live ads, of live
/// provenance, of the deriving edge, of synthesis and of AI usage, in document order. The export
/// lists them sorted by name and kind.
/// provenance, of the deriving edge, of synthesis, of AI usage and of contribution records, in
/// document order. The export lists them sorted by name and kind.
pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
wire("WAI1", WireKind::Magic, "SPEC.md", "§2", WireStatus::Defined),
wire("WAI2", WireKind::Magic, "SPEC.md", "§7.1", WireStatus::Defined),
@ -644,6 +648,11 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
wire("WAU1", WireKind::Magic, AI_USAGE, "§4.2", WireStatus::Defined),
wire("wai_ai_usage", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined),
wire("WAI-Usage-Claim", WireKind::HttpHeader, AI_USAGE, "§7.3", WireStatus::Proposed),
wire("wai-session", WireKind::DocumentKind, CONTRIBUTION, "§3.1", WireStatus::Defined),
wire("wai-membership", WireKind::DocumentKind, CONTRIBUTION, "§3.2", WireStatus::Defined),
wire("wai-step", WireKind::DocumentKind, CONTRIBUTION, "§4.1", WireStatus::Defined),
wire("wai-contribution", WireKind::DocumentKind, CONTRIBUTION, "§6", WireStatus::Defined),
wire("wai-authorship-map", WireKind::DocumentKind, CONTRIBUTION, "§7.3", WireStatus::Defined),
];
/// A string with the shape of a capability that the reference implementation uses

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,875 @@
//! The conformance corpus of extensions/contribution.md §12: `live-conformance/contribution/`.
//!
//! [`generate`] writes it from fixed seeds: every key is SHA-256 of this corpus's label and an
//! index, every drawn picture is SHA-256 output of a label, every generated picture is made by
//! `wai.generate.spectral` with the model file of its conformance vector, every time and text
//! is written here, and Ed25519 signing is deterministic (RFC 8032), so the file is the same
//! bytes on every machine. Each case states the outcome it was written to have; the generator
//! asserts that this module reaches it before writing it, and that the corpus gives every
//! reason of §11.
//!
//! [`verify`] checks a corpus directory's files against this module.
//! `live-conformance/contribution_verify.py` checks the same file from the extension's text,
//! sharing no code with this module.
use super::*;
use serde_json::{json, Map, Value};
const ISSUED: &str = "2026-10-21T12:00:00Z";
const TOOL_NAME: &str = "conformance-studio/1";
/// The model file of `wai.generate.spectral`'s conformance vector.
pub const MODEL_FILE: &str = include_str!("../../tests/vectors/generate/generate_spectral_model.json");
// Keys.
const TOOL: u8 = 0;
const ALICE: u8 = 1;
const BOB: u8 = 2;
const CAROL: u8 = 3;
// Parties, by position.
const P_ALICE: u64 = 0;
const P_BOB: u64 = 1;
const P_SPECTRAL: u64 = 2;
const P_FLOAT: u64 = 3;
const P_CAROL: u64 = 4;
const W: u64 = 8;
const H: u64 = 6;
fn key(i: u8) -> SigningKey {
SigningKey::from_bytes(&sha256(&[b"wai:contribution/corpus/key\x00", &[i]]))
}
fn pk(i: u8) -> [u8; 32] {
key(i).verifying_key().to_bytes()
}
fn label(s: &str) -> [u8; 32] {
sha256(&[b"wai:contribution/corpus/label\x00", s.as_bytes()])
}
/// `n` bytes from a label: SHA-256 of the label and a counter, concatenated.
fn bytes(s: &str, n: u64) -> Vec<u8> {
(0u32..).flat_map(|i| sha256(&[s.as_bytes(), &i.to_le_bytes()])).take(n as usize).collect()
}
fn text(v: &Value) -> String {
jcs(v).expect("a record in the subset")
}
fn model() -> crate::generate::SpectralModel {
spectral_model(MODEL_FILE.as_bytes()).expect("the conformance model file")
}
fn spectral_digest() -> [u8; 32] {
sha256(&[MODEL_FILE.as_bytes()])
}
fn rect(x: u64, y: u64, w: u64, h: u64) -> Rect {
Rect { x, y, w, h }
}
fn rects_json(rs: &[Rect]) -> Value {
Value::Array(rs.iter().map(|r| json!({"x": r.x, "y": r.y, "w": r.w, "h": r.h})).collect())
}
/// `input` with the pixels of `patch` inside the region.
fn paste(input: &[u8], patch: &[u8], w: u64, h: u64, region: &[Rect]) -> Vec<u8> {
let mut out = input.to_vec();
for y in 0..h {
for x in 0..w {
if region.iter().any(|r| r.contains(x, y)) {
let k = ((y * w + x) * 3) as usize;
out[k..k + 3].copy_from_slice(&patch[k..k + 3]);
}
}
}
out
}
/// A colour table: each channel's curve.
fn lut_table() -> Vec<u8> {
let mut t = Vec::with_capacity(768);
for c in 0..3u32 {
for v in 0..256u32 {
t.push(match c {
0 => (v * 3 / 4 + 40).min(255),
1 => v,
_ => 255 - v,
} as u8);
}
}
t
}
fn op(derivation: &str, params: &Value) -> Op {
parse_op(derivation, params).expect("a pinned derivation")
}
// ---- a session being written ----
/// What a step does, for [`Sess::add`].
struct Doing<'a> {
party: u64,
act: &'a str,
inputs: Vec<(u64, [u8; 32])>,
output: Value,
region: Value,
params: Value,
check: &'a str,
}
fn act<'a>(party: u64, act: &'a str, inputs: &[(u64, [u8; 32])], output: Value, params: Value) -> Doing<'a> {
Doing { party, act, inputs: inputs.to_vec(), output, region: Value::Null, params, check: "replicate" }
}
#[derive(Clone)]
struct Sess {
session: Value,
link: [u8; 32],
memberships: Vec<Value>,
steps: Vec<Value>,
states: Vec<Vec<u8>>,
openings: Vec<Value>,
next: u64,
}
fn party_key(p: u64) -> u8 {
match p {
P_ALICE => ALICE,
P_BOB => BOB,
P_CAROL => CAROL,
_ => TOOL,
}
}
impl Sess {
fn new(name: &str) -> Sess {
let session = sign(
json!({
"kind": "wai-session",
"session": hex(&label(&format!("session {name}"))),
"tool": TOOL_NAME,
"media": "image",
"parties": [
{"role": "human", "key": hex(&pk(ALICE))},
{"role": "human", "key": hex(&pk(BOB))},
{"role": "model", "digest": hex(&spectral_digest()), "type": "algorithmic"},
{"role": "model", "digest": hex(&label("float image model")), "type": "trained"},
{"role": "audience", "key": hex(&pk(CAROL))},
],
"issued_at": ISSUED,
}),
&key(TOOL),
);
let link = link_hash(&session);
let memberships = [(P_ALICE, ALICE), (P_BOB, BOB), (P_CAROL, CAROL)]
.iter()
.map(|&(p, k)| sign(json!({"kind": "wai-membership", "session": hex(&link), "party": p, "issued_at": ISSUED}), &key(k)))
.collect();
Sess { session, link, memberships, steps: vec![], states: vec![], openings: vec![], next: 0 }
}
/// Register a state; its samples digest.
fn put(&mut self, px: Vec<u8>) -> [u8; 32] {
let d = sha256(&[&px]);
if !self.states.iter().any(|s| sha256(&[s]) == d) {
self.states.push(px);
}
d
}
/// A salted commitment to a prompt, and its opening.
fn prompt(&mut self, text: &str) -> [u8; 32] {
let salt = label(&format!("salt of {text}"));
let c = commit(&salt, text);
self.openings.push(json!({"commitment": hex(&c), "salt": hex(&salt), "text": text}));
c
}
fn body(&self, a: &Doing) -> Value {
json!({
"kind": "wai-step",
"session": hex(&self.link),
"seq": self.next,
"prev": self.steps.last().map(|s| json!(hex(&link_hash(s)))).unwrap_or(Value::Null),
"party": a.party,
"act": a.act,
"inputs": a.inputs.iter().map(|(s, d)| json!({"step": s, "samples": hex(d)})).collect::<Vec<_>>(),
"output": a.output,
"region": a.region,
"params": a.params,
"check": a.check,
"issued_at": ISSUED,
})
}
/// Sign and append a step; its `seq`.
fn add(&mut self, a: Doing) -> u64 {
self.add_body(self.body(&a), party_key(a.party))
}
fn add_body(&mut self, body: Value, k: u8) -> u64 {
let seq = body["seq"].as_u64().expect("a seq");
self.steps.push(sign(body, &key(k)));
self.next = seq + 1;
seq
}
fn links(&self) -> Vec<[u8; 32]> {
self.steps.iter().map(link_hash).collect()
}
fn parsed(&self) -> (Session, Vec<Step>) {
(read_session(&text(&self.session)).expect("the corpus session"), self.steps.iter().map(|s| read_step(&text(s)).expect("a corpus step")).collect())
}
/// The map a tool states for the output (§7.2).
fn map(&self, out: &Out) -> Value {
let (session, steps) = self.parsed();
let labels = computed_map(&session, &steps, out.step, out.samples, out.w, out.h);
encode_map(&out.samples, out.w, out.h, &labels)
}
fn record(&self, out: &Out, map: &Value, origin: Option<[u8; 32]>, estimates: Value) -> Value {
sign(
json!({
"kind": "wai-contribution",
"session": hex(&self.link),
"steps": self.steps.len(),
"root": hex(&root(&self.links())),
"output": {"step": out.step, "samples": hex(&out.samples), "width": out.w, "height": out.h, "content_hash": out.content_hash.map(|c| hex(&c))},
"map": hex(&map_digest(map)),
"origin": origin.map(|o| hex(&o)),
"estimates": estimates,
"issued_at": ISSUED,
}),
&key(TOOL),
)
}
/// Every step, with its proof.
fn disclosed(&self) -> Vec<Disclosed> {
let links = self.links();
self.steps.iter().enumerate().map(|(i, s)| Disclosed { text: text(s), proof: proof(&links, i).iter().map(|p| hex(p)).collect() }).collect()
}
/// The case of full disclosure: every step, state, opening and the model file.
fn case(&self, record: &Value, map: &Value) -> Case {
Case {
session: text(&self.session),
memberships: self.memberships.iter().map(text).collect(),
record: text(record),
steps: self.disclosed(),
map: text(map),
openings: self.openings.iter().map(text).collect(),
states: self.states.clone(),
models: vec![MODEL_FILE.as_bytes().to_vec()],
origins: vec![],
tier: Tier::Stated,
}
}
/// The record and the map of the output, and the full case.
fn full(&self, out: &Out) -> (Value, Value, Case) {
let map = self.map(out);
let record = self.record(out, &map, None, json!([]));
let case = self.case(&record, &map);
(record, map, case)
}
}
/// The output a record names.
#[derive(Clone, Copy)]
struct Out {
step: u64,
samples: [u8; 32],
w: u64,
h: u64,
content_hash: Option<[u8; 32]>,
}
fn one(d: &[u8; 32], w: u64, h: u64) -> Value {
json!({"samples": hex(d), "width": w, "height": h})
}
fn gen_params(generator: &str, prompt: Option<[u8; 32]>, prompt_by: Option<u64>, seed: Option<u64>, steps: Option<u64>) -> Value {
json!({"generator": generator, "prompt": prompt.map(|p| hex(&p)), "prompt_by": prompt_by, "seed": seed, "steps": steps})
}
fn derive(derivation: &str, params: Value) -> Value {
json!({"derivation": derivation, "params": params})
}
// ---- the sessions ----
/// A person draws, a collaborator brushes inside a region, the first crops (§12).
fn brush(defect: Option<&str>) -> (Sess, Out) {
let mut s = Sess::new("brush");
let canvas = bytes("brush canvas", W * H * 3);
let d0 = s.put(canvas.clone());
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCreation"})));
let region: Vec<Rect> = if defect == Some("region") { vec![rect(2, 1, 3, 2)] } else { vec![rect(2, 1, 3, 2), rect(5, 4, 2, 1)] };
let mut brushed = paste(&canvas, &bytes("brush strokes", W * H * 3), W, H, &region);
if defect == Some("outside") {
brushed[0] ^= 0x01;
}
let edit_input = if defect == Some("input") { s.put(bytes("a picture of no step", W * H * 3)) } else { d0 };
let (ew, eh) = if defect == Some("region") { (W - 2, H) } else { (W, H) };
let edited = if defect == Some("region") { brushed.iter().copied().take((ew * eh * 3) as usize).collect() } else { brushed };
let d1 = s.put(edited.clone());
s.add(Doing { region: rects_json(&region), ..act(P_BOB, "edit", &[(0, edit_input)], one(&d1, ew, eh), json!({})) });
let crop = json!({"x": 1, "y": 1, "w": 6, "h": 4});
let mut cropped = if defect == Some("region") {
bytes("a crop of a smaller picture", 6 * 4 * 3)
} else {
op(CROP, &crop).eval(&[(W, H, edited.as_slice())]).expect("in range").2
};
if defect == Some("recompute") {
cropped[5] ^= 0x10;
}
let d2 = s.put(cropped);
s.add(act(P_ALICE, "transform", &[(1, d1)], one(&d2, 6, 4), derive(CROP, crop)));
(s, Out { step: 2, samples: d2, w: 6, h: 4, content_hash: Some(label("published brush picture")) })
}
/// A model inpaints a region one person marked, from a prompt the other wrote (§12).
fn inpaint(defect: bool) -> (Sess, Out) {
let mut s = Sess::new("inpaint");
let photo = bytes("inpaint photo", W * H * 3);
let d0 = s.put(photo.clone());
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCapture"})));
let prompt = "a calm ocean horizon";
let c = s.prompt(prompt);
let region = [rect(3, 2, 4, 3)];
let fill = crate::generate::generate(&model(), prompt, 7, 1, H as usize, W as usize);
let mut inpainted = paste(&photo, &fill, W, H, &region);
if defect {
// A byte of the first pixel inside the region, (3, 2), that the generator did not make.
inpainted[((2 * W + 3) * 3) as usize] ^= 0x01;
}
let d1 = s.put(inpainted);
let mut p = gen_params(SPECTRAL, Some(c), Some(P_BOB), Some(7), Some(1));
p["mask_by"] = json!(P_ALICE);
s.add(Doing { region: rects_json(&region), ..act(P_SPECTRAL, "inpaint", &[(0, d0)], one(&d1, W, H), p) });
(s, Out { step: 1, samples: d1, w: W, h: H, content_hash: Some(label("published inpainted picture")) })
}
/// Three candidates, one selected by an audience member, one rejected, then a colour table (§12).
fn candidates(seeds_stated: [u64; 3]) -> (Sess, Out) {
let mut s = Sess::new("candidates");
let prompt = "a bright spiral of fire";
let c = s.prompt(prompt);
let m = model();
let set: Vec<[u8; 32]> = [1u64, 2, 3].iter().map(|&seed| s.put(crate::generate::generate(&m, prompt, seed, 1, H as usize, W as usize))).collect();
let chosen = set[1];
let mut p = gen_params(SPECTRAL, Some(c), Some(P_ALICE), None, Some(1));
p.as_object_mut().expect("params").remove("seed");
p["seeds"] = json!(seeds_stated);
s.add(act(P_SPECTRAL, "candidates", &[], json!({"set": set.iter().map(|d| hex(d)).collect::<Vec<_>>(), "width": W, "height": H}), p));
s.add(act(P_CAROL, "select", &[(0, chosen)], one(&chosen, W, H), json!({})));
s.add(act(P_ALICE, "reject", &[(0, set[0])], Value::Null, json!({})));
let table = json!({"table": hex(&lut_table())});
let chosen_px = generated(prompt, 2);
let graded = op(LUT, &table).eval(&[(W, H, chosen_px.as_slice())]).expect("in range").2;
let d3 = s.put(graded);
s.add(act(P_ALICE, "transform", &[(1, chosen)], one(&d3, W, H), derive(LUT, table)));
(s, Out { step: 3, samples: d3, w: W, h: H, content_hash: None })
}
fn generated(prompt: &str, seed: u64) -> Vec<u8> {
crate::generate::generate(&model(), prompt, seed, 1, H as usize, W as usize)
}
/// A generated layer composed over a drawing on a wider canvas with a fill (§12).
fn compose() -> (Sess, Out) {
let mut s = Sess::new("compose");
let bg = bytes("compose background", W * H * 3);
let d0 = s.put(bg.clone());
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCreation"})));
let prompt = "a calm star";
let c = s.prompt(prompt);
let layer = crate::generate::generate(&model(), prompt, 11, 1, 3, 4);
let d1 = s.put(layer.clone());
s.add(act(P_SPECTRAL, "generate", &[], one(&d1, 4, 3), gen_params(SPECTRAL, Some(c), Some(P_BOB), Some(11), Some(1))));
let params = json!({"width": 10, "height": 6, "fill": "204060", "at": [{"x": 0, "y": 0}, {"x": 5, "y": 2}]});
let composed = op(OVERLAY, &params).eval(&[(W, H, bg.as_slice()), (4, 3, layer.as_slice())]).expect("in range").2;
let d2 = s.put(composed);
s.add(act(P_BOB, "compose", &[(0, d0), (1, d1)], one(&d2, 10, 6), derive(OVERLAY, params)));
(s, Out { step: 2, samples: d2, w: 10, h: 6, content_hash: None })
}
/// A chain of transforms, each recomputed (§12).
fn transforms() -> (Sess, Out) {
let mut s = Sess::new("transforms");
let mut px = bytes("transforms drawing", W * H * 3);
let (mut w, mut h) = (W, H);
let d0 = s.put(px.clone());
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCreation"})));
let mut last = (0u64, d0);
for (party, derivation, params) in [
(P_ALICE, ROTATION, json!({"quarter_turns": 1})),
(P_BOB, MIRROR, json!({"flip": "left-right"})),
(P_ALICE, syn::UPSCALE2, json!({})),
(P_ALICE, syn::DENOISE3, json!({})),
(P_BOB, CROP, json!({"x": 2, "y": 3, "w": 8, "h": 9})),
] {
let (nw, nh, out) = op(derivation, &params).eval(&[(w, h, px.as_slice())]).expect("in range");
let d = s.put(out.clone());
let seq = s.add(act(party, "transform", &[last], one(&d, nw, nh), derive(derivation, params)));
(w, h, px, last) = (nw, nh, out, (seq, d));
}
(s, Out { step: last.0, samples: last.1, w, h, content_hash: None })
}
/// An inpainting by a trained model the tool attests (§12).
fn inpaint_attested() -> (Sess, Out) {
let mut s = Sess::new("inpaint attested");
let photo = bytes("street photo", W * H * 3);
let d0 = s.put(photo.clone());
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCapture"})));
let c = s.prompt("remove the lamp post");
let region = [rect(1, 1, 3, 3)];
let d1 = s.put(paste(&photo, &bytes("float inpainting", W * H * 3), W, H, &region));
let mut p = gen_params("image-inpainting", Some(c), Some(P_ALICE), None, None);
p["mask_by"] = json!(P_ALICE);
s.add(Doing { region: rects_json(&region), check: "attest", ..act(P_FLOAT, "inpaint", &[(0, d0)], one(&d1, W, H), p) });
(s, Out { step: 1, samples: d1, w: W, h: H, content_hash: None })
}
/// An attested scaling, then a brush stroke over its output (§12).
fn scaling() -> (Sess, Out) {
let mut s = Sess::new("scaling");
let px = bytes("scaling drawing", W * H * 3);
let d0 = s.put(px.clone());
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCreation"})));
// The reference edge's nearest sample (deriving-edge §4.2), which no verifier holds a step to.
let mut small = Vec::new();
for j in 0..3u64 {
for i in 0..4u64 {
let k = (((j * H / 3) * W + i * W / 4) * 3) as usize;
small.extend_from_slice(&px[k..k + 3]);
}
}
let d1 = s.put(small.clone());
s.add(Doing { check: "attest", ..act(P_ALICE, "transform", &[(0, d0)], one(&d1, 4, 3), derive(SCALING, json!({"width": 4, "height": 3}))) });
let region = [rect(0, 0, 2, 1)];
let d2 = s.put(paste(&small, &bytes("scaling brush", 4 * 3 * 3), 4, 3, &region));
s.add(Doing { region: rects_json(&region), ..act(P_BOB, "edit", &[(1, d1)], one(&d2, 4, 3), json!({})) });
(s, Out { step: 2, samples: d2, w: 4, h: 3, content_hash: None })
}
// ---- origin claims (synthesis §3.1) ----
fn origin_claim(content_hash: &[u8; 32], class: &str, step: &str, source: &str, models: Value, oversight: Value) -> Value {
syn::sign(
json!({
"kind": "wai-origin",
"subject": {"type": "object", "content_hash": hex(content_hash)},
"media": "image",
"class": class,
"step": step,
"operation": if step == "capture" { "capture" } else { "inpainting" },
"record": null,
"source": source,
"models": models,
"oversight": oversight,
"inputs": [],
"evidence": null,
"issued_at": ISSUED,
}),
&key(TOOL),
)
}
fn synthesized_claim(content_hash: &[u8; 32]) -> Value {
origin_claim(content_hash, "synthesized", "generate", "algorithmicMedia", json!([{"digest": hex(&spectral_digest()), "type": "algorithmic"}]), json!("prompt_guided"))
}
fn captured_claim(content_hash: &[u8; 32]) -> Value {
origin_claim(content_hash, "captured", "capture", "digitalCreation", json!([]), Value::Null)
}
// ---- vectors ----
#[derive(Clone, Debug)]
enum Intent {
Accept,
Refuse(At, Reason),
}
struct Vector {
name: &'static str,
about: &'static str,
case: Case,
intent: Intent,
}
fn with(mut body: Value, path: &[&str], v: Value) -> Value {
let mut at = &mut body;
for p in &path[..path.len() - 1] {
at = at.get_mut(*p).expect("a member");
}
at[path[path.len() - 1]] = v;
body
}
/// A record body without its signature, to change and sign again.
fn unsigned(v: &Value) -> Value {
let mut b = v.clone();
let o = b.as_object_mut().expect("a record");
o.remove("sig");
o.remove("signer_pubkey");
b
}
fn flip_digit(s: &str, at: usize) -> String {
let c = if &s[at..at + 1] == "0" { "1" } else { "0" };
format!("{}{c}{}", &s[..at], &s[at + 1..])
}
/// The case with the record's origin claim given.
fn with_origin(s: &Sess, out: &Out, claim: &Value) -> Case {
let map = s.map(out);
let record = s.record(out, &map, Some(syn::link_hash(claim)), json!([]));
let mut c = s.case(&record, &map);
c.origins = vec![text(claim)];
c
}
fn vectors() -> Vec<Vector> {
let mut out = Vec::new();
let mut accept = |name, about, case| out.push(Vector { name, about, case, intent: Intent::Accept });
let (b, bo) = brush(None);
accept("brush", "a person draws, a collaborator brushes inside two rectangles, and the first crops: the edit changed nothing outside its region and the crop recomputes; the origin claim, captured, agrees with a map no model made any sample of (§4, §7, §9.1)", with_origin(&b, &bo, &captured_claim(&bo.content_hash.expect("published"))));
let (ip, ipo) = inpaint(false);
accept("inpaint", "the pinned generator inpaints a region one person marked from a prompt the other wrote: the fill regenerates, nothing outside the region changed, and the synthesized origin claim agrees (§4.5, §7.2, §9.1)", with_origin(&ip, &ipo, &synthesized_claim(&ipo.content_hash.expect("published"))));
let (cs, cso) = candidates([1, 2, 3]);
accept("candidates", "three candidates, each regenerated from its seed; an audience member selects the second, a person rejects the first, and a colour table is applied: what was chosen over what is evidence (§4.2, §7.2)", cs.full(&cso).2);
let (cp, cpo) = compose();
accept("compose", "a generated layer placed over a drawing on a wider canvas with a fill: each layer's map where it is visible, the fill the arranger's (§4.5, §7.2)", cp.full(&cpo).2);
let (tr, tro) = transforms();
accept("transforms", "a rotation, a mirror, an upscale, a denoise and a crop, each recomputed: labels carried through each sample mapping, the filters taking the union of what they mix (§4.5, §7.2)", tr.full(&tro).2);
let (ia, iao) = inpaint_attested();
accept("inpaint_attested", "an inpainting by a trained model the tool attests: nothing outside its region changed, the region made by the model, and the outcome checked, not recomputed (§4.4, §10.3)", ia.full(&iao).2);
let (sc, sco) = scaling();
accept("scaling_unknown", "an attested scaling, whose sample mapping no rule pins, then a brush stroke over it: the samples outside the stroke are unknown, credited to no one (§7.1, §7.2)", sc.full(&sco).2);
let (_, cs_map, cs_full) = cs.full(&cso);
let mut partial = cs_full.clone();
partial.steps.remove(0);
partial.states.clear();
accept("candidates_partial", "the candidates session with its generating step undisclosed and no state held: the selection, the rejection and the colour table check against the root, and the candidates' samples are undisclosed (§7.1, §8.2)", partial.clone());
let mut private = cs_full.clone();
private.openings.clear();
accept("candidates_prompt_private", "every step of the candidates session disclosed but its prompt kept private: the candidates are not regenerated, and the outcome is checked (§8.1, §8.2)", private);
let mut stated = cs_full.clone();
stated.steps.clear();
stated.openings.clear();
stated.states.clear();
stated.models.clear();
accept("stated", "the record and the map alone: the signed map is held, nothing recomputed (§8.2)", stated);
let bm = b.map(&bo);
let br = b.record(&bo, &bm, None, json!([{"estimator": "conformance-estimator", "class": "information-theoretic", "party": P_BOB, "basis": "made_by", "share_ppm": 412_500}]));
let mut est = b.case(&br, &bm);
est.steps.clear();
accept("estimate", "a record that attaches an estimated share with its class, reported as stated beside the exact counts (§6)", est);
// ---- refusals, each a control of one defect ----
let mut refuse = |name, about, case, at, reason| out.push(Vector { name, about, case, intent: Intent::Refuse(at, reason) });
let (_, b_map, b_full) = b.full(&bo);
let mut c = b_full.clone();
c.steps[1].text = text(&sign(with(unsigned(&b.steps[1]), &["note"], json!("an extra member")), &key(BOB)));
refuse("record_malformed", "a step with a member §4.1 does not list", c, At::Step(1), Reason::RecordMalformed);
let mut c = b_full.clone();
c.memberships[0] = text(&with(b.memberships[0].clone(), &["signer_pubkey"], json!(format!("01{}", "00".repeat(31)))));
refuse("bad_key", "a membership whose key is the identity point, of small order (SPEC §7 *Signatures* step 2)", c, At::Membership(0), Reason::BadKey);
let mut c = b_full.clone();
c.steps[0].text = text(&with(b.steps[0].clone(), &["sig"], json!(flip_digit(b.steps[0]["sig"].as_str().expect("sig"), 0))));
refuse("bad_signature", "a step whose signature has one digit changed", c, At::Step(0), Reason::BadSignature);
let mut c = b_full.clone();
let direct = sign(with(unsigned(&b.steps[2]), &["act"], json!("direct")), &key(ALICE));
c.steps.push(Disclosed { text: text(&direct), proof: vec![] });
refuse("act_reserved", "a step naming the reserved act direct (§4.2)", c, At::Step(3), Reason::ActReserved);
let mut c = b_full.clone();
let mut split = b_map.clone();
let row0 = split["rows"][0].as_array().expect("a row").clone();
let k = row0.iter().position(|r| r[1].as_u64().expect("a length") >= 2).expect("a run of two pixels");
let (i, n) = (row0[k][0].as_u64().expect("i"), row0[k][1].as_u64().expect("n"));
let mut r: Vec<Value> = row0[..k].to_vec();
r.extend([json!([i, 1]), json!([i, n - 1])]);
r.extend(row0[k + 1..].iter().cloned());
split["rows"][0] = Value::Array(r);
c.map = text(&split);
refuse("map_malformed", "a map with a run split in two: a map has one encoding (§7.3)", c, At::Map, Reason::MapMalformed);
let mut c = ip.full(&ipo).2;
c.openings[0] = text(&with(ip.openings[0].clone(), &["text"], json!("a calm ocean horizon at night")));
refuse("opening_mismatch", "a prompt commitment that does not open: the opening's text is not the text committed (§8.1)", c, At::Opening(0), Reason::OpeningMismatch);
let mut c = b_full.clone();
c.memberships[1] = text(&sign(unsigned(&b.memberships[1]), &key(TOOL)));
refuse("membership_invalid", "the collaborator's membership signed by the tool's key, not the collaborator's (§3.2)", c, At::Membership(1), Reason::MembershipInvalid);
let mut c = b_full.clone();
c.memberships.remove(1);
refuse("party_not_countersigned", "a person enrolled as a party of the session without countersigning it (§3.2)", c, At::SessionParty(1), Reason::PartyNotCountersigned);
let mut c = b_full.clone();
c.steps[1].text = text(&sign(with(unsigned(&b.steps[1]), &["session"], json!(hex(&label("another session")))), &key(BOB)));
refuse("session_mismatch", "a step of another session (§4.1)", c, At::Step(1), Reason::SessionMismatch);
let mut c = b_full.clone();
c.steps[1].text = text(&sign(with(unsigned(&b.steps[1]), &["party"], json!(9)), &key(BOB)));
refuse("party_unknown", "a step by a party the session does not have (§4.1)", c, At::Step(1), Reason::PartyUnknown);
let mut c = b_full.clone();
c.steps[0].text = text(&sign(with(unsigned(&b.steps[0]), &["party"], json!(P_SPECTRAL)), &key(TOOL)));
refuse("party_role_mismatch", "a drawing stated as authored by a model: author is a person's act (§4.2)", c, At::Step(0), Reason::PartyRoleMismatch);
let mut c = b_full.clone();
c.steps[1].text = text(&sign(unsigned(&b.steps[1]), &key(TOOL)));
refuse("signer_mismatch", "a person's brush edit signed by the tool's key instead of the person's (§4.1)", c, At::Step(1), Reason::SignerMismatch);
let mut c = b_full.clone();
c.steps[2].text = text(&sign(with(unsigned(&b.steps[2]), &["check"], json!("attest")), &key(ALICE)));
refuse("check_mismatch", "a pinned crop stated as attested (§4.4)", c, At::Step(2), Reason::CheckMismatch);
// A crop outside its input, in a session otherwise whole.
let mut s = Sess::new("crop outside");
let d0 = s.put(bytes("crop outside canvas", W * H * 3));
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCreation"})));
let d1 = s.put(bytes("a crop no input has", 6 * 4 * 3));
s.add(act(P_ALICE, "transform", &[(0, d0)], one(&d1, 6, 4), derive(CROP, json!({"x": 4, "y": 1, "w": 6, "h": 4}))));
refuse("params_invalid", "a crop whose region lies outside its input (§4.5)", s.full(&Out { step: 1, samples: d1, w: 6, h: 4, content_hash: None }).2, At::Step(1), Reason::ParamsInvalid);
// A gap: the tool skips seq 2 and the record counts three steps.
let mut s = b.clone();
s.steps.truncate(2);
s.next = 3;
let crop_body = with(with(unsigned(&b.steps[2]), &["seq"], json!(3)), &["prev"], json!(hex(&link_hash(&b.steps[1]))));
s.add_body(crop_body, ALICE);
let record = s.record(&Out { step: 2, ..bo }, &b_map, None, json!([]));
refuse("chain_broken", "a gap in the chain: the steps are numbered 0, 1 and 3, and the record counts three (§5.1)", s.case(&record, &b_map), At::Step(2), Reason::ChainBroken);
let mut c = b_full.clone();
c.steps[1].proof[0] = flip_digit(&c.steps[1].proof[0], 4);
refuse("inclusion_failed", "a step whose inclusion proof has a sibling changed (§5.2)", c, At::Step(1), Reason::InclusionFailed);
// A drawing that states a size its samples do not have.
let mut s = Sess::new("wrong size");
let d0 = s.put(bytes("wrong size canvas", W * H * 3));
s.add(act(P_ALICE, "author", &[], one(&d0, 4, H), json!({"source": "digitalCreation"})));
refuse("samples_mismatch", "a drawing that states 4 × 6 pixels for samples of 8 × 6 (§10.2 step 6)", s.full(&Out { step: 0, samples: d0, w: 4, h: H, content_hash: None }).2, At::Step(0), Reason::SamplesMismatch);
let (bi, bio) = brush(Some("input"));
refuse("input_unknown", "an edit whose input names the drawing's step with samples that step did not produce (§10.2 step 6)", bi.full(&bio).2, At::Step(1), Reason::InputUnknown);
// A selection of a picture the set does not hold.
let mut s = Sess::new("selection");
let prompt = "a bright spiral of fire";
let c0 = s.prompt(prompt);
let set: Vec<[u8; 32]> = [1u64, 2].iter().map(|&seed| s.put(generated(prompt, seed))).collect();
let mut p = gen_params(SPECTRAL, Some(c0), Some(P_ALICE), None, Some(1));
p.as_object_mut().expect("params").remove("seed");
p["seeds"] = json!([1, 2]);
s.add(act(P_SPECTRAL, "candidates", &[], json!({"set": set.iter().map(|d| hex(d)).collect::<Vec<_>>(), "width": W, "height": H}), p));
let other = s.put(generated(prompt, 3));
s.add(act(P_ALICE, "select", &[(0, other)], one(&other, W, H), json!({})));
refuse("selection_not_in_set", "a selected output that is not a member of the committed set: the picture of seed 3, where the set holds seeds 1 and 2 (§4.2)", s.full(&Out { step: 1, samples: other, w: W, h: H, content_hash: None }).2, At::Step(1), Reason::SelectionNotInSet);
let (bz, bzo) = brush(Some("region"));
refuse("region_invalid", "a brush edit whose output is 6 × 6 over an input of 8 × 6 (§10.2 step 6)", bz.full(&bzo).2, At::Step(1), Reason::RegionInvalid);
let (bx, bxo) = brush(Some("outside"));
refuse("outside_region_changed", "a sample outside the edit's region changed: the first byte of the edited picture differs from the drawing's (§10.2 step 6)", bx.full(&bxo).2, At::Step(1), Reason::OutsideRegionChanged);
let (cr, cro) = candidates([1, 2, 4]);
refuse("regeneration_mismatch", "a candidate that does not regenerate: the set holds the picture of seed 3, and the step states seed 4 (§4.5)", cr.full(&cro).2, At::Step(0), Reason::RegenerationMismatch);
let (id, ido) = inpaint(true);
refuse("regeneration_mismatch_inpaint", "an inpainting whose region does not regenerate: one byte inside the region is not the generator's (§4.5)", id.full(&ido).2, At::Step(1), Reason::RegenerationMismatch);
let (bc, bco) = brush(Some("recompute"));
refuse("recompute_mismatch", "a crop whose stated output has one byte the crop does not give (§4.5)", bc.full(&bco).2, At::Step(2), Reason::RecomputeMismatch);
let mut c = b_full.clone();
let wrong_out = Out { step: 1, ..bo };
c.record = text(&b.record(&wrong_out, &b_map, None, json!([])));
refuse("output_mismatch", "a record naming the edit's step for the crop's output (§10.2 step 7)", c, At::Record, Reason::OutputMismatch);
let mut c = b_full.clone();
c.map = text(&cs_map);
refuse("map_unbound", "the map of another session's output given with the record (§7.3)", c, At::Map, Reason::MapUnbound);
let labels: Vec<Label> = read_map(&text(&b_map), 5).expect("the brush map").labels.iter().map(|l| if l.made_by.has(P_BOB) { Label { made_by: Set::party(P_ALICE), ..*l } } else { *l }).collect();
let wrong = encode_map(&bo.samples, bo.w, bo.h, &labels);
let r = b.record(&bo, &wrong, None, json!([]));
refuse("map_mismatch", "a map that does not recompute: the collaborator's brush strokes credited to the first person (§7.2, §10.2 step 8)", b.case(&r, &wrong), At::MapPixel(1, 0), Reason::MapMismatch);
let sc_labels: Vec<Label> = read_map(&text(&sc.map(&sco)), 5)
.expect("the scaling map")
.labels
.iter()
.map(|l| if l.made_by.unknown { Label { made_by: Set::party(P_ALICE), directed_by: Set::party(P_ALICE) } } else { *l })
.collect();
let credited = encode_map(&sco.samples, sco.w, sco.h, &sc_labels);
let r = sc.record(&sco, &credited, None, json!([]));
refuse("unknown_attributed", "an attested scaling's samples, which no rule maps, credited to the person who chose it instead of unknown (§7.1, §10.2 step 8)", sc.case(&r, &credited), At::MapPixel(2, 0), Reason::UnknownAttributed);
refuse("origin_mismatch", "an origin claim about another object (§9.1)", with_origin(&ip, &ipo, &synthesized_claim(&label("another object"))), At::Origin(0), Reason::OriginMismatch);
refuse("origin_understated", "an inpainted picture whose origin claim says captured: a model made part of it (§9.1)", with_origin(&ip, &ipo, &captured_claim(&ipo.content_hash.expect("published"))), At::Origin(0), Reason::OriginUnderstated);
// ---- the second site of a check, each a control of its own ----
let wrong_size = Out { w: 6, h: 5, ..bo };
let map = encode_map(&bo.samples, 6, 5, &vec![Label { made_by: Set::party(P_ALICE), directed_by: Set::party(P_ALICE) }; 30]);
let mut c = b.case(&b.record(&wrong_size, &map, None, json!([])), &map);
c.steps.clear();
refuse("samples_mismatch_output", "a record stating 6 × 5 pixels for an output whose held samples are 6 × 4, no step disclosed (§10.2 step 7)", c, At::Record, Reason::SamplesMismatch);
let mut c = bz.full(&bzo).2;
c.steps.remove(0);
refuse("region_invalid_buffer", "a brush edit whose output is 6 × 6 over a held input of 8 × 6, the input's step not disclosed (§10.2 step 6)", c, At::Step(0), Reason::RegionInvalid);
let mut s = Sess::new("selection output");
let c0 = s.prompt(prompt);
let set: Vec<[u8; 32]> = [1u64, 2].iter().map(|&seed| s.put(generated(prompt, seed))).collect();
let mut p = gen_params(SPECTRAL, Some(c0), Some(P_ALICE), None, Some(1));
p.as_object_mut().expect("params").remove("seed");
p["seeds"] = json!([1, 2]);
s.add(act(P_SPECTRAL, "candidates", &[], json!({"set": set.iter().map(|d| hex(d)).collect::<Vec<_>>(), "width": W, "height": H}), p));
s.add(act(P_ALICE, "select", &[(0, set[0])], one(&set[1], W, H), json!({})));
refuse("selection_not_in_set_output", "a selection whose output is not the member it selected (§4.2)", s.full(&Out { step: 1, samples: set[1], w: W, h: H, content_hash: None }).2, At::Step(1), Reason::SelectionNotInSet);
let mut s = Sess::new("crop size");
let d0 = s.put(bytes("crop size canvas", W * H * 3));
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCreation"})));
let d1 = s.put(bytes("a crop of the wrong size", 5 * 4 * 3));
s.add(act(P_ALICE, "transform", &[(0, d0)], one(&d1, 5, 4), derive(CROP, json!({"x": 1, "y": 1, "w": 6, "h": 4}))));
let mut c = s.full(&Out { step: 1, samples: d1, w: 5, h: 4, content_hash: None }).2;
c.states.retain(|b| sha256(&[b]) != d0);
refuse("recompute_mismatch_size", "a crop of 6 × 4 whose output states 5 × 4, the input's samples not held (§10.2 step 6)", c, At::Step(1), Reason::RecomputeMismatch);
let mut s = Sess::new("rotation five");
let d0 = s.put(bytes("rotation canvas", W * H * 3));
s.add(act(P_ALICE, "author", &[], one(&d0, W, H), json!({"source": "digitalCreation"})));
let d1 = s.put(bytes("a rotation by five quarter turns", W * H * 3));
s.add(act(P_ALICE, "transform", &[(0, d0)], one(&d1, H, W), derive(ROTATION, json!({"quarter_turns": 5}))));
refuse("params_invalid_shape", "a rotation by five quarter turns: not a parameter of the pinned rotation (§4.4, deriving-edge §4.2)", s.full(&Out { step: 1, samples: d1, w: H, h: W, content_hash: None }).2, At::Step(1), Reason::ParamsInvalid);
let mut c = b_full.clone();
c.record = text(&b.record(&bo, &b_map, None, json!([{"estimator": "conformance-estimator", "class": "information-theoretic", "party": 9, "basis": "made_by", "share_ppm": 1}])));
refuse("party_unknown_estimate", "an estimate naming a party the session does not have (§6)", c, At::Record, Reason::PartyUnknown);
let mut c = b_full.clone();
c.record = text(&sign(with(unsigned(&b.record(&bo, &b_map, None, json!([]))), &["session"], json!(hex(&label("another session")))), &key(TOOL)));
refuse("session_mismatch_record", "a contribution record of another session (§6)", c, At::Record, Reason::SessionMismatch);
let mut c = b_full.clone();
c.record = text(&sign(unsigned(&b.record(&bo, &b_map, None, json!([]))), &key(ALICE)));
refuse("signer_mismatch_record", "a contribution record signed by a person's key instead of the tool's (§6)", c, At::Record, Reason::SignerMismatch);
let mut s = b.clone();
s.steps.truncate(2);
s.next = 2;
s.add_body(with(unsigned(&b.steps[2]), &["prev"], json!(hex(&link_hash(&b.steps[0])))), ALICE);
refuse("chain_broken_prev", "a step whose prev names the step two before it, skipping its predecessor (§5.1)", s.case(&s.record(&bo, &b_map, None, json!([])), &b_map), At::Step(2), Reason::ChainBroken);
let mut c = partial;
c.tier = Tier::Recomputed;
refuse("tier_not_met", "the partially disclosed candidates session under a policy that accepts only recomputed outcomes (§10.1)", c, At::Policy, Reason::TierNotMet);
out
}
// ---- the file ----
fn case_json(c: &Case) -> Map<String, Value> {
let mut m = Map::new();
m.insert("policy".into(), json!({"tier": c.tier.name()}));
m.insert("session".into(), json!(c.session));
m.insert("memberships".into(), json!(c.memberships));
m.insert("record".into(), json!(c.record));
m.insert("steps".into(), json!(c.steps.iter().map(|d| json!({"step": d.text, "proof": d.proof})).collect::<Vec<_>>()));
m.insert("map".into(), json!(c.map));
m.insert("openings".into(), json!(c.openings));
m.insert("states".into(), json!(c.states.iter().map(|b| hex(b)).collect::<Vec<_>>()));
m.insert("models".into(), json!(c.models.iter().map(|b| hex(&sha256(&[b]))).collect::<Vec<_>>()));
m.insert("origins".into(), json!(c.origins));
m
}
fn outcome_json(o: &Outcome) -> Map<String, Value> {
let mut m = Map::new();
match o {
Outcome::Refuse { at, reason } => {
m.insert("outcome".into(), json!("refuse"));
m.insert("reason".into(), json!(reason.code()));
m.insert("at".into(), at.to_json());
}
Outcome::Accept(st) => {
m.insert("outcome".into(), json!("accept"));
m.insert("statements".into(), st.clone());
}
}
m
}
fn case_from_json(v: &Value, models: &BTreeMap<String, Vec<u8>>) -> Option<Case> {
let strs = |k: &str| -> Option<Vec<String>> { v[k].as_array()?.iter().map(|x| x.as_str().map(String::from)).collect() };
Some(Case {
session: v["session"].as_str()?.to_string(),
memberships: strs("memberships")?,
record: v["record"].as_str()?.to_string(),
steps: v["steps"]
.as_array()?
.iter()
.map(|d| Some(Disclosed { text: d["step"].as_str()?.to_string(), proof: d["proof"].as_array()?.iter().map(|p| p.as_str().map(String::from)).collect::<Option<_>>()? }))
.collect::<Option<_>>()?,
map: v["map"].as_str()?.to_string(),
openings: strs("openings")?,
states: v["states"].as_array()?.iter().map(|x| unhex(x.as_str()?)).collect::<Option<_>>()?,
models: v["models"].as_array()?.iter().map(|x| models.get(x.as_str()?).cloned()).collect::<Option<_>>()?,
origins: strs("origins")?,
tier: Tier::from_name(v["policy"]["tier"].as_str()?)?,
})
}
/// The corpus: `vectors.json`.
pub fn generate() -> Vec<(String, String)> {
let vs = vectors();
let mut seen = BTreeSet::new();
let mut cases = Vec::new();
for v in &vs {
let got = super::verify(&v.case);
match (&v.intent, &got) {
(Intent::Accept, Outcome::Accept(_)) => {}
(Intent::Refuse(at, r), Outcome::Refuse { at: a, reason }) if at == a && r == reason => {
seen.insert(*r);
}
_ => panic!("vector {} does not reach the outcome it was written to have: {got:?}", v.name),
}
let mut m = Map::new();
m.insert("name".into(), json!(v.name));
m.insert("about".into(), json!(v.about));
m.extend(case_json(&v.case));
m.extend(outcome_json(&got));
cases.push(Value::Object(m));
}
for r in Reason::ALL {
assert!(seen.contains(&r), "no vector of the corpus is refused with {r}");
}
let doc = json!({
"about": "the conformance corpus of extensions/contribution.md §12, written by wai_contribution_vectors gen",
"models": [{"sha256": hex(&spectral_digest()), "file": MODEL_FILE}],
"vectors": cases,
});
vec![("vectors.json".into(), serde_json::to_string_pretty(&doc).expect("JSON") + "\n")]
}
/// Check the corpus's files: every case gets the outcome and the statements it states.
pub fn verify(files: &BTreeMap<String, String>) -> Vec<String> {
let mut fails = Vec::new();
let Some(doc) = files.get("vectors.json").and_then(|s| serde_json::from_str::<Value>(s).ok()) else {
return vec!["vectors.json does not read".into()];
};
let models: BTreeMap<String, Vec<u8>> = doc["models"]
.as_array()
.into_iter()
.flatten()
.filter_map(|m| Some((m["sha256"].as_str()?.to_string(), m["file"].as_str()?.as_bytes().to_vec())))
.collect();
for v in doc["vectors"].as_array().into_iter().flatten() {
let name = v["name"].as_str().unwrap_or("?");
let Some(c) = case_from_json(v, &models) else {
fails.push(format!("{name}: the case does not read"));
continue;
};
let got = Value::Object(outcome_json(&super::verify(&c)));
let mut want = Map::new();
for k in ["outcome", "reason", "at", "statements"] {
if let Some(x) = v.get(k) {
want.insert(k.into(), x.clone());
}
}
if got != Value::Object(want) {
fails.push(format!("{name}: {got}"));
}
}
fails
}

View file

@ -469,6 +469,14 @@ pub mod live_moderation;
/// cbindgen:ignore
#[cfg(feature = "synthesis")]
pub mod synthesis;
// Contribution records (extensions/contribution.md): `wai.media.contribution` — a session's parties
// and their countersigned memberships, the signed chain of steps in a Merkle tree, the authorship
// map computed by re-executing each act's dependency rule, the checks on regions, selections,
// regeneration and recomputation, and the agreement with the output's origin claim. Rust-only,
// like the live modules: its constants must not become macros in include/wai.h.
/// cbindgen:ignore
#[cfg(feature = "contribution")]
pub mod contribution;
// Forensic watermark sessions (extensions/forensic-watermark.md): `wai.live.watermark` — the edge's
// session record of the ETSI TS 104 002 A/B variants it embedded, bound to the session's grant by a
// salted commitment, the recovered sequence and the rule that matches it, and the room authority's

View file

@ -0,0 +1,61 @@
//! `wai.media.contribution` (extensions/contribution.md): the committed corpus
//! `live-conformance/contribution/` regenerates byte for byte from its fixed seeds, holds no
//! other file, every case in it gets the outcome it states, and the extension's §11 lists
//! exactly the reasons the reference reports. The generator itself asserts that the corpus
//! gives every one of them.
#![cfg(feature = "contribution")]
use std::collections::BTreeMap;
use std::path::PathBuf;
use wai::contribution::{corpus, Reason};
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn committed() -> BTreeMap<String, String> {
let dir = wai_dir().join("live-conformance/contribution");
std::fs::read_dir(&dir)
.expect("the committed corpus")
.flatten()
.map(|e| (e.file_name().to_string_lossy().into_owned(), e.path()))
.filter(|(n, _)| n != "README.md")
.map(|(n, p)| (n, std::fs::read_to_string(p).expect("a text file")))
.collect()
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
let files = committed();
let generated: BTreeMap<String, String> = corpus::generate().into_iter().collect();
let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect();
let missing: Vec<&String> = generated.keys().filter(|k| !files.contains_key(*k)).collect();
let differ: Vec<&String> = generated.iter().filter(|(k, v)| files.get(*k).is_some_and(|f| f != *v)).map(|(k, _)| k).collect();
assert!(extra.is_empty() && missing.is_empty() && differ.is_empty(), "extra {extra:?}, missing {missing:?}, differ {differ:?}: run `wai_contribution_vectors gen`");
}
#[test]
fn every_case_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// §11 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {
let text = std::fs::read_to_string(wai_dir().join("extensions/contribution.md")).expect("the extension");
let at = text.find("\n## 11. Reasons").expect("a §11");
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
let listed: Vec<String> =
text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
let want: Vec<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
assert_eq!(listed, want, "§11's table and Reason::ALL differ");
}
/// The corpus generates with the model file of `wai.generate.spectral`'s conformance vector,
/// byte for byte (§12).
#[test]
fn the_corpus_names_the_generators_model_file() {
let file = std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/generate/generate_spectral_model.json")).expect("the model file");
assert_eq!(file, corpus::MODEL_FILE.as_bytes());
}

View file

@ -224,7 +224,7 @@ fn every_decoded_output_states_a_sha256() {
("ops-conformance", "integer operators: every operator, stage and graph vector, the lower_* decodes of the sets under sets/ among them, states its output integers in full or its refusal code, never a digest of them; certificates/ states each lowered set's SHA-256 (a parameter-set identity) and holds the lowered graphs as bytes, compared byte for byte; the output digests a capability decoded through its lowering reproduces (integer-operators §14.4) are stated by that capability's own corpus"),
("signer-conformance", "key logs, grants and statuses: signed documents, no decoded output"),
("edge-conformance", "derivation records of a deriving edge: each replicate-tier output is recomputed and compared byte for byte by its verifier, and every output is named by its content hash in the record"),
("live-conformance", "live events, rooms, takes, compositions, recordings, captions, metering, steering, moderation, watermark sessions, ads and AI usage: signed records, commitments, packages, clips, cues and their renderings, per-hop records, ledgers and session receipts with their units and joules recomputed, steering policies and decisions with each pathway's evidence and the steering manifests recomputed byte for byte, review policies, decisions, notices and the items' timelines with each statement-of-reasons export written byte for byte, watermark tokens, session records, recovered sequences and disclosures, ad policies, breaks with their interstitial tags, events and asset lists written byte for byte and their impression receipts, usage statements with every term's preference, receipted envelopes (whose payloads are opaque test bytes no case decodes), room usage records, HTTP fields, catalogs, consumption claims and gate decisions, and their verdicts, and layouts and mixes stated in full, rectangle for rectangle and sample for sample; nothing decoded against a parameter set"),
("live-conformance", "live events, rooms, takes, compositions, recordings, captions, metering, steering, moderation, watermark sessions, ads, AI usage and contribution records: signed records, commitments, packages, clips, cues and their renderings, per-hop records, ledgers and session receipts with their units and joules recomputed, steering policies and decisions with each pathway's evidence and the steering manifests recomputed byte for byte, review policies, decisions, notices and the items' timelines with each statement-of-reasons export written byte for byte, watermark tokens, session records, recovered sequences and disclosures, ad policies, breaks with their interstitial tags, events and asset lists written byte for byte and their impression receipts, usage statements with every term's preference, receipted envelopes (whose payloads are opaque test bytes no case decodes), room usage records, HTTP fields, catalogs, consumption claims and gate decisions, and their verdicts, and layouts and mixes stated in full, rectangle for rectangle and sample for sample; nothing decoded against a parameter set but the contribution corpus's generated pictures, which wai.generate.spectral makes with its model file and which the corpus names, every one, by the SHA-256 of its RGB8 samples"),
("http-conformance", "objects and head proofs for transport: no case decodes a payload, and the component a fetch case picks is stated by its SHA-256"),
("cache-conformance", "cache rules: a driver, its controls and their reports, with no objects of its own"),
("byte-exact-conformance", "goldens over tests/vectors, whose sidecars state the SHA-256"),