# WAI Extension: Channel Lineup and Guide

> Status: Draft. Registers the record `wai.video.lineup`: a linear channel's signed **lineup**,
> `WLU1`, which describes each programme asset of one channel schedule (its title, description,
> categories, rating, artwork by content hash, where it may be shown, and its rights holder) and
> where each programme of the guide begins, and which binds that schedule by its root. It
> defines how a channel as operators exchange it (an XMLTV schedule, Media RSS catalogues, a
> JSON form of both, and the SCTE-35 date ranges of the channel's HLS Media Playlist) is
> **imported** into a `wai.video.channel` receipt and a lineup; and the channel's **guide**, an
> XMLTV document or a now/next document for a player's guide row, rendered from a receipt and its
> lineup, naming both, so that anyone holding them recomputes the guide and compares. The
> `wai.video.channel` receipt keeps its signed bytes and travels in revenue-share's JSON carriage
> of it. No new transport. The reference implementation is the `channel_lineup` module of `wai-rs` (feature
> `channel_lineup`); the corpus is [`live-conformance/channels/`](../live-conformance/channels/),
> and its second implementation is `live-conformance/channel_lineup_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> This revision adds the **cue tags** of a Media Playlist of the channel (§7.4): a packager that
> writes the channel's segments, MPEG-2 transport stream segments among them, marks each break of
> the receipt's schedule with `EXT-X-CUE-OUT` and the break's duration, `EXT-X-CUE-OUT-CONT` on each
> later segment of the break and `EXT-X-CUE-IN` on its return, and, for a break whose cue carries
> its SCTE-35 section, with the `EXT-X-DATERANGE` tags of RFC 8216 §4.3.2.7.1; a verifier given
> the receipt, its cues and the playlist recomputes the tags and compares (§8.4). An import, a
> lineup and a guide are unchanged.
>
> Texts read for this revision, each at its source:
>
> - the XMLTV DTD, `xmltv.dtd`: its date and time format, and the elements `tv`, `channel`,
>   `display-name`, `programme`, `title`, `desc`, `category`, `icon`, `episode-num`, `rating`
>   and `value`, with their attributes and the order the DTD gives a programme's children;
> - *Media RSS Specification* 1.5.1, December 2009: §4.1 (`media:group`), §4.2
>   (`media:content`), §5 (where its optional elements appear), §5.2 (`media:rating`), §5.3
>   (`media:title`), §5.4 (`media:description`), §5.6 (`media:thumbnails`), §5.7
>   (`media:category`) and §5.13 (`media:restriction`); and the RSS 2.0 elements it extends
>   (`rss`, `channel`, `item`, `title`, `description`, `category`, `guid`);
> - `draft-pantos-hls-rfc8216bis-22`: §4.2 (attribute lists, decimal-floating-point and
>   hexadecimal-sequence values), §4.4.5.1 (`EXT-X-DATERANGE`) and §4.4.5.1.1 (SCTE-35 in a
>   date range);
> - ANSI/SCTE 35-1 2023r2: §9.6 (Table 5, `splice_info_section()`) and §9.7.3 (Table 10,
>   `splice_insert()`);
> - RFC 8216 (HTTP Live Streaming): §4.2 (attribute lists), §4.3.2.1 (`EXTINF`), §4.3.2.6
>   (`EXT-X-PROGRAM-DATE-TIME`), §4.3.2.7 (`EXT-X-DATERANGE`), §4.3.2.7.1 (SCTE-35 in a date
>   range), §4.3.3.1, §4.3.3.2 and §4.3.3.4 (`EXT-X-TARGETDURATION`, `EXT-X-MEDIA-SEQUENCE`,
>   `EXT-X-ENDLIST`), §6.2.1 and §6.3.1 (the server's and the client's general rules), §7 (protocol
>   versions) and §8.10 (its example of a date range carrying SCTE-35);
> - RFC 8259 (JSON), RFC 8785 (the JSON Canonicalization Scheme), and XML 1.0 for the subset of
>   §3.3.
>
> A section number of one of these texts is that revision's.

## 1. Scope

In scope: the bounds and value forms an import reads (§3); the import of a channel's schedule,
catalogue and ad breaks into a schedule, its assets and its cues (§4); the JSON carriage a
`wai.video.channel` receipt travels in, which is revenue-share's (§5); the lineup, its rules, its signature and how it binds its
schedule (§6); the guide, in XMLTV and as a now/next document (§7), and the cue tags of a Media
Playlist of the channel (§7.4); verification of a receipt, a
lineup and a guide together (§8), and of a playlist's cue tags (§8.4); the operations that do
each, with a worked example (§9).

Out of scope: a transport; how feeds travel between operators; playout, which turns a schedule
into segments; the ad decision of a break, which [live-ads](live-ads.md) and `wai.video.ssai`
make; the revenue an asset's rights holder is owed; how a packager cuts and encodes segments,
which §7.4 takes as given; interstitial date ranges, which [stitched-presentation](stitched-presentation.md)
reads; SCTE 224 audience and blackout rules (a lineup states where an asset may be shown, and
says nothing about when); and the elements of the XMLTV DTD and of Media RSS that §4 does not
name, which an import does not read.

Relationship to the core spec: `wai.video.lineup` 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.
A guide is not a record: it is a rendering of two records that anyone recomputes, so it carries
no signature of its own.

Relationship to `wai.video.channel` (SPEC §5): an import seals a channel receipt with
`ChannelReceipt::seal`, whose signed bytes are unchanged here, and carries it in the JSON form
[revenue-share](revenue-share.md) §4.1 defines for it; a lineup binds the receipt's
schedule by its `schedule_root`. A receipt's program-as-run (`aired`) is what a verification
sets beside a guide (§8.3).

Relationship to [live-events](live-events.md): each ad break an import reads becomes a `cue`
payload of live-events §4.3, unchanged, which a room's principal signs as a `cue` event when the
channel is delivered as a room. Relationship to [cue-injection](cue-injection.md): its injection
record (§7 there) binds a cue to the SCTE 104 message it was translated from; a cue an import
yields comes from a playlist's SCTE-35 date range, and an import makes no injection record.

## 2. Terms

- **Channel**: a linear channel, named by a **channel id**.
- **Schedule**: the entries of a `wai.video.channel` receipt, gapless and non-overlapping, each
  an asset id, a start, a duration and a kind: program, ad or filler.
- **Programme**: a span of the schedule the guide lists as one item: an episode or a film, with
  the ad breaks inside it. A programme's **asset** is the asset id of its program entries.
- **Asset**: what an asset id names; the lineup describes each programme asset (§6.2).
- **Catalogue**: the assets a channel's content owners describe, in Media RSS or the JSON form.
- **Break**: a span of a programme that ads fill, from a date range or the JSON form (§4.5).
- **Operator**: the party that runs the channel and signs its receipt and its lineup.
- **Rights holder**: the party an asset's lineup entry names as holding its rights.
- **Feed**: a document an import reads: an XMLTV document, a Media RSS document, the JSON form,
  or a Media Playlist.
- **Guide**: an XMLTV document (§7.2) or a now/next document (§7.3) rendered from a receipt and a
  lineup.

An instant is a count of milliseconds since 1970-01-01T00:00:00Z, leap seconds not counted. An
instant or a duration an import reads, a lineup states or a guide writes is at most 2^53 − 1,
the integers JSON carries exactly (RFC 8785 §3.2.2.3). A span from `a` to `b` holds the instants from `a`, included, to `b`,
excluded. Integers of the lineup are little-endian. A digest is SHA-256 (FIPS 180-4), 32 bytes.
Ed25519 is RFC 8032's, 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`. Hexadecimal
written by this text is lowercase.

## 3. Reading

### 3.1 Bounds

An import refuses `import_oversized` a feed or an artwork file of more than 16 777 216 bytes, and
a feed that holds more than 10 000 programmes of the channel (XMLTV, the JSON form), more than
10 000 items (Media RSS, all its documents together), more than 10 000 assets (the JSON form) or
more than 10 000 breaks (the JSON form and the playlist together). The size of each feed is
checked before it is read, and each count as soon as its document has been read as XML, JSON or
lines, before any of its elements is read further. An XML document is read to a depth of 64
elements at most (§3.3). An import therefore reads a bounded amount for any input.

### 3.2 Text values

A **text** is a sequence of Unicode scalar values in UTF-8 with no character below U+0020 other
than U+0009 (tab) and U+000A (line feed). A **short text** is a text of 1 to 255 bytes; a
**long text** a text of 0 to 65 535 bytes. A value an import reads that must be one of these and
is not is refused `import_field_invalid`. Text is taken as the feed writes it; nothing is
normalised.

### 3.3 The XML subset

An XMLTV or Media RSS document is read over the XML subset of
[stitched-presentation](stitched-presentation.md) §3.5, with its local names and its text of an
element, and with one addition: before the root element, where that subset admits a comment, the
document **MAY** hold one document type declaration with no internal subset, which is skipped.
Such a declaration is `<!DOCTYPE`, white space, a name (an ASCII letter or `_`, then ASCII
letters, digits, `_`, `-`, `.` and `:`), and then optionally white space and either `SYSTEM`,
white space and a literal, or `PUBLIC`, white space, a literal, white space and a literal;
then optional white space and `>`. A literal is `"`, bytes other than `"`, and `"`; or `'`,
bytes other than `'`, and `'`. White space is one or more of space, tab, CR and LF. A
declaration of any other form (one with an internal subset among them), or a second one, is
outside the subset. A document outside the subset, or whose root element is not the one §4.2 or
§4.3 names, is refused `import_xml_malformed`.

An element's **first** `x` child is its first child element of local name `x`. Elements are
named by local name only, so a Media RSS element and an RSS 2.0 element of one local name
(`media:title` and `title`, for one) are both `title` children; §4.3 says which one an import
takes.

### 3.4 Times and durations

An **XMLTV time** is 14 ASCII digits `YYYYMMDDhhmmss`, optionally followed by one space and a
numeric zone: `+` or `-` and four digits `HHMM` with `HH` at most 23 and `MM` at most 59. With no
zone the time is UTC, as the DTD says. The instant is the UTC instant of the date and time, less
the zone's offset. A value is refused:

- `import_timezone_unknown` when its first 14 bytes are digits and what follows them is a space
  and anything other than a numeric zone (a zone named by letters, for one);
- `import_time_malformed` otherwise when it is not of that form, when its year is below 1970, its
  month not 1 to 12, its day not a day of that month in the Gregorian calendar, its hour above 23,
  its minute above 59 or its second above 59, or when its instant is before 1970-01-01T00:00:00Z.

An **HLS date-time** is `YYYY-MM-DDThh:mm:ss`, optionally `.` and one or more digits, then `Z` or
a zone `+hh:mm` or `-hh:mm` with `hh` at most 23 and `mm` at most 59, the fields as above. Its
fraction is read to the millisecond, and every digit after the third **MUST** be 0. A value of
this form without a zone is refused `import_timezone_unknown`; any other value that is not of
this form, or breaks a rule above, is refused `import_time_malformed`.

A **duration** is a decimal-floating-point value (`draft-pantos-hls-rfc8216bis-22` §4.2): one or
more digits, optionally `.` and one or more digits, a number of seconds. It is read to the
millisecond, every fractional digit after the third **MUST** be 0, and the count of milliseconds
**MUST** be at most 2^53 − 1; otherwise it is refused `import_time_malformed`.

## 4. Import

### 4.1 Inputs

An import is given:

- **channel**: the channel id, a short text;
- **feeds**, one of these sets, each optionally with one Media Playlist:
  - an XMLTV document, and zero or more Media RSS documents;
  - one or more Media RSS documents, with no XMLTV document;
  - one JSON form (§4.4), and no XMLTV or Media RSS document;
- for each Media RSS document, optionally its **owner**, the rights holder of its items: a short
  text;
- **owner**: optionally, the rights holder of every asset that no other input gives one: a short
  text;
- **id_system**: the XMLTV `episode-num` system that names a programme's asset (§4.2); when not
  given, `wai-asset-id`, the system a guide writes (§7.2);
- **xmltv_channel**: the XMLTV channel id whose programmes are read; when not given, `channel`;
- **start**: an instant, which a schedule made of Media RSS items alone requires (§4.6);
- **artwork**: a set of files, each named by a file name (§4.6);
- **window**: optionally, the receipt's window, a span from `window_start` to `window_end` (§4.9);
- the operator's Ed25519 key and the receipt's `signer_id` (a short text), and optionally the
  receipt hash of the window before this one, its `parent_receipt_hash`.

An input set that is none of the sets above, a Media RSS owner given for some of the Media RSS
documents and not for the others, a value above that is not of its form, or a schedule of Media
RSS items alone with no start, is refused `import_inputs_invalid`.

### 4.2 An XMLTV schedule

The document's root element is `tv`. Its **channel name** is the text of the first
`display-name` child of the first `channel` child of `tv` whose `id` attribute is
`xmltv_channel`, when that text is not empty; it **MUST** then be a short text. The import's
**programmes** are the `programme` children of `tv` whose `channel` attribute is
`xmltv_channel`, in document order; when there are none, the import is refused `import_empty`.
Each one is read, in document order, by these steps, and refused at the first that fails:

1. Its `start` attribute is an XMLTV time (§3.4); an absent `start` is `import_time_malformed`.
2. It has a `stop` attribute: otherwise `import_no_stop`. That attribute is an XMLTV time (§3.4),
   and its instant is after the start's: otherwise `import_time_malformed`.
3. **Its asset.** It has an `episode-num` child whose `system` attribute is `id_system` (an
   absent `system` is `onscreen`, the DTD's default), and the text of the first such child is a
   short text: otherwise `import_asset_unknown`. That text is the programme's asset id.
4. **Its description**, each value as §3.2 says (`import_field_invalid`): its **title**, the text
   of its first `title` child, a long text of 1 byte or more; its **description**, the text of
   its first `desc` child, or empty when it has none, a long text; its **categories**, the text
   of each `category` child, in order, each a short text, at most 16 of them; its **artwork**,
   the `src` attribute of each `icon` child, in order, each a long text of 1 byte or more, at
   most 8 of them (an `icon` with no `src` is refused); and its **rating**, when it has a
   `rating` child: the first one's `system` attribute, or empty when it has none, which when
   not empty is a short text; and the text of that child's first `value` child, a short text
   (a `rating` with no `value`, or an empty one, is refused).

Every instant an XMLTV document gives is a whole second.

### 4.3 A Media RSS catalogue

The document's root element is `rss`, with a `channel` child; the first `channel` child is
read. Its **channel name** is the text of that channel's first `title` child, when not empty; it
**MUST** then be a short text. Its **items** are that channel's `item` children, in document
order. Each one is read, in document order, by these steps, and refused at the first that fails:

1. **Its asset id** is the text of its first `guid` child (RSS 2.0's `guid`), a short text:
   otherwise `import_asset_unknown`. An asset id that an item read before it has, in this
   document or in an earlier one, is refused `import_asset_duplicate`.
2. **Its content** is its first `content` child (`media:content`, §4.2 there), or, when it has
   none, the first `content` child of its first `group` child (`media:group`, §4.1 there). Its
   **content URI** is the content's `url` attribute, a long text, empty when absent or when it
   has no content. Its **duration** is the content's `duration` attribute read as a duration
   (§3.4; `import_time_malformed`), which **MUST** be at least 1 millisecond
   (`import_time_malformed`); with no such attribute it is unknown.
3. **Its description**, each value as §3.2 says (`import_field_invalid`): its title, the text of
   its first `title` child, a long text of 1 byte or more; its description, the text of its
   first `description` child, or empty, a long text; its categories, the text of each `category`
   child, each a short text, at most 16; its artwork, the `url` attribute of each `thumbnail`
   child (`media:thumbnail`, §5.6 there), each a long text of 1 byte or more, at most 8 (a
   `thumbnail` with no `url` is refused); its rating, when it has a `rating` child
   (`media:rating`, §5.2 there): the first one's `scheme` attribute, or `urn:simple` when absent
   (§5.2's default), which when not empty is a short text; and its text, a short text.
4. **Where it may be shown**, from its first `restriction` child whose `type` attribute is
   `country` (`media:restriction`, §5.13 there): its `relationship` attribute is `allow` or
   `deny`, and its text, split at white space (space, tab, CR, LF) into non-empty tokens, is
   either the one token `all` or the one token `none`, or one or more tokens of two ASCII
   letters each: otherwise `import_field_invalid`. Two letters name the ISO 3166-1 alpha-2 code
   they spell in upper case, and the codes are taken in increasing order, each once. `allow` with `all`, `deny` with `none`, or no such child: shown
   everywhere; `allow` with `none`, or `deny` with `all`: shown nowhere; `allow` with codes:
   shown only in those countries; `deny` with codes: shown everywhere but those.

The item's title is RSS 2.0's `title` or Media RSS's `media:title`, whichever the item writes
first, and so for `description` and `category`. Elements that a `media:group` or a
`media:content` holds, other than the content itself, are not read.

### 4.4 The JSON form

The JSON form carries a channel's schedule, its catalogue and its breaks in one document. It is
one JSON text (RFC 8259) in UTF-8, nested at most 64 deep, whose objects each name a member once.
Its value is an object with exactly these members:

| member | value |
|---|---|
| `kind` | the string `wai-channel-feed` |
| `channel_name` | a string |
| `programmes` | an array of objects, each with exactly `start_ms` and `stop_ms` (integers) and `asset_id` (a string) |
| `assets` | an array of objects, each with exactly the members of the next table |
| `breaks` | an array of objects, each with exactly `id` (a string), `start_ms` and `duration_ms` (integers) and `scte35` (a string, or `null`) |

| asset member | value |
|---|---|
| `asset_id`, `title`, `description`, `content_uri` | strings |
| `categories` | an array of strings |
| `rating` | `null`, or an object with exactly `system` and `value`, strings |
| `artwork` | an array of objects, each with exactly `uri` and `sha256`, strings |
| `geo` | `null` (shown everywhere), or an object with exactly one member, `allow` (shown only in) or `deny` (shown everywhere but), an array of strings |
| `rights_holder` | a string, or `null` |
| `duration_ms` | an integer: the asset's duration, 0 when unknown |

An integer is written as decimal digits with no sign, fraction, exponent or leading zero, at most
2^53 − 1. A document that is not so is refused `import_json_malformed`. Then, in document order,
each value is read as §3.2 and this paragraph say (`import_field_invalid` unless said otherwise):
`channel_name` a short text; each programme's `asset_id` a short text, with `stop_ms` after
`start_ms` (otherwise `import_time_malformed`); each asset's `asset_id` a short text (one that an
asset before it has is `import_asset_duplicate`), its `title` a long text of 1 byte or more, its
`description` and `content_uri` long texts, its categories as in §4.2, its `rating`'s `system` a
short text or empty and its `value` a short text, its artwork at most 8, each `uri` a long text
of 1 byte or more and each `sha256` 64 lowercase hexadecimal digits, its `geo` codes each two
upper-case ASCII letters in strictly increasing order (a `deny` with none is refused), and its
`rights_holder` a short text when not `null`; each break as §4.5 says of a break.

### 4.5 Breaks from a Media Playlist

A Media Playlist is read as stitched-presentation §3.2 reads one: UTF-8, split at each line feed
with a CR before it dropped, its first line `#EXTM3U` (otherwise `import_playlist_malformed`).
Each line that begins `#EXT-X-DATERANGE:` is a date range tag, and the rest of the line its
attribute list, read by the rules of stitched-presentation §3.2; a tag whose list does not read,
or whose `ID` is not a quoted string, is refused `import_playlist_malformed`.

Tags are grouped by `ID`, each group in the order its first tag appears. Two tags of one group
that carry one attribute **MUST** carry the same value, a quoted string or not as both write it
(`draft-pantos-hls-rfc8216bis-22` §4.4.5.1): otherwise `import_playlist_malformed`. A group is a
**break** when one of its tags carries `SCTE35-OUT`; the other groups are not read. A break's
attributes are those of its tags together, and it is read by these steps, each refused at the
first that fails:

1. Its **id** is its `ID`, a short text (`import_field_invalid`).
2. Its `START-DATE` is a quoted string, an HLS date-time (§3.4): an absent one, or one that is
   not quoted, is `import_playlist_malformed`. Its instant is the break's **start**.
3. Its `SCTE35-OUT` (and its `SCTE35-IN`, when it has one) is an unquoted hexadecimal-sequence
   (§4.2 there: `0x` or `0X`, then hexadecimal digits, read here in either case) of an even
   count of digits, whose bytes are a `splice_info_section()` as live-events §4.3 reads form 1:
   `table_id` 0xFC, and `section_length` (the low 12 bits of its bytes 1 and 2) plus 3 its
   length. Otherwise `import_scte35_malformed`. The bytes of `SCTE35-OUT` are the break's
   **section**.
4. Its **duration** is its `DURATION`, unquoted, when it has one; otherwise its `END-DATE`, a
   quoted HLS date-time, less its start, when it has one; otherwise its `PLANNED-DURATION`,
   unquoted. A duration attribute that is quoted is `import_playlist_malformed`, and one that
   does not read is refused as §3.4 says. A break with none of the three, or whose duration is 0
   or above 2^32 − 1 milliseconds, or an `END-DATE` before its start, is refused
   `import_break_duration`.

A break of the JSON form is its `id` (step 1); its `start_ms`; its `scte35`, when not `null`,
an even count of hexadecimal digits (either case, no `0x`) read as step 3 reads a section
(`import_scte35_malformed`); and its `duration_ms`, refused as step 4 refuses one when 0 or above
2^32 − 1. A break of the JSON form with a `null` `scte35` has no section.

### 4.6 The programmes and their assets

The programmes are, by the feeds:

- **XMLTV**: those of §4.2;
- **the JSON form**: its `programmes`, each the span from `start_ms` to `stop_ms`, with its `asset_id`; none is
  `import_empty`;
- **Media RSS alone**: the items of the documents in the order given, each document's in
  document order, one programme an item, back to back from `start`: each programme begins where
  the one before it ends and lasts its item's duration. An item whose duration is unknown is
  refused `import_duration_unknown`, and an instant above 2^53 − 1 `import_time_malformed`. No
  item is `import_empty`.

The programmes are ordered by start (programmes of one start keep their order), and each next to
the one after it: a programme that ends before the next begins is refused `import_gap`, and one
that ends after the next begins `import_overlap`. Every programme's start, and the last one's
end, **MUST** be a whole second, a multiple of 1000 (`import_time_malformed`): a guide's XMLTV
times have no finer unit (§7.2).

Each distinct asset id of the programmes, in the order of the first programme that names it, gets
its lineup entry (§6.2) by these steps, refused at the first that fails:

1. **Its description.** With the JSON form, its `assets` entry, which **MUST** exist
   (`import_asset_unknown`). With XMLTV, the item of that asset id when a Media RSS document has
   one, otherwise the first programme that names it. With Media RSS alone, its item. The
   description is title, description, categories, artwork, rating, where it may be shown,
   content URI and duration; a programme gives no content URI, a duration of 0 and shown
   everywhere.
2. **Its rights holder**: the JSON form's `rights_holder` when not `null`; otherwise the owner of
   the Media RSS document its item came from, when given; otherwise the import's owner, when
   given. None is refused `import_owner_unknown`.
3. **Its artwork.** Each artwork URI of a description that came from XMLTV or Media RSS is bound
   by its content hash: its **name** is the part of the URI after its last `/`, once the first
   `?` or `#` and everything after it are removed. The name **MUST** be 1 to 255 bytes of ASCII
   letters, digits, `.`, `_` and `-`, other than `.` and `..`, and the artwork set **MUST** hold a
   file of that name; otherwise `import_artwork_missing`. A file of more than 16 777 216 bytes is
   `import_oversized`. The artwork's `sha256` is the SHA-256 of that file's bytes. The JSON form
   gives its artwork's `sha256` itself.

Programmes and assets are read once each: an asset's later programmes give it nothing, and
catalogue items no programme names are read (§4.3) but enter no lineup.

### 4.7 The schedule

The breaks are those of the JSON form, then those of the playlist, ordered by start (breaks of
one start keep their order). A break whose end (its start and its duration) is after the next
break's start is refused `import_overlap`; a break that does not lie within one programme, a
programme whose start is at most its start and whose end is at least its end, is refused
`import_break_outside`.

The **schedule** is, for each programme in order: a program entry from the programme's start, or
from the end of its previous break, to the start of its next break or to its end, of the
programme's asset id, wherever that span is not empty; and an ad entry for each of its breaks,
from its start to its start plus its duration, whose asset id is the break's id. The schedule is therefore gapless
and non-overlapping, and every entry of a programme lies within it.

### 4.8 The cues

Each break, in the order of §4.7, yields one `cue` payload of live-events §4.3, unchanged:
`type` 4 (break: out and back, its duration known), `duration_ms` the break's duration, and
`form` 1 with the break's section verbatim, or `form` 0 for a break with no section. Its
`cue_id` is the section's `splice_event_id` when the section's `encrypted_packet` bit (the high
bit of byte 4) is 0, its `splice_command_type` (byte 13) is 0x05 (`splice_insert()`, SCTE 35-1
§9.7.3) and it is 18 bytes or longer: the big-endian integer of bytes 14 to 17 (SCTE 35-1 §9.6,
Table 5, and §9.7.3, Table 10). Otherwise the `cue_id` is the big-endian integer of the first 4
bytes of SHA-256 over `wai:channel-cue-id` (ASCII), 0x01 and the break's id. A cue says, as its
message does, that the break is out of the program and back.

### 4.9 What an import yields

- **The receipt**: `ChannelReceipt::seal` (SPEC §5, `wai.video.channel`) by the operator's key,
  with the import's `signer_id`, `channel_id` the channel, the schedule of §4.7, the window
  (when not given, from the schedule's first start to its last end), `joules_micro` 0 and no
  class (the import meters nothing; energy-measurement §2), the grant reference with no ceilings
  (`grant_hash` 32 zero bytes, `capability` `video.channel`, `joule_ceiling_micro` 2^64 − 1, no
  `funds_ceiling`), and `parent_receipt_hash` as given. A window that is empty, or that does not
  lie within the schedule (its start before the schedule's first start, or its end after its
  last end), is refused `import_window_outside`.
- **The lineup** (§6): the channel id, the channel name (the first of these that gives one: the
  XMLTV document, the first Media RSS document, the JSON form; when none does, the channel id), the receipt's
  `schedule_root`, the schedule's first start and last end, each programme's start, and the
  assets of §4.6 in the order of their asset ids' bytes, signed by the operator's key.
- **The cues** of §4.8, each with its break's id and start.

### 4.10 The order of refusal

An import is refused for the first step that fails, in this order: §4.1; then each feed, the
XMLTV document, the Media RSS documents in the order given, the JSON form and then the playlist,
each first by its size (§3.1), then as XML, JSON or lines, then by its counts (§3.1), then by
its own steps (§4.2 to §4.5), element by element in document order; then §4.6, §4.7 and §4.9.
An import that is refused yields nothing.

## 5. The channel receipt's JSON carriage

A `wai.video.channel` receipt keeps its signed bytes, and this text defines no form of its own for
it: an import writes a receipt, and a verifier reads one, in the JSON carriage of
[revenue-share](revenue-share.md) §4.1, the one JSON form of a channel receipt. A file that holds
a receipt holds one JSON text (RFC 8259) in UTF-8 of that carriage. A receipt **reads** when it
is such a text and reads as revenue-share §4.1 says; its entries' leaves, its schedule root, its
signing payload, its signature and its **identity** (its receipt hash) are revenue-share §4.2's;
its **rules** are rules 1 to 5 of revenue-share §4.3, and its signature that section's rule 6. A
receipt **verifies** when its signature verifies and its rules hold.

## 6. The lineup

### 6.1 Layout

| size | field | meaning |
|---|---|---|
| 4 | `magic` | `WLU1` (ASCII) |
| 1 | `version` | 1 in this revision |
| short | `channel_id` | the channel: a short text |
| short | `channel_name` | its name, as a guide shows it: a short text |
| 32 | `schedule_root` | the root of the schedule it describes (§5) |
| 8 | `schedule_start_ms` | u64: the schedule's first start |
| 8 | `schedule_end_ms` | u64: the schedule's last end |
| 4 | `p` | u32: the programmes' count, 1 to 10 000 |
| 8 × `p` | `programmes` | each programme's start (u64), in order |
| 4 | `a` | u32: the assets' count, 1 to 10 000 |
| … | `assets` | `a` assets, §6.2, in the order of their asset ids' bytes |
| 32 | `signer` | the operator's Ed25519 key |
| 64 | `sig` | §6.3 |

A **short** field is a u8 length and that many bytes; a **long** field a u16 length and that
many bytes. Programme *k* runs from its start to the start of programme *k* + 1, the last one to
`schedule_end_ms`.

### 6.2 An asset

| size | field | meaning |
|---|---|---|
| short | `asset_id` | the schedule's asset id: a short text |
| long | `title` | a long text of 1 byte or more |
| long | `description` | a long text |
| 1 | `c` | the categories' count, 0 to 16 |
| short × `c` | `categories` | each a short text |
| short | `rating_system` | the rating's system, or no bytes |
| short | `rating_value` | the rating, or no bytes for none |
| 1 | `k` | the artworks' count, 0 to 8 |
| … | `artwork` | `k` artworks, each 32 bytes (the SHA-256 of the image's bytes) and a long field (its URI, a long text of 1 byte or more) |
| 1 | `geo` | where it may be shown: 0 everywhere; 1 only in the countries listed; 2 everywhere but them |
| 1 | `g` | the countries' count |
| 2 × `g` | `countries` | each an ISO 3166-1 alpha-2 code, two of `A`–`Z`, in strictly increasing order |
| short | `rights_holder` | the party that holds its rights, as the operator names it: a short text |
| long | `content_uri` | the asset's media, or no bytes |
| 8 | `duration_ms` | u64: the asset's duration, 0 when unknown |

A rights holder is a name, not a key: a contract between the operator and the rights holder
(a share contract of the revenue it earns, for one) names its parties itself, and a lineup states
what the operator published.

### 6.3 Digest and signature

The **digest** of a lineup is SHA-256 over its bytes from `magic` through `signer`. The
**signature** is Ed25519 by `signer` over `wai:channel-lineup` (ASCII) and 0x01, followed by the
bytes the digest covers.

### 6.4 Reading and rules

A reader reads exactly the bytes it is given as one lineup, in the order of §6.1, and refuses it
`lineup_malformed` when the bytes end before a field the layout requires or go on after `sig`,
or when:

1. its magic is not `WLU1` or its version not 1;
2. `channel_id` or `channel_name` is not a short text;
3. `schedule_start_ms` is not below `schedule_end_ms`, either is not a multiple of 1000, or
   `schedule_end_ms` is above 2^53 − 1;
4. `p` is 0 or above 10 000; or the programmes' starts are not strictly increasing, the first is
   not `schedule_start_ms`, the last is not below `schedule_end_ms`, or one is not a multiple of
   1000;
5. `a` is 0 or above 10 000, or the asset ids are not in strictly increasing order of their
   bytes;
6. an asset breaks §6.2: an `asset_id`, a category or `rights_holder` that is not a short text;
   a `title` empty or not a text; a `description`, an artwork's URI or `content_uri` not a text;
   an empty artwork URI; `c` above 16 or `k` above 8; a `rating_system` that is not empty or a
   short text, or one that is not empty beside an empty `rating_value`; a `rating_value` that is
   not empty or a short text; `geo` above 2, `geo` 0 with `g` above 0, `geo` 2 with `g` 0, or
   countries not two of `A`–`Z` each in strictly increasing order; or `duration_ms` above
   2^53 − 1.

### 6.5 A lineup and its schedule

A lineup **describes** a receipt's schedule when:

1. its `channel_id` is the receipt's;
2. its `schedule_root` is the receipt's, and its `schedule_start_ms` and `schedule_end_ms` are
   the start of the receipt's first entry and the end of its last;
3. each programme's start is the start of an entry;
4. each programme holds at least one program entry, and every program entry it holds (every
   entry that starts in its span) has one asset id, the programme's asset;
5. the asset ids of its assets are exactly the programmes' assets.

A programme's ad and filler entries are its breaks; a guide lists the programme over them.

## 7. The guide

### 7.1 The programmes of a range

A guide is rendered from a receipt and a lineup that describes its schedule. The programmes of a
range from `from` to `to`, with `from` below `to`, are the lineup's programmes whose span meets it (a
programme that begins before `to` and ends after `from`), in order, each with its start, its
end, its asset's id and its asset's entry (§6.2).

### 7.2 The XMLTV guide

The XMLTV guide of a range is these bytes, in UTF-8, each line ending with a line feed:

```
<?xml version="1.0" encoding="UTF-8"?>
<tv source-data-url="urn:wai:guide:ROOT:LINEUP:FROM:TO">
  <channel id="CHANNEL">
    <display-name>NAME</display-name>
  </channel>
  <programme start="START" stop="STOP" channel="CHANNEL">
    <title>TITLE</title>
    <desc>DESCRIPTION</desc>
    <category>CATEGORY</category>
    <icon src="URI"/>
    <episode-num system="wai-asset-id">ASSET</episode-num>
    <rating system="SYSTEM">
      <value>VALUE</value>
    </rating>
  </programme>
</tv>
```

- `ROOT` is the receipt's `schedule_root` and `LINEUP` the lineup's digest, each 64 lowercase
  hexadecimal digits, and `FROM` and `TO` the range's ends in decimal, with no leading zero.
  `urn:wai:guide:` names a guide's sources; a guide names them in its `source-data-url`, an
  attribute the DTD gives `tv` for where the data came from.
- `CHANNEL` is the lineup's `channel_id` and `NAME` its `channel_name`.
- There is one `programme` element for each programme of the range, in order. `START` and `STOP`
  are its start and its end as XMLTV times (§3.4) in UTC with the zone written: `YYYYMMDDhhmmss
  +0000`.
- Of a programme's children, in the order the DTD gives them: one `title`; one `desc` only when
  the description is not empty; one `category` for each category, in order; one `icon` for each
  artwork, in order, its `src` the URI; one `episode-num`, whose text is the asset id; and one
  `rating` only when `rating_value` is not empty, with a `system` attribute only when
  `rating_system` is not empty.
- In text, `&`, `<` and `>` are written `&amp;`, `&lt;` and `&gt;`; in an attribute value,
  those and `"`, tab and line feed are written `&amp;`, `&lt;`, `&gt;`, `&quot;`, `&#9;` and
  `&#10;`. Every other character is written as itself.

The guide's `episode-num` system is `wai-asset-id`, the import's default `id_system` (§4.1), so
a guide imports back into the programmes it lists.

### 7.3 The now/next document

The now/next document of an instant `at` is the JCS serialisation (RFC 8785) of this object,
followed by one line feed:

| member | value |
|---|---|
| `kind` | `wai-now-next` |
| `channel`, `channel_name` | the lineup's `channel_id` and `channel_name` |
| `schedule_root` | the receipt's, 64 lowercase hexadecimal digits |
| `lineup` | the lineup's digest, 64 lowercase hexadecimal digits |
| `at_ms` | `at` |
| `now` | the programme whose span holds `at`, or `null` |
| `next` | when `now` is a programme, the one after it, or `null` for the last; when `at` is before the schedule's first start, the first programme; otherwise `null` |

A programme is an object with the members `start_ms`, `stop_ms` and `asset_id`; `title`,
`description` and `content_uri`; `categories`, an array of strings; `rating`, `null` or
`{"system", "value"}`; `artwork`, an array of `{"sha256", "uri"}`; `geo`, `null` for everywhere,
`{"allow": [...]}` or `{"deny": [...]}`; `rights_holder`; and `duration_ms`.

### 7.4 The Media Playlist's cue tags

A receipt also gives the ad markers of a Media Playlist of the channel (RFC 8216), so that a
packager that writes the channel's segments, MPEG-2 transport stream segments among them, marks
each break where the signed schedule places it, and anyone holding
the receipt and its cues recomputes the markers and compares (§8.4). Each break is marked in two
forms:

- the tags `EXT-X-CUE-OUT`, `EXT-X-CUE-OUT-CONT` and `EXT-X-CUE-IN`, in a form in common use, which
  no published standard defines and which this section fixes; and
- for a break whose cue carries its section, the `EXT-X-DATERANGE` tags of RFC 8216 §4.3.2.7.1,
  which carry the SCTE-35 section (ANSI/SCTE 35-1 2023r2 §9.6) in `SCTE35-OUT`.

A client ignores a tag it does not recognise (RFC 8216 §6.3.1), so a client that reads one form
plays a playlist that carries both.

**The breaks** are the receipt's `ad` entries of a duration above 0, in schedule order. A
break's **id** is its entry's asset id, its **start** the entry's start, its **duration** the
entry's duration, and its **end** its start plus its duration. Each break has its cue, the one an
import wrote for it (§4.8, the `cues.json` of §9); its **section** is the cue's message when the
cue is of form 1, and it has none when the cue is of form 0. The receipt signs each break's id,
start and duration, which are all the cue tags are written from. It does not sign the section a
date range carries: that is the cue's, which §8.4 holds to its break's id and duration and no
further.

**The window** is what a Media Playlist lists: its media sequence number (RFC 8216 §4.3.3.2); the
instant its first segment begins; its segments in order, each a URI and a duration, each beginning
where the one before it ends; and whether it is **ended** (`EXT-X-ENDLIST`, RFC 8216 §4.3.3.4). A
segment's URI is 1 to 2 048 bytes of ASCII from `!` to `~`, the first of which is not `#`, and its
duration 1 to 2^32 − 1 milliseconds; the media sequence number is at most 2^53 − 1, and the last
segment ends before 253 402 300 800 000, the first instant of the year 10000. A break in the window
starts at the latest where the window ends, so every date below has four digits of year. A window
that is not so is refused `playlist_malformed`.

A segment **starts in** a break when it starts at or after the break's start and before its end.
A break's **last segment before it** is the segment that ends at its start, and its **return** the
segment that starts at its end. A break **is in** the window when the window holds its last
segment before it, a segment that starts in it, or its return. The tags are refused:

- `cue_unaligned`, at the first segment of the window, in order, inside which a break's start or
  end lies (after the segment's start and before its end): a marker applies from a segment's
  start, and the splice the schedule places would fall inside the segment;
- then `cue_unwritable`, at the first break, among the breaks, that is in the window, has a
  section and has an id holding a double quote, a line feed or a carriage return, which no
  quoted-string can hold (RFC 8216 §4.2).

Otherwise the **playlist** of the window is these lines, each ended by a line feed:

```
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:TARGET
#EXT-X-MEDIA-SEQUENCE:SEQUENCE
#EXT-X-PROGRAM-DATE-TIME:FIRST
```

then, for each segment of the window in order, its cue lines (below), `#EXTINF:DURATION,` and its
URI; and then `#EXT-X-ENDLIST` when the window is ended.

- `TARGET` is the longest segment's duration in seconds, rounded up, so that each `EXTINF` rounded
  to the nearest integer is at most it (RFC 8216 §4.3.3.1); `SEQUENCE` the media sequence number in
  decimal, with no leading zero; and `FIRST` the first segment's start as a date.
- A **date** is an instant written `YYYY-MM-DDTHH:MM:SS.mmmZ`, in UTC, always three digits after the
  point (RFC 8216 §4.3.2.6); a **span** is a count of milliseconds written as seconds with three
  digits after the point (`90.000`). A segment's `DURATION` is its duration as a span: a decimal
  duration, which needs `EXT-X-VERSION` 3 (RFC 8216 §4.3.2.1, §7).

A segment's **cue lines** are, in this order:

1. The date range tags written at it, break by break in schedule order, of each break that has a
   section: its **out tag** at the first segment of the window that puts it in the window, and its
   **in tag** at its return, when the window holds it:

   ```
   #EXT-X-DATERANGE:ID="ID",START-DATE="START",PLANNED-DURATION=SPAN,SCTE35-OUT=0xSECTION
   #EXT-X-DATERANGE:ID="ID",START-DATE="START",DURATION=SPAN
   ```

   `ID` is the break's id, `@` and its start in decimal; `START` its start as a date; `SPAN` its
   duration as a span; and `SECTION` its section in uppercase hexadecimal, a hexadecimal-sequence
   (RFC 8216 §4.2).
2. `#EXT-X-CUE-IN`, when a break ends at the segment's start.
3. `#EXT-X-CUE-OUT:SPAN`, `SPAN` the break's duration, when a break starts at the segment's start;
   otherwise `#EXT-X-CUE-OUT-CONT:ElapsedTime=ELAPSED,Duration=SPAN`, when the segment starts in a
   break, `ELAPSED` the time from the break's start to the segment's as a span.

So:

- **A date range announces its splice and is kept while it maps a segment.** RFC 8216 §4.3.2.7.1
  places an out section in `SCTE35-OUT`, with a `START-DATE` that is the splice's and a
  `PLANNED-DURATION`, and has a tag announcing a splice added with the last segment before it; once
  the end is established, another tag of the same `ID` signals it. RFC 8216 §6.2.1 keeps a date range
  while any of its dates maps to a segment the playlist holds. Two tags of one `ID` agree on every
  attribute both carry (RFC 8216 §4.3.2.7), here `ID` and `START-DATE`, which that section requires
  of every date range. The in tag carries no `SCTE35-IN`: an import keeps the out section alone
  (§4.5 step 3).
- **An `ID` names one break.** A channel's schedule may repeat a break's id on another day; its start
  makes the `ID` unique in any playlist, as RFC 8216 §4.3.2.7.1 requires of an id that may recur.
- **The cue form is a function of each segment.** A segment's `EXT-X-CUE` lines depend on its own
  start and the schedule alone, so it carries the same ones in every window that holds it, and a
  player that joins inside a break reads, on the first segment it is given, how far into the break
  it is and how long the break is. The date range tags depend on the window, as RFC 8216 places them.
- **A break with no section has no date range**: RFC 8216 §4.3.2.7.1 maps a section, and there is
  none to map. Its cue form is written all the same.
- **Every playlist states its program date-time** (`FIRST`), which RFC 8216 §4.3.2.7 requires of a
  playlist that carries a date range, on the receipt's UTC timeline.
- **Nothing is spliced.** The segments are the channel's own, the program and the ad entries
  alike; the tags say where a downstream inserter replaces the ad entries' segments. No
  `EXT-X-DISCONTINUITY` is written, since the window's segments run on one timeline.

For brk-1 of the corpus's sample (§11), 90 seconds from 18:12:00 UTC, in ten-second segments, the
last segment before it, its first and its return carry:

```
#EXT-X-DATERANGE:ID="brk-1@1793297520000",START-DATE="2026-10-29T18:12:00.000Z",PLANNED-DURATION=90.000,SCTE35-OUT=0xFC30…
#EXTINF:10.000,
seg-71.ts
#EXT-X-CUE-OUT:90.000
#EXTINF:10.000,
seg-72.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=10.000,Duration=90.000
#EXTINF:10.000,
seg-73.ts
…
#EXT-X-DATERANGE:ID="brk-1@1793297520000",START-DATE="2026-10-29T18:12:00.000Z",DURATION=90.000
#EXT-X-CUE-IN
#EXTINF:10.000,
seg-81.ts
```

## 8. Verification

### 8.1 Inputs

A verifier is given a receipt in its JSON carriage (§5), a lineup's bytes, and optionally a
guide's bytes: an XMLTV guide when its first byte is `<`, a now/next document when it is `{`.

### 8.2 Steps

A verification gets one verdict, **verified** or the reason of the first step it fails:

1. **The receipt** reads (§5): `channel_receipt_malformed`.
2. Its signature verifies under `signer_pubkey` (§5): `bad_key`, `bad_signature`.
3. Its rules hold (§5): `channel_receipt_invalid`.
4. **The lineup** reads (§6.4): `lineup_malformed`.
5. Its signature verifies under `signer` (§6.3): `bad_key`, `bad_signature`.
6. Its `signer` is the receipt's `signer_pubkey`: one operator signs both. Otherwise
   `lineup_signer_mismatch`.
7. It describes the receipt's schedule (§6.5): `lineup_schedule_mismatch`.
8. **The guide**, when one is given, reads: `guide_malformed`. An XMLTV guide reads when it is in
   the subset of §3.3 with the root `tv`; its `source-data-url` is `urn:wai:guide:`, two runs of
   64 lowercase hexadecimal digits and two decimal integers with no leading zero, the first below
   the second, separated by `:`; and each of its `programme` children has `start` and `stop`
   attributes that are XMLTV times (§3.4) and an `episode-num` child of system `wai-asset-id`,
   the text of the first of which is its asset id. A now/next document reads when it is one JSON
   object whose members are those of §7.3, with `at_ms` an integer of at most 2^53 − 1,
   `schedule_root` and `lineup` strings, and `now` and `next` each `null` or an object whose `start_ms` and `stop_ms` are
   integers and whose `asset_id` is a string.
9. It names the receipt's `schedule_root` and the lineup's digest: `guide_source_mismatch`.
10. Each of its programmes is the lineup's: for an XMLTV guide, each programme has the start, the
    end and the asset of one of the programmes of the guide's range (§7.1); for a now/next
    document, `now` and `next` have those of the programmes §7.3 names for its `at_ms`, and are
    `null` where those are. Otherwise `guide_entry_unscheduled`, at the first programme, in the
    guide's order, that is not (its index among the guide's `programme` elements; for a now/next
    document, `now` 0 and `next` 1).
11. Its bytes are the guide of §7.2 for its range, or the document of §7.3 for its `at_ms`:
    `guide_mismatch`.

Step 10 catches a guide that places a programme where the schedule does not; step 11 every other
difference: a title, a rating or a picture the lineup does not state, a programme left out.

### 8.3 What a verification states

- its verdict, and the **part** it is about: `receipt` for steps 1 to 3, `lineup` for steps 4 to
  7, `guide` for steps 8 to 11, none when verified;
- the receipt's identity, when it reads; and the lineup's digest, when it reads;
- for a guide that reads: its format (`xmltv` or `now-next`), its range or its `at_ms`, and its
  programmes, each with its start, its end, its asset id and whether it **aired**: whether an
  entry the receipt's `aired` lists meets its span. A guide shows what the schedule placed; the
  receipt states which of it was on air in its window;
- for `guide_entry_unscheduled`, the index of step 10.

### 8.4 A playlist's cue tags

A verifier given a receipt in its JSON carriage (§5), a cues document and a Media Playlist's bytes
gets one verdict, **verified** or the reason of the first step it fails:

1. The receipt reads, as §8.2 step 1: `channel_receipt_malformed`.
2. Its signature verifies, as §8.2 step 2: `bad_key`, `bad_signature`.
3. Its rules hold, as §8.2 step 3: `channel_receipt_invalid`.
4. **The cues** read: one JSON text (RFC 8259) in UTF-8, nested at most 64 deep, whose objects each
   name a member once, an array of objects of exactly `break` (a string), `start_ms` (an integer of
   at most 2^53 − 1, written as §4.4 writes one) and `payload` (an even count of lowercase
   hexadecimal digits), as `channel.import` writes `cues.json` (§9). Otherwise `cues_malformed`.
5. They are the breaks' cues (§7.4): there are as many as there are breaks, and the cue at each index
   is its break's: its `break` the break's id and its `start_ms` the break's start, and its payload
   the one §4.8 yields for a break of that id and duration and of the section the payload carries,
   when it carries a message of form 1 that reads as §4.5 step 3 reads a section, or of none, when it
   is of form 0. Otherwise `cues_mismatch`, at the first index that is not, a cue missing or added at
   the first index past the shorter list. A break whose duration is above 2^32 − 1 milliseconds has no
   cue of §4.8, and its index fails.
6. **The playlist** reads: UTF-8 text whose lines, split at each line feed, the last ended by one,
   begin with `#EXTM3U`; with exactly one line `#EXT-X-MEDIA-SEQUENCE:` and a decimal integer with no
   leading zero, and a line `#EXT-X-PROGRAM-DATE-TIME:` and an HLS date-time (§3.4) before its first
   segment; whose segments are each a line `#EXTINF:`, a duration (§3.4) and `,`, followed by a line,
   the segment's URI, at least one of them. It is ended when one of its lines is `#EXT-X-ENDLIST`. Its
   **window** is that media sequence number, the first such date-time's instant as its first
   segment's start, its segments in order and whether it is ended, which **MUST** hold to §7.4's
   bounds. Otherwise `playlist_malformed`. No other line is read here: step 8 compares every line.
7. Its window's tags can be written (§7.4): `cue_unaligned`, at the segment's index; `cue_unwritable`,
   at the break's index among the breaks.
8. Its bytes are the playlist of §7.4 of its window: `playlist_mismatch`.

Step 8 catches a marker the schedule does not place, at a segment or in a date range: a break
another duration, another start or another section than its cue's, a tag left out, added or moved.

A verification states its verdict and the **part** it is about (`receipt` for steps 1 to 3, `cues`
for steps 4 and 5, `playlist` for steps 6 to 8, none when verified); the receipt's identity, when it
reads; for a playlist that reads, its window (its media sequence number, its first segment's start,
how many segments it holds and whether it is ended) and the breaks in it, each with its id, its
start, its duration and whether it has a date range; and the index of step 5 or step 7.

## 9. Operations and a worked example

Three operations of the WAI command line carry this text (the operation catalogue,
`wai/catalogue/ops.json`), each with the toolchain's agent rules: `--json` for the envelope,
`--dry-run` on an operation that writes, and `--yes` before a file is replaced.

- `channel.import` (`wai channel import`): feeds in; the receipt (`channel.json`, its JSON
  carriage), the lineup (`lineup.wlu`) and the cues (`cues.json`) out, into a directory. A
  refusal's code is the error's code, and its exit status 1.
- `channel.guide` (`wai channel guide`): a receipt and a lineup in; an XMLTV guide of a range
  (the whole schedule unless `--from` and `--to` say otherwise), or with `--now-next --at` a
  now/next document, out.
- `channel.verify` (`wai channel verify`): a receipt, a lineup and optionally a guide in; what
  §8.3 states out, exit status 0 when verified and 1 otherwise.

The corpus's sample channel (§11) is imported, guided and verified so:

```
wai channel import --channel sample-1 --xmltv sample.xmltv.xml \
    --mrss sample-a.mrss.xml --mrss-owner owner-a --mrss sample-b.mrss.xml --mrss-owner owner-b \
    --owner owner-c --playlist sample.m3u8 --artwork . --key operator.key --signer-id sample-operator \
    --window-start 1793296800000 --window-end 1793300400000 --out-dir out
wai channel guide --receipt out/channel.json --lineup out/lineup.wlu --out guide.xml
wai channel guide --receipt out/channel.json --lineup out/lineup.wlu --now-next --at 1793299545000 --out now-next.json
wai channel verify --receipt out/channel.json --lineup out/lineup.wlu --guide guide.xml
```

The test `tests/channel_lineup.rs` runs these commands on the corpus's files and requires the
corpus's receipt, lineup, cues and guides from them, byte for byte.

## 10. Reasons

The codes this extension adds, each with where it is raised:

| reason | raised at |
|---|---|
| `import_inputs_invalid` | §4.1 |
| `import_oversized` | §3.1, §4.6 step 3 |
| `import_xml_malformed` | §3.3 |
| `import_json_malformed` | §4.4 |
| `import_playlist_malformed` | §4.5 |
| `import_time_malformed` | §3.4, §4.2, §4.3, §4.4, §4.6 |
| `import_timezone_unknown` | §3.4 |
| `import_no_stop` | §4.2 step 2 |
| `import_empty` | §4.2, §4.6 |
| `import_asset_unknown` | §4.2 step 3, §4.3 step 1, §4.6 step 1 |
| `import_asset_duplicate` | §4.3 step 1, §4.4 |
| `import_field_invalid` | §3.2, §4.2 to §4.5 |
| `import_scte35_malformed` | §4.5 step 3 |
| `import_break_duration` | §4.5 step 4 |
| `import_duration_unknown` | §4.6 |
| `import_gap` | §4.6 |
| `import_overlap` | §4.6, §4.7 |
| `import_owner_unknown` | §4.6 step 2 |
| `import_artwork_missing` | §4.6 step 3 |
| `import_break_outside` | §4.7 |
| `import_window_outside` | §4.9 |
| `channel_receipt_malformed` | §8.2 step 1 |
| `channel_receipt_invalid` | §8.2 step 3 |
| `lineup_malformed` | §6.4, §8.2 step 4 |
| `lineup_signer_mismatch` | §8.2 step 6 |
| `lineup_schedule_mismatch` | §8.2 step 7 |
| `guide_malformed` | §8.2 step 8 |
| `guide_source_mismatch` | §8.2 step 9 |
| `guide_entry_unscheduled` | §8.2 step 10 |
| `guide_mismatch` | §8.2 step 11 |
| `cues_malformed` | §8.4 step 4 |
| `cues_mismatch` | §8.4 step 5 |
| `playlist_malformed` | §7.4, §8.4 step 6 |
| `cue_unaligned` | §7.4, §8.4 step 7 |
| `cue_unwritable` | §7.4, §8.4 step 7 |
| `playlist_mismatch` | §8.4 step 8 |

A verification also gives live-events' `bad_key` and `bad_signature` (§8.2 steps 2 and 5, §8.4
step 2).

## 11. Conformance

[`live-conformance/channels/`](../live-conformance/channels/) holds the corpus, written from fixed
seeds by `wai_channel_lineup_vectors gen`: `vectors.json`, which holds the operator's key and
each vector's inputs and outcome; the feeds of a sample channel (an XMLTV document with a second
channel's programmes beside it, two Media RSS documents of two content owners, a Media Playlist
with SCTE-35 date ranges, and the JSON form of the same channel); the artwork files; the receipt
and the guides the sample yields. Every document is a fixture written for its case, of
programmes, owners and pictures made for it. The vectors:

- **imports**: the sample from XMLTV, Media RSS and the playlist; the same channel from the JSON
  form, which yields the same receipt and lineup byte for byte; a channel made of one owner's
  Media RSS items alone, back to back; and a control of each refusal of §4, each the sample with
  one change (an edit the vector states, which a verifier applies to its copy);
- **verifications**: the sample's receipt and lineup alone, with its XMLTV guide, with a guide of
  part of the schedule, and with now/next documents before, inside and after the schedule; and a
  control of each step of §8.2 (a receipt that does not read, a flipped signature, a key of small
  order, a program-as-run the window does not give, a lineup that does not read, one another key
  signed, one for each rule of §6.5 that it alone breaks, a guide that does not read, one naming
  another schedule, one that places a programme the schedule does not, and one whose title the
  lineup does not state);
- **playlists**: windows of the sample's Media Playlist and the playlist each renders (§7.4): brk-1
  from before its last segment before it to after its return; a live window that begins inside
  brk-2, whose section is a `time_signal()`; brk-3, which begins half a second past a whole second,
  in six-second segments, the window ended; a window that begins at a return; one no break is in;
  and brk-1 from the JSON form with no section, which yields the same receipt and other cues, and
  no date range. And a control of each refusal of §7.4: segments cut off a break's start, a segment
  across a return, a break named with a double quote, and a segment of no duration;
- **playlist windows at their bounds**: brk-1 in segments of 6.006 seconds, whose `EXTINF` and
  `ElapsedTime` state milliseconds and whose `TARGETDURATION` is 7, 6.006 rounded up; a segment URI
  of 2 048 bytes; a segment of 2^32 − 1 milliseconds; a media sequence number of 2^53 − 1; and, of
  a channel scheduled at the end of the year 9999, a window that ends at 9999-12-31T23:59:59.999Z,
  where a break starts, its date range dated in the year 9999. And a control of each bound: brk-1 named with a line feed, and
  with a carriage return in a receipt the operator signed (`cue_unwritable`); that window with a
  segment of a millisecond after it, which ends at 10000-01-01T00:00:00Z; a window of no segment; a
  segment URI of no byte, of 2 049 bytes, holding a space, holding a letter outside ASCII and
  beginning with `#`; a segment of 2^32 milliseconds; and a media sequence number of 2^53
  (`playlist_malformed`);
- **playlist verifications**: the playlists of the first six windows, the one of 6.006-second
  segments and the one of the year 9999, each verified against its receipt and its cues (§8.4); and a
  control of each step: a receipt that does not read, a flipped signature, a
  program-as-run its window does not give, cues that do not read, a cue a millisecond longer than its
  break, one whose section names another `splice_event_id`, one a second late, one left out, another
  receipt's, a playlist with no media sequence number, one that ends at 10000-01-01T00:00:00Z, one
  whose segment URI begins with `#`, one whose segments move off a break's start,
  a break named with a double quote, and playlists that state another duration, leave out an
  `EXT-X-CUE-OUT-CONT`, return a segment early, carry another section, start a date range a second
  late, and leave out a date range's end.

A conforming implementation gives every import its outcome (the receipt, the lineup and the cues,
or the refusal), every verification its verdict and facts, every playlist window its playlist or
its refusal, and every playlist verification its verdict and facts, as the corpus states.
`wai_channel_lineup_vectors verify` and the test `tests/channel_lineup.rs` check that every file
regenerates byte for byte, that `wai-rs` gives every vector its outcome, that §10 lists exactly
the codes the reference adds, and that the corpus gives every reason of §10 and both of
live-events'. `live-conformance/channel_lineup_verify.py` is the second implementation, written
from this text: it shares no code with `wai-rs`; it verifies Ed25519 with WAI's strict rule
(`claim-conformance/_ed25519.py`), BLAKE3 with `claim-conformance/_blake3.py`, and reads a channel
receipt's carriage, its signed bytes, its identity and its rules as the revenue-share corpus's
second implementation reads them (§5); it reads XML, JSON and playlists itself, and renders and
reads a Media Playlist's cue tags itself (§7.4, §8.4); and it mutates copies of the corpus and requires
every copy to fail, and drops each of its own checks in turn and requires the corpus to catch the
weakened verifier.

## 12. Security and privacy considerations

- **What a lineup proves.** That the operator stated this description of each programme asset of
  one schedule, and where its guide's programmes begin. It does not prove that a rights holder
  agreed: a rights holder's name in a lineup is the operator's statement, and a party paid by it
  checks a contract it signed, not the lineup.
- **What a guide proves.** Nothing by itself: it is recomputed from a receipt and a lineup and
  compared, so a guide that verifies says exactly what the two signed records say, and a guide a
  third party altered is refused. A guide is a schedule published ahead of air; which of it was on
  air in a window is the receipt's program-as-run (§8.3), and a guide covering a span the receipt's
  window does not is checked against the schedule alone.
- **Artwork.** A lineup binds each picture by its SHA-256, so a player that fetches the URI and
  checks the hash shows the picture the operator stated, wherever it was fetched from.
- **Bounded reading.** An import reads bounded documents (§3.1), XML to a bounded depth with no
  document type declaration that defines anything (§3.3), so no entity is ever expanded, and
  integers it reads are checked against their bounds before any arithmetic.
- **What the cue tags prove.** A playlist that verifies marks each break where the receipt's
  signed schedule places it, with its signed duration, and every segment of it; one that moves a
  break, shortens it or leaves a segment of it unmarked is refused. The section a date range carries
  is the import's (§4.8): the receipt does not sign it, and a verifier holds it only to its break's
  id and duration, and through the cue id to a `splice_insert()`'s `splice_event_id`. A party that
  splices on a section's own fields holds the playlist it was imported from, or the signed `cue`
  events of a room that carried it (live-events §3.2, §4.3), which sign the section itself.
- **Availability.** Where an asset may be shown is the operator's statement of the catalogue's
  restriction; a player enforces it, or a delivery that a `wai.video.manifest` describes does.
  A lineup holds no viewer's data.
