open-standards/wai/extensions/opportunity-report.md
Transaction Science 372f063bcb wai: the opportunity report and the statement check (wai.delivery.opportunity)
extensions/opportunity-report.md: the ad breaks a channel's own signed records
prove, per owner and period (WOR1): the cues of a room a share contract names,
made into breaks by live-ads and their SCTE 35 sections read for slots, and the
program-as-run's ad entries no cue covers, each attributed by its start to the
programme on air; a documented JSON and CSV form for a platform's reported
figures, never evidence; and the check (WCK1) setting them side by side, with
the operator's signed revenue-share statement where given.

wai-rs: the opportunity_report module (feature opportunity_report), its SCTE 35
subset reader, the reported statement's reader, the corpus generator
wai_opportunity_vectors and tests/opportunity_report.rs with the worked example.
revenue_share's the_runs and asset_at become pub(crate), unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:12:30 -04:00

643 lines
38 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.

# WAI Extension: Opportunity Report
> Status: Draft. Registers the record `wai.delivery.opportunity`: the ad opportunities a linear
> channel's own signed records prove, per content owner and per period, and the check of a
> platform's reported figures against them. An **opportunity report**, `WOR1`, states for one
> party of a [revenue-share](revenue-share.md) contract and one period the ad breaks that aired in
> the programmes of the splits it shares in: each break from the cues of a room the contract
> names ([live-ads](live-ads.md) §4.3, the SCTE 35 sections they carry read for their slots) or
> from an ad entry of the channel's signed program-as-run (`wai.video.channel`), placed at its
> start on a window that aired it and attributed to the programme on air; and, per split and class,
> the breaks, their seconds and their slots, with the records each line came from. A **reported
> statement** is what a platform says it counted: a documented JSON or CSV form of impressions and
> revenue for the period, per day or per programme, which this text reads and never treats as
> evidence. A **statement check**, `WCK1`, sets the report beside the reported figures: per line,
> the opportunities, the reported impressions, the implied fill and the revenue per slot, the lines
> where a reported figure has no break behind it or lies far from the period's own rate, and, where
> the operator's signed revenue-share statement of the same party and period is given, the signed
> impressions and revenue beside the reported ones. Integer arithmetic throughout. No new
> transport, no payment, and no reading of any platform's own report format. The reference
> implementation is the `opportunity_report` module of `wai-rs` (feature `opportunity_report`);
> the corpus is [`live-conformance/opportunity/`](../live-conformance/opportunity/), and its second
> implementation is `live-conformance/opportunity_report_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: the WAI texts this one cites; ANSI/SCTE 35-1 2023r2 through the
> fields cue-injection §5 writes (its §9.6 to §9.9 and §10.2 and §10.3, as that text read them);
> ISO/IEC 13818-1 Annex A (the CRC); RFC 4180 (the CSV form of §9.2); RFC 8259; RFC 8032;
> FIPS 180-4.
>
> Why a text of its own, beside revenue-share: a report rests on records the statement does not
> read (a room's cues and the SCTE 35 sections they carry), needs no invoice, and is useful to an
> owner whose platform signs no statement; and revenue-share's records keep their bytes. Why the
> name: the record states the opportunities a channel's records prove, and names both of its
> parts, the report and the check, under one string.
## 1. Scope
In scope: the evidence a report rests on, the contract, the runs and the rooms (§3); breaks, from
a room's cues and from the program-as-run, their start, duration and slots (§4); the subset of an
SCTE 35 section this text reads, and its agreement with the cue that carries it (§5); attribution,
from a break's start to a split (§6); the report, its lines and citations (§7) and its
verification (§8); the reported statement, in JSON and in CSV (§9); the check, its lines, figures
and flags, and the signed statement beside it (§10), and its verification (§11); and the
operations (§12).
Out of scope: a transport; a parser of any platform's own report format, which turns that format
into §9's; paying an owner, which revenue-share leaves out too; an audience count, which no record
of this text states (§10.3); and the records this text reads, which their own extensions define,
verify and leave as they are. No existing record's signed bytes change.
Relationship to the core spec: `wai.delivery.opportunity` is a **record** ([SPEC](../SPEC.md) §5):
a signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
## 2. Terms
- **Contract**, **operator**, **owner**, **party**, **split**, **run**, **window**, **entry**,
**programme**, **carrier**, **ppm**, **time**, **period**: as revenue-share §2 and §3 define
them. Time is microseconds since 1970-01-01T00:00:00Z; a period is `[start, end)`.
- **Room**, **cue**, **break**, **open**, **closed**, **avail**: as live-ads §2 and §4.3.
- **Issuer**: the Ed25519 key (RFC 8032) that signs a report or a check: the owner itself, the
operator, or an auditor either chooses. A report's figures are recomputed from the records, so
an issuer's signature binds the issuer to what it states, and adds nothing a verifier trusts.
- **Section**: an SCTE 35-1 `splice_info_section()` a cue of form 1 carries (live-events §4.3).
- **Slots**: the ads one break holds (§4.4).
- **Day**: a UTC calendar date, written `YYYY-MM-DD`; its span is from its 00:00:00Z (included)
to the next date's (excluded).
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers in this text's records; the bit fields
of a section are big-endian, as SCTE 35 writes them. A **digest** is SHA-256 (FIPS 180-4); zero
is 32 zero bytes. A *text* field is a u16 length and that many bytes of UTF-8.
## 3. The evidence
### 3.1 The contract
The contract is a revenue-share contract (revenue-share §3), in force, and verified as
revenue-share §8.2 steps 1 and 2 verify one, with the other contracts given for the overlap check.
It names the channel, the key that signs its runs, its carriers, its owners, its splits and the
map from each asset to a split, and the rule for an entry that is not a programme.
### 3.2 The runs
Each run is a `wai.video.channel` receipt in its JSON carriage (revenue-share §4.1), verified as
revenue-share §8.2 step 4 verifies one: it reads, it verifies, it is of the contract's channel and
signer, and its window shares no instant with the window of a run before it.
### 3.3 The rooms
Each room is given as a live-ads verifier is given one (live-ads §9.1), without receipts: its
`descriptor`, `grants`, `key_log` (the key-log head, or `null`), `chains` and `policy`. It is
verified as live-ads §9.2 steps 1 to 3 verify one: the room (live-rooms §9), the policy, and the
schedule its accepted cues make. A room with a gap has no schedule: a schedule read from part of
the cues is not the room's, and the room is refused `delivery_gap`. The room's id is a carrier of
kind 1 of the contract, or the room is refused `room_foreign`; a room whose id is that of a room
given before it is refused `room_repeated`.
So the cues are the room's own: each signed by a principal the room's grants permit `cue`, in its
chain, and held to live-ads' schedule rules. A cue an injector signed ([cue-injection](cue-injection.md))
is such a cue; its injection record rides in the same chain, and this text does not translate it
again (cue-injection §8 does).
## 4. Breaks
### 4.1 Cued breaks
Each break of a room's schedule (live-ads §4.3) is a **cued break**:
- its **start** is the room descriptor's `t0` plus the `t` of the cue that opens it, in
microseconds since 1970; a start that does not fit in a u64 lies outside every period;
- its **duration**, in milliseconds, is a closed break's duration (live-ads §4.3: an out's, from
its in, or a break's of type 4), and an open break's avail;
- its **opening cue** is the cue whose digest names it (live-ads §4.4), and its **closing cue**
the in that closed it, when one did;
- its **section** is the opening cue's, when the cue is of form 1 (§5).
### 4.2 Scheduled breaks
The runs are taken in ascending order of `window_start_ms`, and in each the entries of its `aired`
list in ascending order. An entry of kind `ad` is a **scheduled break**, once: an entry whose leaf
(revenue-share §4.2) is the leaf of an entry taken in an earlier run is the same break and is
passed over. Its start is the entry's `start_ms` × 1000, its duration the entry's `duration_ms`,
and its run and entry those it was taken in. An entry that airs in two windows is one break.
A scheduled break is **cued** when the start of a cued break of any room given lies within its
span, from its start (included) to its start plus its duration × 1000 (excluded). A cued scheduled
break is counted by its cued break, and is not a break of its own.
### 4.3 Classes, and the period
A break is in a report when its start lies within the report's period; a break outside it is
another period's, and is passed over. Every break of a report is of one **class**:
| class | name | the break |
|---|---|---|
| 1 | **cued** | a cued break (§4.1) |
| 2 | **scheduled** | a scheduled break no cued break counts (§4.2) |
A class is never summed into another: each has its own lines (§7.2).
### 4.4 Slots
A break's **slots**, and where they come from:
1. a cued break whose section states slots (§5.2): those;
2. another cued break: its room policy's `slots` (live-ads §3.1), the most ads one break holds;
3. a scheduled break: none. Its record states no slots, and it is **unslotted**.
A section that states slots is the content's own signal of the break's placements, and is taken
over the policy, which states the most a decision fills.
## 5. The section a cue carries
### 5.1 Reading
A section is read as SCTE 35-1 Table 5 lays it out, every field in its order and its width, and
refused `cue_section_invalid` at the first of these that holds:
1. it is shorter than 20 bytes, or its `CRC_32`, the last four bytes, is not the MPEG-2 CRC
(ISO/IEC 13818-1 Annex A: polynomial 0x04C11DB7, register preset to all ones, no reflection,
no final inversion) over every byte before them;
2. its `protocol_version` is not 0;
3. its `encrypted_packet` is 1: the section is read no further, and states nothing (§5.2);
4. its command, the `splice_command_length` bytes from byte 14 (after `splice_command_type`, byte
13), runs past the CRC; a `splice_command_length` of 0xFFF states no length, and the command's
length is then what step 5 reads of it, or, for a command step 5 does not read, the section
states nothing past its CRC check;
5. the command does not read as its table lays it out, filling exactly its length:
`splice_insert()` (type 0x05): `splice_event_id`, `splice_event_cancel_indicator` and, when it
is 0, the flags `out_of_network_indicator`, `program_splice_flag`, `duration_flag` and
`splice_immediate_flag`, then `splice_time()` when the program flag is 1 and the immediate flag
0, or `component_count` and each component with its `splice_time()` when the immediate flag is
0, when the program flag is 0; then `break_duration()` (`auto_return`, `duration`) when the
duration flag is 1; then `unique_program_id`, `avail_num` and `avails_expected`;
`time_signal()` (type 0x06): one `splice_time()`; `splice_null()` (type 0x00): nothing; a
`splice_time()` is one byte when its `time_specified_flag` is 0, and five, with `pts_time`,
when it is 1. Another command is not read;
6. `descriptor_loop_length` runs past the CRC, or the loop's last descriptor does not end where
the CRC begins; a descriptor (a tag, a length, that many bytes) runs past the loop;
7. a descriptor of tag 0x02 whose first four bytes are `CUEI` does not read as a
`segmentation_descriptor()` (SCTE 35-1 Table 20) fills its length: `segmentation_event_id`,
`segmentation_event_cancel_indicator`, and, when that is 0, `program_segmentation_flag`,
`segmentation_duration_flag`, `delivery_not_restricted_flag` and the restriction bits, the
components when the program flag is 0 (six bytes each), `segmentation_duration` (40 bits) when
flagged, `segmentation_upid_type`, `segmentation_upid_length` and the UPID,
`segmentation_type_id`, `segment_num` and `segments_expected`, then, for the types 0x30, 0x32,
0x34, 0x36, 0x38, 0x3A, 0x44 and 0x46, the two bytes `sub_segment_num` and
`sub_segments_expected` when two bytes remain. Any other count of bytes remaining refuses it.
A section is read only as far as this. Another descriptor is passed over whole.
### 5.2 What a section states
- **Its id**: a `splice_insert()`'s `splice_event_id`; a `time_signal()`'s opening descriptor's
`segmentation_event_id`. The **opening descriptor** is the first segmentation descriptor of the
section not cancelled whose `segmentation_type_id` makes an out (cue-injection §6.2): 0x22, 0x30,
0x32, 0x34 or 0x36.
- **Its duration**, in 90 kHz ticks: a `splice_insert()`'s `break_duration()` `duration`, when its
duration flag is 1; a `time_signal()`'s opening descriptor's `segmentation_duration`, when
flagged.
- **Its slots**: the `sub_segments_expected` of its first segmentation descriptor not cancelled
whose `segmentation_type_id` is a placement opportunity start, 0x34 or 0x36, when that
descriptor carries the sub-segment fields and `sub_segments_expected` is above 0.
A section states nothing more. An encrypted section, a command this text does not read, and a
`time_signal()` with no opening descriptor state no id and no duration; a form 2 message and a cue
of form 0 have no section.
### 5.3 The section and the cue's summary
A producer **MUST NOT** write a summary its message contradicts (live-events §4.3). An opening
cue's section is refused `cue_section_mismatch` when:
- it is a `splice_insert()` whose cancel indicator is 1, or whose `out_of_network_indicator` is 0:
it states a cancellation or a return, and the cue opens a break;
- it states an id that is not the cue's `cue_id`;
- it states a duration *d* ticks and ⌊*d* / 90⌋ is not the cue's `duration_ms`.
## 6. Attribution
A break is attributed as revenue-share §6 attributes an impression, by its start:
- a cued break: its **run** is the run whose window holds its start, and its **entry** the entry of
that run's schedule whose span holds it. One with no run, or a run and no entry, aired in no
window the verifier holds, and is refused `outside_window`;
- a scheduled break: its run and entry are those of §4.2.
The entry gives the **asset**: a programme's own `asset_id`, or, for an entry that is not a
programme, the asset the contract's rule gives (revenue-share §6.2), or none. A break with an asset
is attributed to the asset's split; an asset the contract does not map is refused
`asset_unmapped`. A break with no asset is attributed to the contract's between split.
## 7. The opportunity report
### 7.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WOR1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `issuer` | 32 | the key that signs the report |
| `contract` | 32 | the contract's digest (revenue-share §3.3) |
| `party` | 32 | the party it is for: the operator or an owner |
| `start`, `end` | 8 each | u64: the period it states |
| `rooms` | 2 + 32 × *m* | a u16 count, then each room's id, in strictly ascending order |
| `runs` | 2 + 32 × *r* | a u16 count, then each run's identity (revenue-share §4.2), in strictly ascending order |
| `lines` | 2 + 75 × *l* | a u16 count, then each line (§7.2), in strictly ascending order of (`split`, `class`) |
| `breaks` | 8 | u64: the sum of the lines' breaks |
| `break_ms` | 8 | u64: the sum of the lines' `break_ms` |
| `slots` | 8 | u64: the sum of the lines' slots |
| `sig` | 64 | §7.4 |
### 7.2 Lines
A line is 75 bytes:
| field | size | meaning |
|---|---|---|
| `split` | 2 | u16: the split's index |
| `class` | 1 | 1 cued, 2 scheduled (§4.3) |
| `breaks` | 8 | u64: the breaks of the class attributed to the split |
| `break_ms` | 8 | u64: the sum of their durations, in milliseconds |
| `slots` | 8 | u64: the sum of their slots |
| `unslotted` | 8 | u64: those of them with no slots (§4.4) |
| `ppm` | 4 | u32: the party's share of the split |
| `cited` | 4 | u32: the records the line cites (§7.3) |
| `cites` | 32 | their citation digest (§7.3) |
There is one line for each split of which the party holds a share (revenue-share §3.2) and each
class with at least one break attributed to that split, and none other. A report so states every
opportunity of every split the party shares in, and the share by which it shares it.
### 7.3 Citations
Each line cites the records its breaks came from: for a cued break, its opening cue and its
closing cue (kind 12, each by its event digest, live-events §3.2), its room's policy when its slots
are the policy's (kind 13, the policy's digest, live-ads §3.2), and its run (kind 10, its identity);
for a scheduled break, its run (kind 10). `cited` is their number, each record counted once, and
`cites` is SHA-256 over the bytes `wai:opportunity-cites` (ASCII) and 0x01, followed by each cited
record's `kind` (u8) and identity (32), in ascending order of (`kind`, identity). Kind 10 is
revenue-share §7.4's; 12 and 13 are this text's.
### 7.4 Digest and signature
The **digest** of a report is SHA-256 over its bytes before `sig`. Its signature is Ed25519 by
`issuer` over the bytes `wai:opportunity-report` (ASCII) and 0x01, followed by the bytes the digest
covers, 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 is `bad_signature`.
### 7.5 Reading and rules
A reader reads exactly the bytes it is given as one report, in the order of §7.1, and refuses at
the first of these that holds: fewer than 4 bytes, `truncated`; a magic other than `WOR1`,
`malformed`; no fifth byte, `truncated`; a version other than 1, `unknown_version`; the bytes end
before a field the layout requires, `truncated`; bytes after `sig`, `malformed`. It then refuses
as `opportunity_report_invalid` a report that breaks any of these:
- `start` is below `end`; `issuer` and `party` are not zero;
- `rooms`, `runs` and `lines` are each in the strictly ascending order §7.1 gives it;
- each line's `class` is 1 or 2, its `breaks` and `cited` above 0, its `unslotted` at most its
`breaks`, and its `ppm` 1 to 10^6.
## 8. Verifying a report
### 8.1 Inputs
A verifier is given the **contract**, the **other contracts** it holds of the operator or the
channel, the **report**, the **runs** and the **rooms**. Its verification document holds them as
one JSON object (RFC 8259) with exactly the members `contract` and `report` (hexadecimal),
`contracts` (a list of hexadecimal strings), `runs` (a list of runs in their carriage,
revenue-share §4.1) and `rooms` (a list of rooms, each an object with exactly the members
`descriptor` and `policy` (hexadecimal), `grants` (a list of hexadecimal strings), `key_log` (a
string or `null`) and `chains` (a list of lists of hexadecimal strings, each a principal's events
in order)). A document that is not such an object is refused `malformed`.
### 8.2 Steps
It runs these steps and stops at the first refusal, which it reports with the reason and where:
1. **The contract**, at `contract`, as revenue-share §8.2 step 1.
2. **The other contracts**, each at `contracts` and its index, as revenue-share §8.2 step 2
(`contract_overlap` among its reasons).
3. **The report**, at `report`: it reads (§7.5: `truncated`, `malformed`, `unknown_version`,
`opportunity_report_invalid`); its `contract` is the contract's digest and its `party` the
operator or an owner of the contract: otherwise `contract_mismatch`; its period lies within the
contract's: otherwise `outside_contract`; its signature verifies under its issuer (§7.4):
`bad_key`, `bad_signature`.
4. **The runs**, each at `run` and its index, as revenue-share §8.2 step 4 (`malformed`,
`run_invalid`, `run_foreign`, `run_overlap`).
5. **The rooms**, each at `room` and its index: it reads (`malformed`); it verifies (§3.3), a
refusal being live-ads' reason with live-ads' place after the index, and a gap
`delivery_gap`; its id is a carrier (`room_foreign`) and not that of a room before it
(`room_repeated`).
6. **The cued breaks** in the period (§4.3), the rooms in order and each room's breaks in its
schedule's order, each at `room`, the room's index, `break` and the break's index in its
room's schedule: its section, when it has one, reads (§5.1: `cue_section_invalid`) and agrees
with its opening cue (§5.3: `cue_section_mismatch`); its span shares an instant with the span
of a cued break in the period of a room before it: `break_overlap` (two rooms that carry the
channel at once would count one break twice); it is placed and attributed (§6:
`outside_window`, `asset_unmapped`).
7. **The scheduled breaks** in the period, in the order of §4.2, each at `run`, the run's index in
the document, `entry` and the entry's index: attributed (§6: `asset_unmapped`).
8. **The figures**, at `report`, recomputed as §7.2 and §7.3 compute them for the report's party,
and compared in this order:
1. a line whose split and class give the party no break, or that states more breaks than they
give: `line_unsourced`;
2. a line whose `cited` or `cites` is not the records it came from: `citation_mismatch`;
3. any other difference (the rooms, the runs, a line missing, a figure of a line, a total):
`opportunity_report_mismatch`.
A figure that cannot be held in its field is a difference.
### 8.3 Outcome
- **refuse**: the reason and where.
- **accept**: the report's digest; the contract's digest; the issuer; the party and whether it is
the operator or an owner; the period; the rooms and the runs; each break in the period, in the
order of steps 6 and 7, with its class, its room and its index there (a cued break) or its run
and entry (a scheduled break), its opening cue's digest or `null`, its start, its duration,
whether it is open, its slots and where they came from (`section`, `policy` or `null`), what its
section states (`null` without one), its run, entry and asset, its split, and the records it
cites; each line, with its cited records; and the totals.
A break of a split the party holds no share of is stated with the others, and counted in no line
of the party's: the outcome shows each party the whole channel's breaks in the period, and its
report states those it shares in.
## 9. The reported statement
A **reported statement** is what a platform reports it counted for a channel and a period. It is
an input, never a signed WAI record and never evidence: a check (§10) names it by the digest of its
bytes, and states it beside what the records prove. A platform's own report is turned into one of
these two forms outside this text.
### 9.1 JSON
One JSON object (RFC 8259) with exactly these members, and optionally `days` and `programmes`:
| member | value |
|---|---|
| `channel` | a string: the channel's id, 1 to 1024 bytes of UTF-8 |
| `start`, `end` | integers: the period, in microseconds since 1970 |
| `currency` | a string of three capitals `A`–`Z` (ISO 4217) |
| `basis` | `gross`, for revenue before the contract's split, or `share`, for the party's part |
| `impressions` | an integer: the impressions reported for the period |
| `revenue` | an integer: the revenue reported, an amount (receipt-ledger §2: millionths of a minor unit) |
| `days` | a list of objects, each with exactly `day` (a day, §2), `impressions` and `revenue` |
| `programmes` | a list of objects, each with exactly `asset` (an `asset_id`, 1 to 1024 bytes of UTF-8), `impressions` and `revenue` |
An integer is a JSON integer from 0 to 2^64 − 1. An empty list is the same as its member left out.
### 9.2 CSV
A text of RFC 4180 records, each ended by CRLF or LF (the last line break optional), each of
exactly four fields. A field is unquoted, holding no comma, double quote, CR or LF, or quoted: a
double quote, any bytes with each double quote doubled, and a double quote. The first record is
`scope,key,impressions,revenue`. Then, in this order:
- `channel`, `start`, `end`, `currency` and `basis`, each once, its value in `key` and the last two
fields empty;
- `period`, once, `key` empty, and the period's `impressions` and `revenue`;
- each `day`, its day in `key`, then each `programme`, its asset in `key`, each with its
`impressions` and `revenue`.
An integer is `0`, or a digit 1 to 9 followed by digits, at most 2^64 − 1. The text read is the
JSON form with the same values.
### 9.3 Rules
A reported statement is refused `reported_invalid` when its text is not one of these forms, or:
- its `start` is not below its `end`;
- a day is not a date of the Gregorian calendar from 1970 to 9999, or its span does not lie within
the period, or the days are not in strictly ascending order;
- the programmes are not in strictly ascending order of `asset`'s bytes.
It is refused `reported_foreign` when its `channel` is not the contract's, its `currency` not the
contract's, or its period not the report's.
## 10. The statement check
### 10.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WCK1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `issuer` | 32 | the key that signs the check |
| `report` | 32 | the report's digest (§7.4) |
| `reported` | 32 | SHA-256 over the reported statement's bytes, as given |
| `form` | 1 | 1 JSON (§9.1), 2 CSV (§9.2) |
| `statement` | 32 | the digest of the signed revenue-share statement set beside it (revenue-share §7.5), or zero |
| `floor_ppm`, `ceiling_ppm` | 4 each | u32: the bounds of §10.3, in millionths of the period's rate |
| `lines` | 2 + … | a u16 count, then each line (§10.2), in strictly ascending order of (`scope`, `key`'s bytes) |
| `sig` | 64 | §10.5 |
### 10.2 Lines
| field | size | meaning |
|---|---|---|
| `scope` | 1 | 1 the period, 2 a day, 3 a programme |
| `key` | text | empty for the period; the day (`YYYY-MM-DD`); the programme's `asset_id` |
| `breaks`, `break_ms`, `slots`, `unslotted` | 8 each | u64: the opportunities of the scope (§10.3) |
| `impressions`, `revenue` | 8 each | u64: the reported figures of the scope |
| `fill_ppm` | 8 | u64: the implied fill (§10.3) |
| `revenue_per_slot` | 8 | u64: an amount (§10.3) |
| `signed` | 1 | 1 when signed figures follow, 0 otherwise |
| `signed_impressions`, `signed_revenue` | 8 each | only when `signed` is 1: §10.4 |
| `flags` | 1 | §10.3 and §10.4 |
There is one line of the period, and one line for each day and each programme the reported
statement lists, and none other.
### 10.3 Figures and flags
The **opportunities** of a line are the report's breaks in the period that are counted in a line
of the report's (§7.2), restricted to its scope: every one for the period; those whose start lies
within the day's span for a day; those whose asset (§6) is the programme's for a programme. A break
with no asset is in no programme's line. `breaks`, `break_ms`, `slots` and `unslotted` are their
count and sums, as §7.2 sums them.
- `fill_ppm` is ⌊`impressions` × 10^6 / `slots`⌋ and `revenue_per_slot` ⌊`revenue` / `slots`⌋,
computed in integers wide enough not to overflow, a quotient above 2^64 − 1 held as 2^64 − 1;
both are 0 when `slots` is 0. The implied fill is impressions per slot: a break's slots are
filled once per viewer, so it is the audience times the share of slots filled, and no record of
this text states an audience.
- The **rate** is the period line's `fill_ppm`.
A line's `flags` are these bits:
| bit | name | set when |
|---|---|---|
| 0 | `unproved` | `impressions` is above 0 and `breaks` is 0: a figure no break the records prove stands behind |
| 1 | `above` | not the period's line, its `slots` and the period's above 0, and `fill_ppm` × 10^6 > rate × `ceiling_ppm` |
| 2 | `below` | not the period's line, its `slots` and the period's above 0, and `fill_ppm` × 10^6 < rate × `floor_ppm` |
| 3 | `impressions_above_signed` | `signed` is 1 and `impressions` > `signed_impressions` |
| 4 | `impressions_below_signed` | `signed` is 1 and `impressions` < `signed_impressions` |
| 5 | `revenue_above_signed` | `signed` is 1 and `revenue` > `signed_revenue` |
| 6 | `revenue_below_signed` | `signed` is 1 and `revenue` < `signed_revenue` |
Every other bit is 0. `above` and `below` are relative to the period's own rate, at bounds the
issuer chooses and the check states: a day or programme far from the rest of the period. A
reported statement low in every line is low against nothing but the signed statement (§10.4).
### 10.4 The signed statement beside it
Where the operator's signed revenue-share statement of the report's contract, party and period is
given, verified as revenue-share §8 verifies it, the period's line is `signed`, and states:
- `signed_impressions`: the sum of the statement's lines' `impressions`;
- `signed_revenue`: for the basis `gross`, the sum of its lines' `revenue`; for `share`, its
`total`, the party's part.
So the three stand side by side: the opportunities the cues and runs prove, the impressions and
revenue the invoices' signed records prove, and the figures the platform reported. A day's or a
programme's line is never `signed`: the statement's lines are per split, and the party's part is
rounded once per line (revenue-share §7.3).
### 10.5 Digest and signature
The **digest** of a check is SHA-256 over its bytes before `sig`. Its signature is Ed25519 by
`issuer` over the bytes `wai:statement-check` (ASCII) and 0x01, followed by the bytes the digest
covers, verified strictly (SPEC §7 *Signatures*).
### 10.6 Reading and rules
A reader reads exactly the bytes it is given as one check, as §7.5 reads a report with `WCK1` for
`WOR1` (`truncated`, `malformed`, `unknown_version`), and refuses as `statement_check_invalid` one
that breaks any of these:
- `issuer` is not zero; `form` is 1 or 2; `floor_ppm` is at most 10^6 and `ceiling_ppm` at least
10^6;
- its first line is of scope 1 with an empty key, and no other line is of scope 1; the lines are in
the strictly ascending order §10.1 gives them; each `scope` is 1, 2 or 3; a day's key is a day
(§9.3) and a programme's 1 to 1024 bytes of UTF-8;
- `signed` is 0 or 1, and 1 only on the period's line; no bit of `flags` above bit 6 is set.
## 11. Verifying a check
### 11.1 Inputs
A check's verification document is one JSON object with exactly the members `report` (a report's
verification document, §8.1), `reported` (an object with exactly `form`, `json` or `csv`, and
`text`, the reported statement's text), `statement` (a revenue-share verification document,
revenue-share §8.1, or `null`) and `check` (hexadecimal). A document that is not such an object is
refused `malformed`. The reported statement's bytes are `text` in UTF-8.
### 11.2 Steps
It runs these steps and stops at the first refusal:
1. **The report**, at `report` and the place §8.2 gives: every reason of §8.
2. **The reported statement**, at `reported`: it reads in its form and keeps §9.3's rules
(`reported_invalid`), and is of the contract's channel and currency and the report's period
(`reported_foreign`).
3. **The signed statement**, when given, at `statement` and the place revenue-share §8.2 gives:
every reason of revenue-share §8; it is of the report's contract, party and period, or it is
refused `statement_foreign`.
4. **The check**, at `check`: it reads (§10.6: `truncated`, `malformed`, `unknown_version`,
`statement_check_invalid`); its signature verifies under its issuer (§10.5: `bad_key`,
`bad_signature`); and its `report`, `reported`, `form` and `statement` are those given and its
lines those §10.2 to §10.4 compute at its own bounds: otherwise `statement_check_mismatch`.
### 11.3 Outcome
- **refuse**: the reason and where.
- **accept**: the check's digest; the issuer; the report's digest, its party and period; the
reported statement's digest, form and basis; the signed statement's digest or `null`; the
bounds; and each line with its figures and the names of its flags.
## 12. Operations
`wai` runs the operations of this extension, under the agent rules of the operation catalogue (the
`--json` envelope, the exit codes, `--dry-run` on every operation that writes, `--yes` before a file
is replaced). A refusal by this text has its reason as the operation's code.
| operation | command line | does |
|---|---|---|
| `opportunity.create` | `wai opportunity create <inputs.json> --party <key> --start µs --end µs --key <file> [--out F]` | the report of a party and a period, computed from a verification document without its report (§8.1), signed by the issuer key, written with its document |
| `opportunity.verify` | `wai opportunity verify <document.json>` | a report's (§8.1) or a check's (§11.1) verification document: its outcome; exit 1 with the reason on a refusal |
| `statement.check` | `wai statement check <report.json> <reported> [--statement <document.json>] [--floor ppm] [--ceiling ppm] --key <file> [--out F]` | the check of a reported statement (JSON, or CSV for a file named `.csv`) against a report's verification document and, when given, a signed statement's, at the bounds given (500000 and 2000000 when not), signed by the issuer key, written with its document |
## 13. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `room_foreign` | §3.3, §8.2 step 5 |
| `room_repeated` | §3.3, §8.2 step 5 |
| `cue_section_invalid` | §5.1, §8.2 step 6 |
| `cue_section_mismatch` | §5.3, §8.2 step 6 |
| `opportunity_report_invalid` | §7.5, §8.2 step 3 |
| `opportunity_report_mismatch` | §8.2 step 8 |
| `reported_invalid` | §9.3, §11.2 step 2 |
| `reported_foreign` | §9.3, §11.2 step 2 |
| `statement_foreign` | §11.2 step 3 |
| `statement_check_invalid` | §10.6, §11.2 step 4 |
| `statement_check_mismatch` | §11.2 step 4 |
A verifier of this extension also raises, with the meaning this text gives them: `truncated`,
`malformed` and `unknown_version` for a report or a check that does not read, and `malformed` for a
document or a room that does not read; `bad_key` and `bad_signature` for their signatures;
`contract_mismatch` and `outside_contract` (revenue-share §11) for a report of another contract or
past its period; `delivery_gap` (live-events §6.4) for a room with a gap; `break_overlap` (live-ads
§4.3) for the breaks of two rooms at once; `outside_window` and `asset_unmapped` (revenue-share §11)
for a break no window places and an asset the contract does not map; `line_unsourced` and
`citation_mismatch` for a line, as revenue-share raises them; and, at a contract, a run, a room or
a signed statement, every code revenue-share and live-ads raise there.
## 14. Conformance
[`live-conformance/opportunity/`](../live-conformance/opportunity/) holds the corpus, in one file,
`vectors.json`, written by `wai_opportunity_vectors gen` from fixed seeds. The contracts, runs,
rooms, ledger documents and statements are held once, by name, and each vector is a report's or a
check's verification document naming them, with the outcome §8.3 or §11.3 gives it:
- **Accepted**: a channel's day under a contract of two owners: two windows airing each owner's
programmes, a room whose producer cues breaks inside them, with SCTE 35 sections of every kind
§5 reads (a `splice_insert()` with a break duration, a `time_signal()` with a placement
opportunity stating its slots, one whose type states none, a `splice_insert()` carrying a
segmentation descriptor) and cues of form 0, a break inside an ad entry, and an ad entry no cue
covers; the reports of both owners and of the operator, and one made from the runs alone; and
checks of reported statements in JSON and in CSV, with and without the operator's signed
statement of the day, one under-reporting an owner's programme, one reporting impressions in a
programme with no break.
- **Refused**: each reason of §13 at least once, each vector a control of one defect; each code
§13 reuses; and a refusal at a contract, a run, a room and a signed statement, at its place.
`wai_opportunity_vectors verify` and the test `tests/opportunity_report.rs` check that the file
regenerates byte for byte, that `wai-rs` gives every vector its outcome, that every reason of §13
refuses a vector, and that §13 lists exactly the reference's codes; the test also holds the worked
example, a channel's day of two owners whose reported statement under-reports one owner's breaks,
with the check that shows it, computed and then recomputed from the public records alone.
`live-conformance/opportunity_report_verify.py` is the second implementation, written from this
text: it shares no code with `wai-rs`, reads every report, check, section and reported statement
itself, verifies contracts, runs and signed statements with `revenue_share_verify.py` and rooms with
`live_ads_verify.py`, recomputes every break, attribution, line, citation and flag, gives every
vector the same outcome, 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.
## 15. Security and privacy considerations
- **What a report proves.** A break in a report is one the room's own cues opened, signed by a
principal the room's grants permit to cue, or an ad entry the channel signer signed as aired,
placed in a window the channel signer signed. Nothing of it rests on the platform's figures or
on the issuer's word: anyone holding the records computes the same report.
- **What it does not.** Opportunities are per break, not per viewer: a report says what was on air
to be sold, not how many saw it. The fill of a check is therefore impressions per slot, and its
`above` and `below` flags are relative to the period's own rate. Only a signed statement states
signed impressions; where none is given, a reported statement uniformly low or high is not
flagged by the report alone.
- **What the operator still controls.** A room the contract does not name, or a channel carried
without cues or runs, proves no opportunity; the contract's carriers and its channel signer are
what the owners agreed to. Two rooms carrying the channel at once are refused rather than
counted twice (`break_overlap`), and so are a cue whose message its summary contradicts and a
break no window places.
- **The reported statement.** It is labelled and kept apart: a check names it by the digest of its
bytes, and no figure of it changes a report. A platform that disputes a check recomputes it from
the same records and its own reported bytes.
- **What a document reveals.** A report's document holds the room's chains, whose events name the
principals of the room and its other events (chat among them, live-events §4.4); a check's holds
a signed statement's, with the ledger's sources (revenue-share §13). Each travels between the
owner, the operator, the platform and an auditor they choose.