open-standards/wai/live-conformance/stitched/README.md
Transaction Science 0ea2b9a0d6
Some checks failed
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 28s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 21s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 23s
wai-audit collector and player integration / does this change touch it (push) Successful in 21s
wai-app and Create / wai-app · does this change touch it (push) Successful in 21s
wai-fleet service / does this change touch it (push) Successful in 23s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 24s
wai-quantum studio and desktop / does this change touch it (push) Successful in 51s
WAI Studio / does this change touch it (push) Successful in 25s
wai-ledger service / does this change touch it (push) Successful in 6m25s
wai-pack / wai-pack · does this change touch it (push) Successful in 6m17s
wai-sink-swift native sink package / does this change touch it (push) Successful in 6m6s
wai-quantum for JavaScript / does this change touch it (push) Successful in 6m18s
WAI operation catalogue / does this change touch it (push) Successful in 6m28s
wai-sink-android native sink library / does this change touch it (push) Successful in 6m9s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 11s
WAI examples / WAI examples · does this change touch them (push) Successful in 8s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 45s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m4s
wai-fleet service / service · linux (push) Successful in 2m30s
wai-audit collector and player integration / collector · linux (push) Successful in 4m53s
wai-quantum studio and desktop / studio · browser (push) Successful in 4m35s
wai-ledger service / service · linux (push) Successful in 2m36s
WAI operation catalogue / the catalogue and every door · linux (push) Failing after 3m13s
wai-pack / wai-pack · linux (push) Successful in 4m24s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 7m47s
wai-quantum for JavaScript / javascript · linux (push) Successful in 10m44s
WAI Studio / Studio in WebKit · linux (push) Failing after 17m40s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 9m38s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 1m52s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 10m25s
wai-pack / wai-pack · linux-arm64 (push) Successful in 5m38s
joule-moq-room room service / joule-moq-room, every target (push) Failing after 8m36s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 8m36s
wai-pack / wai-pack · independent verifier (Python) (push) Successful in 25m47s
wai-pack / wai-pack · dependency licences (push) Failing after 29m20s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m23s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m1s
wai-fleet service / service · macos (push) Successful in 56s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Failing after 7m31s
wai-ledger service / service · macos (push) Failing after 1m23s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 8m4s
wai-pack / wai-pack · macos (push) Successful in 3m58s
WAI Studio / Studio in Chromium and Firefox · macos (push) Failing after 12m13s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m20s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 14m54s
wai-pack / wai-pack · the floor through a media prober (push) Failing after 1h3m51s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Failing after 38m8s
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
WAI examples / WAI examples · portable · linux (push) Has been cancelled
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Has been cancelled
Stitched presentation: a verification of one channel holds each pod-input record to the keys bound to it
stitched-presentation §7.6 (new), §2, §7.1, §7.2 step 3, §7.5, §9, §10, §11. A verifier may be
given a channel, a 32-byte channel id the parties name it by, and the bindings the buyer and the
seller agreed beside the signers, each a signer's key and a channel id. A verification of a
channel accepts a record only when a binding pairs its signer with that channel, and refuses any
other pod_channel_unbound (new, §9) at step 3, after pod_signer_unknown: a key bound to other
channels alone, and a key bound to none, so a binding left out refuses records rather than
admitting them. The verification states its channel id. A party that signs many channels'
breaks for one buyer under a key for each can no longer have one channel's record counted in
another channel's verification under the other channel's key. WSI1 keeps its layout and its
signature; a verification of no channel reads no binding and states what it stated before; the
receipt ledger bills a stitched source per signer, as before.

Reference: stitched_presentation::Channel (admits), verify_channel, verify_in,
Verification::channel, Reason::PodChannelUnbound; the tool's verify and the service's
POST /v1/verify read a bundle's channel and bindings. Corpus live-conformance/stitched/ gains
two vectors: channel (channel A's verification; a key bound to two channels accepted, a key
bound to none refused, its ad's claim then naming no decision) and channel_other_key (channel
B's; a key bound to B accepted, B's break signed with channel A's key refused though that key is
among the signers). It regenerates byte for byte on x86_64 (Framework) and aarch64 (Thor), and
wai-rs gives every vector its facts.

