diff --git a/.github/workflows/energy-hardware.yml b/.github/workflows/energy-hardware.yml index 00118e5f3..801826740 100644 --- a/.github/workflows/energy-hardware.yml +++ b/.github/workflows/energy-hardware.yml @@ -22,6 +22,10 @@ name: energy meters on real hardware # fetched with plain git, and each workspace keeps its target directory in the # runner's home between runs. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: pull_request: paths: diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 6c2973727..80346dcd8 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -195,7 +195,9 @@ jobs: # allocator) checks that a header past the caps is refused before anything sized from it # is allocated; tests/int_payload_cost.rs (its own binary, a live-bytes allocator) checks # that int_payload::declared_cost bounds what each decode holds, and, in the release - # step only, decodes a 2^30-byte clip a 3 KB payload codes. tests/staged_resources.rs (its + # step only, decodes a 2^30-byte clip a 3 KB payload codes; tests/int_video_enc_memory.rs + # (its own binary, the same allocator) checks that int_video_enc::working_bytes bounds + # what the encoder holds. tests/staged_resources.rs (its # own binary, a counting allocator) checks that the staged sink's work and memory per # layer grow linearly in the layer's units, that a layer past the chain budget is # refused before any stream is decoded, that what no unit holds is dropped, and that a @@ -209,10 +211,10 @@ jobs: # wai.generate.spectral vector, which `generate` enables. - name: Fast integer kernels ≡ reference kernels (overflow checks on) working-directory: wai/wai-rs - run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage --test staged_resources + run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test int_video_enc_memory --test cli_stage --test staged_resources - name: Fast integer kernels ≡ reference kernels (release, overflow checks off) working-directory: wai/wai-rs - run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage --test staged_resources + run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test int_video_enc_memory --test cli_stage --test staged_resources # The integer decode path builds under each of its features alone, so a sink can take # one medium without the others, and for wasm32 (a browser sink); `int_payload` arms a # medium only under its feature, and none needs `neural`. diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml new file mode 100644 index 000000000..6fd8ec5b4 --- /dev/null +++ b/.github/workflows/wai-pack.yml @@ -0,0 +1,192 @@ +name: wai-pack + +# The reference packager of WAI's packaging extension (wai/extensions/packaging.md) and +# its conformance corpus (wai/pack-conformance). Three jobs: +# +# - legs: on each architecture of the fleet, clippy with float arithmetic denied (no +# float decides anything in a package), the tests in release with a floor on how many +# each target lists, and the corpus recomputed by the crate. +# - independent: the corpus recomputed by its second implementation, written in Python +# from the extension text and sharing no code with wai-pack. +# - licences: every crate in wai-pack's dependency graph is under a licence deny.toml +# allows; a source-available licence fails it. +# +# Push filters match the push's delta, so every path a build or a verifier reads is +# listed: the crate, the wai crate it builds on, the corpus, the BLAKE3 and integer +# payload verifiers and the registry export pack_verify.py reads, and this file. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + paths: + - 'wai/wai-pack/**' + - 'wai/wai-rs/**' + - 'wai/pack-conformance/**' + - 'wai/claim-conformance/_blake3.py' + - 'wai/int-payload-conformance/int_payload_verify.py' + - 'wai/registry/capabilities.json' + - '.github/workflows/wai-pack.yml' + push: + branches: [main] + paths: + - 'wai/wai-pack/**' + - 'wai/wai-rs/**' + - 'wai/pack-conformance/**' + - 'wai/claim-conformance/_blake3.py' + - 'wai/int-payload-conformance/int_payload_verify.py' + - 'wai/registry/capabilities.json' + - '.github/workflows/wai-pack.yml' + +jobs: + legs: + name: wai-pack · ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Self-hosted fleet labels: x86_64 by linux, arm64 by linux-arm64 and macos. + # A Windows leg waits for its runner (see wai-byte-equality.yml). + os: [linux, linux-arm64, macos] + steps: + - uses: actions/checkout@v5 + + # The docker runners ship no Rust, so rustup is bootstrapped; the macOS host has + # it, but launchd's PATH omits ~/.cargo/bin. + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup component add --toolchain 1.98.0 clippy + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + # clippy::float_arithmetic is denied for the library and the CLI (Cargo.toml and + # the crate attributes); -D warnings makes every other lint fail the step too. + - name: clippy, float arithmetic denied + working-directory: wai/wai-pack + run: cargo clippy --locked --all-targets -- -D warnings + + - name: Tests (release) + working-directory: wai/wai-pack + run: cargo test --release --locked + + # A target that lost its tests (a module dropped, a cfg that compiles them out) + # would still report ok. Each target must list at least as many tests as it had + # when the floor was set; raise a floor when tests are added. + - name: Test-count floor per target + shell: bash + working-directory: wai/wai-pack + run: | + set -euo pipefail + fail=0 + while read -r target floor; do + n=$(cargo test --release --locked $target -- --list 2>/dev/null | grep -c ': test$' || true) + echo "$target: $n tests (floor $floor)" + [ "$n" -ge "$floor" ] || { echo "::error::$target lists $n tests, under its floor of $floor"; fail=1; } + done <<'EOF' + --lib 17 + --test=conformance 1 + --test=scenecut 3 + --test=source_alloc 1 + EOF + exit $fail + + - name: The corpus, recomputed by wai-pack + working-directory: wai/wai-pack + run: cargo run --release --locked --bin wai-pack -- conformance check ../pack-conformance + + independent: + name: wai-pack · independent verifier (Python) + runs-on: linux + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + # Standard library only (tomllib, hashlib); BLAKE3 comes from the shared pure-Python + # module beside the claim corpus. + - name: pack_verify.py + working-directory: wai + shell: bash + run: python3 pack-conformance/pack_verify.py pack-conformance + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # The source readers differentially: seeded Y4M and WAV mutations, every verdict of + # wai-pack (debug and release, both with overflow checks) equal to pack_verify.py's, + # and no reader panicking. + - name: source_diff.py, wai-pack against pack_verify.py + shell: bash + working-directory: wai/wai-pack + run: | + set -euo pipefail + cargo build --locked --bin wai-pack + cargo build --release --locked --bin wai-pack + for profile in debug release; do + python3 ../pack-conformance/source_diff.py "target/$profile/wai-pack" 1 3000 + done + # The policy reader differentially: byte-level cases and seeded random policies, + # every verdict and digest of wai-pack equal to pack_verify.py's. + - name: policy_diff.py, wai-pack against pack_verify.py + shell: bash + working-directory: wai/wai-pack + run: python3 ../pack-conformance/policy_diff.py target/release/wai-pack 1 3000 + # wai.pack.scenecut/1 differentially: the streaming segmenter against + # pack_verify.py's on-demand form on seeded random clips. + - name: segment_diff.py, wai-pack against pack_verify.py + shell: bash + working-directory: wai/wai-pack + run: python3 ../pack-conformance/segment_diff.py target/release/wai-pack 1 2000 + + licences: + name: wai-pack · dependency licences + runs-on: linux + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Install cargo-deny + shell: bash + run: cargo install --locked cargo-deny --version 0.19.8 + - name: cargo deny check licenses + working-directory: wai/wai-pack + run: cargo deny --locked check licenses + # deny.toml's MPL-2.0 exception covers cbindgen as wai's build-time tool: it must + # reach wai-pack through no normal (linked) edge. The second query sees the build + # edge, so an empty answer from the first is an answer. + - name: cbindgen is a build dependency only + shell: bash + working-directory: wai/wai-pack + run: | + set -euo pipefail + err=$(mktemp) + if ! linked=$(cargo tree --locked -e normal --target all -i cbindgen 2>"$err"); then + grep -q "did not match any packages" "$err" || { cat "$err"; exit 1; } + linked="" + fi + if [ -n "$linked" ]; then + echo "::error::cbindgen is a linked dependency of wai-pack; its MPL-2.0 exception covers a build tool only" + echo "$linked" + exit 1 + fi + cargo tree --locked -e normal,build --target all -i cbindgen | grep -q '^cbindgen v' || { echo "::error::cbindgen not found as wai's build dependency"; exit 1; } + echo "cbindgen is a build dependency only" diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 9c4a4881d..d28ee9e05 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,50 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Packaging: a declared policy, its digest, and pieces cut at scene changes + +- **The extension** [packaging](extensions/packaging.md), first sections: a package is + made under one TOML policy, `wai.pack.policy/1` (§3), read in a fixed order with + every refusal named: value kinds a canonical JSON cannot carry exactly (floats, + dates, negatives, integers past `2^53 − 1`), then the schema, every default written + in, then the constraints across members (ladders and their refinement chains, + classes, rules, deadlines against layer lags). The effective policy's digest is + BLAKE3 over its RFC 8785 form, so two spellings of one policy have one digest. +- **Pieces** (§4): `wai.pack.scenecut/1` cuts a video component at scene changes by + integer histogram distance, with a bounded lookahead whose return and revisit + clauses keep a short flash from cutting; a decision reads no frame past the + lookahead, so live and on-demand packing cut alike. Pieces are bounded by `min` and + `max`, aligned across components to the lead, and classified static, synthetic, + high-motion or natural. Y4M and WAV sources convert by stated integer rules. +- **`wai-pack`**, the reference packager, as far as a plan (`wai-pack policy`, + `wai-pack plan`), with float arithmetic denied for the whole crate, and the corpus + [`pack-conformance/`](pack-conformance/) with a second implementation in Python. +- **What a packager may emit** (§3.6): only capabilities SPEC §5 registers as payloads + with a pinned format, in a role each can play, and under `royalty_free_only` only + royalty-free ones; every refusal names the capability (`capability-unknown`, + `capability-not-payload`, `capability-candidate`, `capability-role`, + `license-unclassified`, `license-refused`). +- **One integer video encoder** (§5.5): `int_video_enc` codes RGB8 frames as the + `WIV1` keyframe-0 payload SPEC §5 pins to `wai.video.int_motion`, with integer block + motion search (whole- or half-pel) over the decoder's own prediction, residuals + quantized in a closed loop, and one residual table for the clip; it states the SSE + of its reconstruction. `int_payload::encode_wiv1_zero_motion` is its zero-motion + case and writes the bytes it wrote before. The corpus holds its payloads for every + piece of every fixture, decoded independently to the same frames. +- **`wai::jcs`**: the canonical JSON form moves out of `claim_receipt` into a module + of its own, compiled under every feature set, unchanged in behaviour. +- **Choosing the step** (§5.6), specified and built ahead of the stage that writes + objects: the largest of `q_candidates` (read as a set) whose SSE per sample, in + thousandths, is at most `max_sse_px_milli`, else the smallest + (`wai_pack::encode::choose_q`), with goldens recomputed independently. +- **Reading what a packager is given**: a policy is read from its bytes (UTF-8, no + byte-order mark, nesting at most 32 deep, `policy-toml` otherwise), ids are unique + across every kind, a cut window holds at least 3 scores, a WAV block alignment is + compared without wrapping, and no declared size sizes a buffer; the segmenter's state + is bounded by its lookahead and window. The encoder codes in two passes and states + what it holds (`int_video_enc::working_bytes`), and `rans::RansReverseEncoder` codes + values last-first into the same bytes. + ### Staged delivery in the browser, a base writer, a trace and a bench - **A base is bounded before it is decoded, and its chain from the start** diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md new file mode 100644 index 000000000..ec09f87fe --- /dev/null +++ b/wai/extensions/packaging.md @@ -0,0 +1,349 @@ +# WAI Extension: Packaging + +> Status: Draft. A packager turns sources into WAI objects under a declared policy; +> this extension fixes what a verifier recomputes. This revision defines the policy, +> its digest and what it may ask a packager to emit (§3), how a source is cut into +> pieces (§4), and the integer video payloads a packager writes (§5.5). Reference +> impl: `wai-pack` — `policy`, `capcheck`, `source`, `scenecut`, `classify`; the +> encoder `wai-rs` `int_video_enc`; the corpus +> [`pack-conformance/`](../pack-conformance/). Keywords **MUST**, **MUST NOT**, +> **SHOULD**, **MAY** are RFC 2119/8174. + +## 1. Scope + +A **packager** turns one or more sources into WAI objects, stage indexes, catalogs +and a package receipt, under a declared **policy**. This extension fixes what a +package contains and what a verifier recomputes. It fixes an encoder's choices only +where it names them. + +## 2. Terms + +- **Component**: one media stream of a package (for example video, or audio). +- **Piece**: a span of a component that decodes with no reference outside itself. +- **Stage**: one object of a piece in staged order: the base is layer 0, refinement + *k* is layer *k* ([staged-delivery](staged-delivery.md)). +- **Rung**: one entry of the policy's ladder: a capability a component's pieces are + coded in, with its role (base, refinement or floor) and parameters. +- **Class**: a delivery profile the policy names. +- **Moment**: a declared span of presentation time with tags. +- **Floor**: a rendition in a standard codec, for sinks that lack the primary. +- **Timebase** of a component: `num / den` units per second. For video one unit is + one frame; for audio, one sample. + +## 3. Policy + +**3.1** A package is made under exactly one policy, a TOML 1.0 document of schema +`wai.pack.policy/1`, in UTF-8 with no byte-order mark. + +**3.2** The **effective policy** is the policy with every default written in (§3.3) +and every override the packager applies (such as a command line forcing +`royalty_free_only`). Its JSON form is §3.3's members, with an optional member that +has no default omitted when absent, and arrays of tables in document order. Its +**policy digest** is + + BLAKE3("wai:pack-policy\x01" ‖ JCS(effective policy as JSON)) + +where JCS is RFC 8785. Two spellings of one policy (member order, inline or standard +tables, dotted keys, string or integer notation, comments, an omitted default or the +default written out) therefore have one digest; the order of an array of tables is +part of the policy. + +A packager **MUST** refuse with `policy-toml`, before anything else, a policy whose +bytes are not UTF-8, that begins with a byte-order mark, that is not TOML 1.0 (an +integer outside TOML's 64-bit range included), or that nests arrays and tables more +than 32 deep: the document's own table is at depth 1, and an array or a table +directly inside one at depth *k* is at depth *k* + 1. Then, before it reads any key, it +**MUST** refuse one holding anywhere a float (`policy-float`), a date or time +(`policy-datetime`), a negative integer (`policy-negative`), or an integer above +`2^53 − 1` (`policy-integer-range`), visiting members in byte order of their keys and +arrays in order. + +**3.3** *Schema.* Within each table, a key this section does not define is refused +first (`policy-unknown-key`, the first in byte order), then each member in the order +listed: a required member that is absent (`policy-missing-key`), a member of another +type (`policy-type`), a value outside its range or vocabulary (`policy-value`). An +absent table reads as empty. + +| Member | Type | Default | Range | +|---|---|---|---| +| `schema` | string | required | `wai.pack.policy/1` (else `policy-schema`) | +| `asset_id` | string | required | 1 to 255 bytes | +| `royalty_free_only` | boolean | `false` | | +| `lead` | string | required | 1 to 255 bytes | +| `component` | array of tables | required | | +| `component.id` | string | required | 1 to 255 bytes | +| `component.media` | string | required | `video`, `image`, `audio`, `world`, `feed`, `avatar`, `film` | +| `component.source` | string | absent | | +| `component.timebase` | table | required | members `num`, `den`, each 1 to `2^32 − 1`, required | +| `segmentation.target_ms` | integer | 2000 | | +| `segmentation.min_ms` | integer | 1000 | | +| `segmentation.max_ms` | integer | 4000 | | +| `segmentation.scene_cut` | string | `wai.pack.scenecut/1` | `wai.pack.scenecut/1` | +| `segmentation.cut_abs_permille` | integer | 300 | 1 to 1000 | +| `segmentation.cut_rel_permille` | integer | 3000 | 0 to 1000000 | +| `segmentation.cut_window` | integer | 8 | 3 to 64 (a window of fewer than 3 scores never applies `cut_rel_permille`, §4.3) | +| `segmentation.lookahead_frames` | integer | 6 | 0 to 64 | +| `segmentation.closed` | boolean | `true` | | +| `ladder` | array of tables | empty | | +| `ladder.id` | string | required | 1 to 255 bytes | +| `ladder.component` | string | required | 1 to 255 bytes | +| `ladder.capability` | string | required | | +| `ladder.role` | string | required | `base`, `refinement`, `floor` | +| `ladder.refines` | string | absent | | +| `ladder.params` | table | empty | by capability, below | +| `floor.supplied_listing` | string | absent | 64 lowercase hexadecimal digits | +| `floor.extra_keyframes` | boolean | `false` | | +| `live.layer_lag_ms` | array of integers | one 0 per layer | §3.4 | +| `class` | array of tables | empty | | +| `class.id` | string | required | 1 to 255 bytes | +| `class.max_layer` | integer | 0 | 0 to 255 | +| `class.max_kbps` | integer | absent | 1 to `2^32 − 1` | +| `class.floor_only` | boolean | `false` | | +| `moment` | array of tables | empty | | +| `moment.id` | string | required | 1 to 255 bytes | +| `moment.from_ms`, `moment.to_ms` | integer | required | `to_ms` above `from_ms` | +| `moment.tags` | array of strings | empty | | +| `rule` | array of tables | empty | | +| `rule.id` | string | required | 1 to 255 bytes | +| `rule.when.piece_kind` | array of strings | absent | `natural`, `static`, `synthetic`, `motion_high` | +| `rule.when.moment_tag` | array of strings | absent | | +| `rule.then.ladder` | array of strings | absent | | +| `rule.then.max_layer` | integer | absent | 0 to 255 | +| `rule.then.deadline_ms` | integer | absent | 1 to `2^32 − 1` | +| `defaults.deadline_ms` | integer | 1500 | 1 to `2^32 − 1` | +| `classify.static_permille` | integer | 5 | 0 to 1000 | +| `classify.static_mad_milli` | integer | 300 | | +| `classify.synthetic_bins` | integer | 12 | 0 to 64 | +| `classify.motion_high_milli` | integer | 9000 | | + +A rung's `params` are those of its capability; every other capability takes none. + +| Capability | Parameter | Default | Range | +|---|---|---|---| +| `wai.video.int_motion` | `block` | 16 | 4, 8, 16, 32 | +| | `search` | 7 | 0 to 32 | +| | `mv_precision` | 2 | 1, 2 | +| | `q_candidates` | `[8]` | a non-empty array, each 1 to 255 | +| | `max_sse_px_milli` | 0 | | +| `wai.video.int_refine` | `q` | required | 1 to 255 | +| `wai.video.av1`, `wai.video.av1.lossless` | `source` | `supplied` | `supplied`, `encode` | +| `wai.audio.int_refine` | `to_rate`, `q` | required | 1 to 384000; 1 to 65535 | +| `wai.audio.flac` | `source` | `encode` | `encode`, `supplied` | + +`wai.audio.int_layered` is reserved for the layered integer audio base: it is not +registered yet, and its parameters are defined when it is. + +**3.4** *Constraints.* A policy that passes §3.3 is checked in this order, and the +first that fails is the refusal: + +1. 1 to 8 components (`policy-too-many-components`); at most 64 rungs + (`policy-too-many-rungs`), 32 classes (`policy-too-many-classes`), 32 rules + (`policy-too-many-rules`) and 32 moments (`policy-too-many-moments`). +2. Ids unique among components, rungs, classes, moments and rules taken together, so + a reference never names two things (`policy-duplicate-id`). +3. `lead` names a component (`policy-lead`). +4. `segmentation.closed` is true: every piece is closed, §4.1 (`policy-open-gop`). +5. `1 ≤ min_ms ≤ target_ms ≤ max_ms` (`policy-piece-bounds`). +6. Every rung names a component (`policy-ladder-component`). +7. A rung carries `refines` exactly when its role is `refinement` + (`policy-refines-role`); `refines` names a rung (`policy-dangling-refines`); no + chain of `refines` returns to a rung it passed (`policy-refines-cycle`); every + chain stays in its component and ends at a base (`policy-refines-base`); no rung is + refined twice (`policy-refines-branch`). +8. Every component has a base rung (`policy-no-base`). +9. A rung's **layer** is 0 for a base or a floor, and one more than the rung it + refines for a refinement; the policy has one more layer than its highest rung + layer. No class's and no rule's `max_layer` exceeds the highest + (`policy-class-layer`). +10. A rule's `then.ladder` names known rungs, all of one component, at least one of + them a base (`policy-rule-ladder`). +11. `live.layer_lag_ms` has one entry per layer, the first 0, the entries + non-decreasing (`policy-layer-lag`). +12. The deadline values (`defaults.deadline_ms` and every rule's `then.deadline_ms`) + number at most 4 (`policy-too-many-deadlines`), and for every refinement layer + *k* and every deadline value *d*, `d − layer_lag_ms[k] ≥ 1` + (`policy-deadline-unreachable`). + +Rules fold in document order: every matching rule applies, and a later `then` member +overrides an earlier one. + +**3.5** A package receipt carries the policy digest. A verifier holding the effective +policy recomputes it; it cannot check that the packager followed the policy, except +by packing again. + +**3.6** *What a packager may emit.* A packager **MUST** refuse a policy with a rung it +may not emit, reading SPEC §5's registry, never the policy. For each rung in ladder +order the first rule that holds is its refusal, and a policy's refusal is its first +rung's: + +1. SPEC §5 does not register the capability: `capability-unknown:`. +2. SPEC §5 registers it as a record or a derivation operation, which names no payload + format: `capability-not-payload:`. +3. It is a candidate, whose payload format no revision has pinned (SPEC §5, + *Candidate capabilities*): `capability-candidate:`. +4. A base or a floor whose capability is not standalone media, or a refinement whose + capability is not refinement-only: `capability-role:`. +5. Under `royalty_free_only`, a capability the registry gives no licence class: + `license-unclassified:`; one of any class other than royalty-free: + `license-refused::