Second implementation stitched_presentation_verify.py mirrors the rule: every vector agrees,
every reason of §9 is given, 30 of 30 mutated copies fail (six new: a key bound to the channel
added, the channel changed, a binding's channel changed, the bindings dropped, the channel
dropped, a refused record stated accepted) and 18 of 18 weakened verifiers are caught (the
channel check among them). The ad-traffic and compose-ad-track second implementations, which
read it, still pass; so do the receipt-ledger, impression-aggregate, compose-ad-track,
channel-lineup, revenue-share and opportunity-report corpus tests that consume this module.

Planted faults, each caught by tests/stitched_presentation.rs: the channel check dropped; an
unbound key admitted; a binding read without its signer; a binding read without its channel;
the channel not stated; the check limited to one response format.

Decision D54: the binding is the parties' agreement read with the signers, not a channel id in
the record, fail closed, and the ledger needs nothing. Four API breaks acknowledged in the
change note (a new Reason variant, ADDED and ALL one longer, Verification::channel).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 23:51:15 -05:00

159 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# live-conformance/stitched — stitched presentation
The corpus of [`extensions/stitched-presentation.md`](../../extensions/stitched-presentation.md)
§10: streams with ads another party stitched into their segments or served as interstitials;
the manifests the players read (HLS Media Playlists, an asset list, an MPD); the ad servers'
responses (VAST documents and a form-1 `wai.video.ssai` receipt); the `WSI1` pod-input record
each break's signer signed; the `WSC1` claims the players signed, good ones and a control of
each check; what a verifier states of them.
**Presentation equivalence.** For each vector, a conforming verifier given its `records` and
`claims`, the files its `manifests`, `responses` and `receipts` name, and the corpus's
`creatives`, `issuers` and `signers` (§7.1), states exactly:
- `record_facts`, each record in the order given: its `digest` (`null` when it does not
read) and its `verdict`, `accepted` or a reason; for an accepted record, `response_checked`,
its `ads` (each `slot`, its `own_claims` by index, `presentations`, `verdict`, `quartiles`,
`presented_spend_micros` and `stated_quartile`), its `presented_spend_micros`, its
`stated_spend_micros`, its frame's `share_bp` and ad `region` (rectangles
`[x, y, w, h]`, `null` for the full picture), and, for a record of version 2, its `consent`
(`gpp`, `gpp_sid`, `child_directed`);
- `claim_facts`, each claim in the order given: its `digest`, its `verdict`, `presented` or a
reason, the `record` and `slot` it names (from §7.3 step 4 on), and, for an own claim, the
`quartile` it reaches, its `run_ms` and its `viewable_ms`;
- `spend`, the presented spend by currency;
- for a vector that states a `channel`, a verification of that channel (§7.6) under the
`bindings` the vector gives (each a `signer` key and a `channel` id, hexadecimal): its
`channel`, which it states as it is given.
`record_about` and `claim_about` say what each record and claim is, and are not checked. The
files a vector's `bound` names are documents a record binds by digest and no verifier reads
(a winning bid, a VMAP response); they are here so that anyone recomputes the digests. A
verifier that states another verdict or fact does not conform.
**Provenance.** Generated here: `wai_stitched_presentation vectors gen` writes every file from
streams authored in code, from fixed seeds. Each session key is SHA-256 of
`wai:stitched-presentation/corpus/player`, a zero byte and its number (`players` lists their
public keys); each signer's key the same with `signer` (signers 1 and 2 are the trusted ones,
`signers`; signer 9 is not); each channel id SHA-256 of `wai:stitched-presentation/corpus/channel`,
a zero byte and the channel's name (`A`, `B`, `C`); each media object's content hash SHA-256 of a fixed label and two
numbers; each token's nonce SHA-256, and its salt SHA-384, of a fixed label and its number. The
attestation issuer's key is the test key RFC 9578 Appendix A.2 publishes, as in
[`../presentation/`](../presentation/). Every playlist, asset list, MPD, VAST document, bid
document and receipt is a fixture written for its case under example domains; none was fetched
from a service. Ed25519, BLAKE3 and RSASSA-PSS over a given salt are deterministic, so every
file is the same bytes on every machine. The generator asserts that each record and claim gets
the verdict it was written to have and that the corpus gives every reason of §9, before it
writes. No recorded data.
## Layout
- `vectors.json`: `creatives`, each its `objects` (`content_hash`, `duration_ms`); `issuers`;
`signers` (Ed25519 public keys, hexadecimal); `players` (informative); and `vectors`.
- `NAME.*`: each vector's documents: `.m3u8` Media Playlists, `.assets.json` an asset list,
`.mpd` an MPD, `.vast.xml` VAST 4.3 responses, `.vmap.xml` a VMAP response, `.bid.json` a
winning bid's document, `.receipt.json` a form-1 receipt in its JSON carriage (§4.5).
The vectors:
- `hls_stitched`: two viewers' personalised playlists, each ad under a date range whose
`X-AD-ID` carries its `AdServingId`; a record per viewer priced from its VAST `Pricing`
(CPM 25.00, 18.00 and 30.00 USD). Presented: viewer 1's two ads (the second to its third
quartile) and viewer 2's first. Viewer 2's second ad was fetched ahead of the break and never
presented: `claim_absent`, no spend. Record controls: `pod_signer_unknown`,
`pod_response_mismatch` (a price the response does not give; a universal creative identifier
it does not carry), `pod_duplicate`, `pod_input_malformed`, `bad_signature`. Claim controls:
`claim_duplicate`, `claim_key_unbound`, `claim_session_unbound`, `claim_no_decision`,
`claim_manifest_unknown`, `claim_marker_absent`, `stitched_claim_malformed` (truncated; a
signed skip with no resumption), `bad_signature`.
- `hls_interstitial`: one shared playlist with an interstitial date range, and the asset list a
viewer was served; records under the cross-media rule with an audible run and attestation,
ad-i1 at a winning bid's price (its document bound by digest), ad-i2 at CPM 20.00.
Presented: viewer 11's two, attested on a television. Controls: `attestation_absent`,
`attestation_invalid` (another presentation's token).
- `squeezeback`: a squeeze on a DASH channel, the program in a 1440 × 810 window in the bottom
right corner and the ad in the L around it (4375 basis points of the picture), marked by an
event whose `messageData` carries the ad's `AdServingId`; one shared record. Presented:
viewers 21, 22 (70% of the region in view) and 23 (to the midpoint): three presentations.
Controls: `claim_frame_mismatch`, `claim_not_viewable`, `claim_duplicate`,
`claim_asset_mismatch`.
- `double_box`: a double box on an HLS channel, its record in EUR at a price the signer states,
its VMAP response bound and not read. Presented: viewer 31 over the ad's box, viewer 32 over
the player's area.
- `form1_prefetch`: a stitcher's own decision sealed in a form-1 receipt whose beacons counted
ad-f2 as started when its first segment was fetched ahead of the break; viewer 41 left after
ad-f1. Stated: quartiles 5 and 1, 5 000 000 µ-units; presented: ad-f1 alone, 3 000 000.
Control: `pod_response_mismatch` (the same receipt at another price).
- `consent`: two viewers' personalised playlists, each with one stitched ad, and a record of
version 2 per viewer (§4.7): viewer 51's carries the GPP string its player gave, the example
the GPP string's text gives for a service reached by a URL, with section 2; viewer 52's no
string and a child-directed request. Presented: both. Controls: viewer 53's record with its
GPP string changed after signing (`bad_signature`), whose ad's claim then names no decision
(`claim_no_decision`); a shared record carrying a viewer's string, and a record stating a
section with no string (each `pod_input_malformed`).
- `channel`: channel A's verification (§7.6), one account's linear HLS channel with two shared
breaks of one ad each, as its live playlist carried each, and one VAST response for both. The
bindings pair signer 1 with channels A and C and name signer 2 nowhere. Presented: the 12:05
break's ad-a, signed with signer 1's key, by viewers 61 and 62 (5 000 000 µ-units). Control:
the 12:10 break's record, signed with signer 2's key, `pod_channel_unbound`, and viewer 61's
claim of its ad `claim_no_decision`.
- `channel_other_key`: channel B's verification, the account's second channel, the bindings
pairing signer 1 with channel A and signer 2 with channel B. Presented: the 12:05 break's
ad-c, signed with signer 2's key, by viewer 63 (3 000 000). Control: the 12:10 break signed
with channel A's key, `pod_channel_unbound` though that key is among the signers, and viewer
63's claim of its ad `claim_no_decision`.
## Running
```
cd ../../wai-rs
cargo run --no-default-features --features stitched_presentation --bin wai_stitched_presentation -- vectors verify ../live-conformance/stitched
cargo run --no-default-features --features stitched_presentation --bin wai_stitched_presentation -- vectors gen ../live-conformance/stitched # regenerate
cargo test --no-default-features --features stitched_presentation --test stitched_presentation
cd ../live-conformance
python3 -I stitched_presentation_verify.py stitched
node --test ../../joule-code/joule-code-rs/crates/joule-moq-web/web/ads/stitched.test.mjs
```
`vectors verify` and `tests/stitched_presentation.rs` require every file to regenerate byte for
byte, no other file but this one to be there, and `wai-rs` to give every vector its facts; the
test also requires §9 to list exactly the codes the reference adds.
`stitched_presentation_verify.py` is the second implementation, written from the extension's
text and sharing no code with `wai-rs`: it reads every manifest, response, record and claim
itself (with the spans' quartile and run and RSASSA-PSS of `ad_presentation_verify.py`, a
form-1 receipt's leaves of `live_ads_verify.py` and the ad frame of `live_compose_verify.py`),
requires every reason to appear, then mutates copies of the corpus, one defect each, each of
which must fail, and drops each of its own checks in turn, each weakened verifier of which the
corpus must catch. The browser client's reference producer requires the corpus's claims from
the corpus's manifests and observation sequences. Pass = an exit status of 0 from each.
## The tool's inputs
`wai_stitched_presentation verify <bundle.json>` (and `POST /v1/verify` of the service) reads one
JSON object:
| member | value |
|---|---|
| `records` | the pod-input records, hexadecimal |
| `claims` | the claims, hexadecimal, at most 2^20 |
| `manifests` | the manifests' texts, as strings |
| `responses` | the VAST and VMAP documents' texts, as strings |
| `receipts` | form-1 receipts in their JSON carriage (§4.5), as objects |
| `creatives` | as `vectors.json`'s |
| `issuers` | as `vectors.json`'s |
| `signers` | the accepted signers' keys, hexadecimal |
| `channel` | optional: the channel id of a verification of a channel (§7.6), hexadecimal |
| `bindings` | with a `channel`: the bindings, each `{"signer", "channel"}`, hexadecimal |
and prints `record_facts`, `claim_facts` and `spend` as this corpus states them, and the
`channel` of a verification of one, with the `capability`. `wai_stitched_presentation sign-pod <draft.json> --key <seed-file>` reads a draft
of a record: the fields of §4.1 by name (`carriage`, `session` in hexadecimal or absent for a
shared break, `avail_id`, `avail_ms`, `response_format`, `response_file` (a path beside the
draft whose SHA-256 is taken) or `response` (hexadecimal), `id_kind`, `id_attr`, `currency`,
`exponent`, `frame` (its fields, or absent for the full picture) and `requirement`
(`require`, `rule`, `token_type`, `issuer`)), and `ads`, each with `marker` as its fields
(`id` and `start` for carriage 1; and `index` for 2; `period`, `scheme`, `value` and `id` for
3), `ad_id`, `serving_id`, `creative_id`, `registry`, `creative_hash`, `duration_ms`,
`price_micros`, `price_source` and `evidence`; and, for a record of version 2, `consent`, as
live-ads §8.6's JSON carriage writes it (`gpp`, `gpp_sid`, `child_directed`). It refuses a draft
that breaks §4.4, and prints the signed record in hexadecimal.