wai/ietf: draft-charlot-moq-wai-packaging-00 (review only, not submitted) + wai_priors #383

Merged
dcharlot merged 6 commits from wai/ietf-draft into main 2026-10-21 14:13:11 -04:00
13 changed files with 5684 additions and 10 deletions

View file

@ -390,6 +390,7 @@ and a role are fixed once registered.
| `wai_machine_generated` | catalog | defined | timed-text §7.4 |
| `wai_model` | catalog | defined | timed-text §7.2 |
| `wai_origin` | catalog | defined | timed-text §7.2 |
| `wai_priors` | catalog | defined | moq-streaming-format §4.2 |
| `wai_receipt_alias` | catalog | defined | moq-streaming-format §4 |
| `wai_receipts` | catalog | defined | moq-streaming-format §4 |
| `wai_receipts_track` | catalog | defined | moq-streaming-format §4 |

View file

@ -234,7 +234,7 @@ Required fields:
| `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. |
| `signer_id` | string | Bound identity string (DID, URN, or display). In §7.1's origin-bound form it names the web origin whose key log lists `signer_pubkey`; an Ed25519 `did:key` names one key; any other form is a label, and a verifier reads nothing from it. |
| `sig` | 64 B hex | Ed25519 signature over §2.3 payload. |
| `parent_receipt_hash` | 32 B hex or `null` | BLAKE3 of the previous group receipt's canonical bytes under this publisher key and `track_alias` (its link hash, §3 step 3); `null` for the first group of that track. |
| `parent_receipt_hash` | 32 B hex or `null` | The link hash (§3 step 3: BLAKE3, domain-separated, over the canonical bytes) of the previous group receipt under this publisher key and `track_alias`; `null` for the first group of that track. |
**The total's class (labelled form).** A group whose total carries a class of
its own adds these members and is signed over the labelled payload (§2.3):
@ -1791,7 +1791,8 @@ match the algorithm style of WAI SPEC.md §4 (numbered, terminating).
the head of its chain whether or not any of its objects arrives (§4.4),
so a dropped object does not break the link of its track's next group.
`canonical_bytes(G)` is G's §2.3 signing payload, in the form G was
signed in, followed by its 64-byte `sig`. The resulting hash is G's
signed in, followed by its 64-byte `sig`, and
`BLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(G))` is G's
**link hash**: the identity a `claim` names G by (§2.4.3), and a
`WAI_RECEIPT` property's `group_receipt`
([moq-streaming-format](moq-streaming-format.md) §5). For a group in

View file

@ -81,6 +81,7 @@ fields, the capabilities a sink needs to dispatch what the track carries:
| `wai_receipt_alias` | integer | The `track_alias` the publisher signs in this track's receipts ([jwp-receipts](jwp-receipts.md) §2.4.1), from 0 to 2^53 − 1. |
| `wai_receipts_track` | string | The name, in this track's namespace, of the track that carries this track's receipts (§4.1). |
| `wai_ai_usage` | object | The usage statement of the track's objects ([ai-usage](ai-usage.md) §3), or of the room record in effect for a room's track. Absent means the catalog states none. |
| `wai_priors` | array of object | OPTIONAL. The parameter sets the track's objects pin (SPEC §3.1), by digest, each with the tier it is offered at (§4.2). Absent means the catalog states none. |
- `wai_receipt_alias` names the track's receipt chain, which a verifier keeps per
publisher key and `track_alias` (jwp-receipts §3 step 3). MOQT's own Track Alias is
@ -354,6 +355,71 @@ prototype header fields of staged-delivery §13 are not read.
"isLive": true }
```
### 4.2 Pinned parameter sets
A capability that decodes against a parameter set names that set in each envelope, by a
pin: the SHA-256 of the whole set, in a declared form, with the tier the sender claims
(SPEC §3.1). A track **MAY** also state, in its catalog entry, the pins its objects carry.
A sink can then tell before it subscribes whether it holds the parameter sets the track
needs, and which tier each is offered at:
```json
"wai_priors": [
{ "capability": "wai.neural.int_hyper",
"sha256": "<64 lowercase hexadecimal digits>",
"form": "wai.prior.set", "tier": "decode-equivalence" }
]
```
| member | presence | meaning |
|---|---|---|
| `capability` | REQUIRED, string | A capability the track's `wai_capabilities` lists. |
| `sha256` | REQUIRED, string | A pin's `sha256`: 64 lowercase hexadecimal digits. |
| `form` | OPTIONAL, string | The pin's `form`. Absent, or `null`, where the pin declares none. |
| `tier` | OPTIONAL, string | The tier the publisher offers the set at, from SPEC §3.1's vocabulary. Advisory (below). |
An entry **lists** a pin for a capability when its `capability` is that capability, its
`sha256` is the pin's, and its `form` is the pin's: both absent, or the same string. A
member of an entry that this revision does not define is ignored.
- **Reading.** `wai_priors`, where present and not `null`, is an array of objects, each
with the members above of their types and no two with the same `capability`, `sha256`
and `form`, and each `capability` one the track's `wai_capabilities` lists. A sink that
reads anything else refuses the track (`catalog_priors_malformed`): it does not
subscribe to it. A `tier` this revision does not register is read as `none` (SPEC §3.1).
- **Exhaustive.** Where a track carries `wai_priors`, the publisher **MUST** list in it
every pin its objects carry. An object **breaks the catalog** when:
1. it carries a pin that no entry lists, either for its `capability` or for its
`fallback`; or
2. an entry names its `capability`, or its `fallback` (not `null`), and the object
carries no pin, or carries a pin that no entry lists for that capability.
A publisher whose objects' pin can decode through the fallback (SPEC §4 step 5)
**SHOULD** list the set for the fallback as well, so that a sink that holds only the
fallback can make the same decision before it subscribes.
- **Mismatch.** A sink checks each object of such a track against the track's
`wai_priors` before it dispatches the object (SPEC §4). An object that breaks the
catalog is refused (`catalog_prior_mismatch`). The track is refused with it: the sink
dispatches no further object of the track, and **MAY** unsubscribe from it. The
catalog never stands in for the envelope. A sink verifies an object's pin under SPEC
§3.1 whether or not the catalog lists it. An entry never satisfies a pin, never
supplies a pin the envelope lacks, and never changes which set a decode reads.
- **Tier.** A listed `tier` is advisory. A publisher **MUST NOT** list a tier above the
one the capability's registry class gives (SPEC §3.1). A sink **MUST NOT** claim a tier
on the strength of a listed tier: the tier it claims for an object is the one its own
decode earns, under SPEC §3.1 and §7 and [determinism-tiers](determinism-tiers.md) §3,
whatever the catalog says. A listed tier that differs from the tier an object's pin
states is not a mismatch.
- `wai_determinism` (§4) still states the weakest tier any object on the track claims.
Reference implementation: `staged::moq::CatalogPriors` reads the member
(`CatalogPriors::read`) and checks an object against it (`CatalogPriors::check`,
`CatalogPriors::check_manifest`). `staged-conformance/moq/priors.json` holds a case for
each reason and for each way an object can break the catalog. `wai_staged_vectors`
checks the cases, and so does `staged_verify.py`, which shares no code with `wai-rs`.
The browser sink of `joule-moq-web` dispatches no capability that decodes against a
parameter set, and does not read the member.
## 5. The per-object property
MOQT carries metadata a relay can see as **Properties** of a track or of an object
@ -374,6 +440,21 @@ commitment:
| `acquisition_evidence` | bytes | For `OnChipCounter` and `CalibratedInstrument`, the evidence that follows the tag byte in `energy-measurement.md` §2.7 (declared uncertainty, then filtering state or calibration reference). Empty for every other class. |
| `group_receipt` | 32 B | The link hash of the group receipt covering this object (`jwp-receipts` §3 step 3). |
On the wire the property is one Key-Value-Pair (MOQT §8.3) of an odd type. Its value is
the five fields in the order of the table, with nothing after them: `content_hash` (32
bytes); `joules_micro` (a MOQT variable-length integer, MOQT §8.1); `acquisition` (a
variable-length integer giving its length, then its UTF-8 bytes); `acquisition_evidence`
(a variable-length integer giving its length, then the bytes); and `group_receipt` (32
bytes). A sink refuses a value that ends early, has bytes left after `group_receipt`, or
whose `acquisition` is not UTF-8 (`property_invalid`). The publisher sets the property
inside the object's Immutable Properties (MOQT §10.7), which no relay may change or
remove. No property type is registered for it. The Internet-Draft source in `wai/ietf/`
requests one from the Specification Required range of MOQT's property registry (MOQT
§16.8). Until one is assigned, a track of packaging `wai` carries the property as type
`0x3A41`, from the range MOQT §8.4 sets aside for an application's own use. Another
application may give that type another meaning, so a sink reads it only on a track of
packaging `wai`.
- A non-zero `joules_micro` **MUST** be accompanied by `acquisition`. A joule
figure without its acquisition class is exactly what `energy-measurement.md`
§2 forbids, and putting it on the wire does not make it acceptable.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

98
wai/ietf/nmsf-comment.md Normal file
View file

@ -0,0 +1,98 @@
# Comment on `draft-herz-moq-nmsf-01`: a model digest and a determinism field
> Status: draft comment for the authors of `draft-herz-moq-nmsf-01` and the moq list.
> **Not sent.** Sending it is the owner's decision. Section numbers below are those of
> `draft-herz-moq-nmsf-01` unless another document is named.
Thank you for the draft. Two catalog fields would let a subscriber check two things
the current text leaves to agreement between the endpoints: which model weights a
track needs, and whether a decoder on another device reproduces what the encoder's
context assumes. Both proposals are small and OPTIONAL, and neither changes the
object wire format of §3.2.
## 1. A model digest
§3.9's `modelVersion` is a version string. Two endpoints can hold different weights
under the same string, through a fine-tune, a re-export or a quantised variant. A
learned decoder run against weights other than the encoder's does not fail: it
produces a plausible picture that is wrong.
Proposal: an OPTIONAL `modelDigest` member in the `nvc` object, the SHA-256 of the
weights' bytes as 64 lowercase hexadecimal digits, with two rules:
- a subscriber that holds no weights with that digest SHOULD NOT subscribe, which
extends §3.9's existing "SHOULD NOT subscribe" rule to a check a subscriber can
make;
- a decoder MUST NOT decode a track against weights whose digest differs.
Where the weights span several files (networks, entropy tables), the digest can
cover a listing of per-file digests. The WAI specification defines one form for such
a listing: one line per file, giving its SHA-256 and its path, in byte order of the
paths. A checksum utility can verify it without WAI software (WAI `SPEC.md` §3.1,
"What the digest covers").
## 2. A determinism field
§4.6 has the encoder run the synthesis transform so that its context buffer "contains
exactly the same data that a decoder would produce". That holds only where the
decoder computes the same values as the encoder, on whatever device it runs:
- **Entropy decode.** §4.2 step 4 entropy-decodes the latent with CDF parameters
derived from the decoded hyperprior. If that derivation runs in floating point, two
devices can derive different parameters from one bitstream, select different
distributions, and decode different symbols from then on.
- **Context.** §4.1 feeds each reconstructed frame back as the next frame's context.
Where the reconstruction differs between devices, the difference carries forward
until the next Intra frame. Where the entropy model also reads that context (a
temporal prior), the entropy decode itself diverges.
Floating-point basic operations are correctly rounded, but runtimes choose
accumulation order, fused multiply-add, vector width and reduced precision per
device, so the same network does not in general give the same bits on two devices.
Proposal: an OPTIONAL `entropyDeterminism` member in the `nvc` object, with two
values:
- `integer-exact`: the decoder derives every entropy-coding parameter in integer or
fixed-point arithmetic, or reads it from the bitstream, and so decodes the same
symbols on every device;
- `unchecked`: no such guarantee is made. This is the meaning when the member is
absent.
A codec whose whole decode, synthesis and context included, is integer could also
state that, so that a subscriber knows the reconstructed frames themselves reproduce.
WAI names these two levels `entropy-consistency` and `decode-equivalence`, and
explains why parameter transmission reaches the first but not the second
(`extensions/determinism-tiers.md` §2).
Both levels are attainable in practice. WAI registers learned decoders whose
hyper-decoder, entropy-parameter derivation and synthesis are integer
(`wai.neural.int_hyper`, and `wai.video.int_hyper` for integer motion-compensated
P-frames after such a keyframe). The repository's CI decodes their committed test
vectors on Linux x86_64, Linux arm64 and macOS arm64, and requires the same output
hashes on each (`extensions/determinism-tiers.md` §6). Those vectors are small, and
the full-scale model is checked against its exporter's integer reference on one
machine per run.
## 3. Integrity, for completeness
§8 relies on hop-by-hop TLS. TLS protects each hop, but not a payload against a relay
that alters it. A publisher signature per group, over the hashes of the group's
objects, carried on a separate track, lets a subscriber detect alteration across
relays and account for objects a relay dropped. If that is of interest, the WAI
receipts track defines one way to do it (`draft-charlot-moq-wai-packaging-00` §6;
`extensions/jwp-receipts.md` §2 and §3).
## Pointers
- WAI `SPEC.md` §3.1: the parameter-set pin, by SHA-256 over the whole set, its
verification order and its refusal reasons.
- `extensions/determinism-tiers.md` §2 and §3: the two tiers and what each lets a
receiver hash.
- `extensions/moq-streaming-format.md` §4 and §4.2: catalog fields that declare a
track's capabilities, its weakest tier, and the parameter sets its objects pin, by
digest.
- `draft-charlot-moq-wai-packaging-00` (in preparation): the same, as an MSF packaging.
All in the WAI repository: https://git.transaction.science/Transaction-Science/open-standards
(directory `wai/`).

View file

@ -2529,6 +2529,13 @@
"section": "§7.2",
"status": "defined"
},
{
"name": "wai_priors",
"kind": "catalog",
"document": "extensions/moq-streaming-format.md",
"section": "§4.2",
"status": "defined"
},
{
"name": "wai_receipt_alias",
"kind": "catalog",

View file

@ -62,7 +62,15 @@ receipt for it, so that step 5 requires `delivered-verified` of its layers.
track), conversion to on demand, and tracks that inherit the catalog track's
namespace, checked under that namespace, under another and with none known — and,
given the catalog track's namespace each case states, for each set the first rule it
fails. `trace.json` carries the envelopes of
fails. `priors.json` holds track entries around §4.2's `wai_priors`, each with the
manifests of objects on the track: members that read (absent, `null`, an advisory tier
that differs from the pin's, an unregistered tier, a `null` form, an undefined member,
a fallback no entry names, a pin listed only for its fallback, two sets for one
capability), each way an object breaks the catalog (a pin listed for another
capability, another digest, a form declared or dropped, no pin, another set for the
fallback, an empty list, a second object that breaks it) and each way the member does
not read, with the verdict a sink gives the track and each object
(`catalog_priors_malformed`, `catalog_prior_mismatch`). `trace.json` carries the envelopes of
`cases/video_int_motion_gop_perframe/` on those tracks, one object to a group (object
0, which each receipt names), through one relay to four subscribers: one of every
track; one of the base and layer 1 tracks only; one of those two whose relay drops the
@ -142,7 +150,7 @@ Every layer application, in every case, is checked by both.
exploding a `WAI2` component (reading the manifest's members as the text they are)
with the refinement role rules; what a packer refuses; `rate_port.json`, from its own
integer predictor and its own f64 form; and `moq/`: the catalog
check, each timeout as its slack, the priorities, each object's fate at the relay,
check, the `wai_priors` reading and check of §4.2, each timeout as its slack, the priorities, each object's fate at the relay,
and each subscriber's steps replayed through its sink. It takes BLAKE3 from
[`../claim-conformance/_blake3.py`](../claim-conformance/_blake3.py) and the rANS
decoder and the `WIV1` keyframe-0 decode from

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,9 @@ specifications, and implements the rest:
- MoQ carriage (moq-streaming-format §4.1): the catalog check, a layer object's timeout
as its slack, the priorities, and the trace of moq/: each object's fate at a relay by
MOQT's per-hop delivery timeout, each subscriber's sink steps, replayed through this
sink, and the status each layer object comes to.
sink, and the status each layer object comes to;
- a track's wai_priors (moq-streaming-format §4.2): whether it reads, and whether each
object of moq/priors.json breaks it.
For every case it starts from the case's stage0.bin, checks its SHA-256, decodes the base
independently where it can (WIV1 keyframe 0, PNG, FLAC) and compares, derives the base's
@ -1862,6 +1864,86 @@ def check_catalog(c, inherited=None):
return [{"set": n, "reason": check_set(n, sets[n], cat)} for n in sorted(sets)]
# moq-streaming-format §4.2, Pinned parameter sets.
PRIORS_MALFORMED, PRIORS_MISMATCH = "catalog_priors_malformed", "catalog_prior_mismatch"
HEX_LOWER = set("0123456789abcdef")
def read_priors(track):
"""§4.2, Reading. ("absent", None) where the track's wai_priors is absent or null;
("malformed", None) where it does not read; else ("ok", entries), each entry
(capability, sha256, form or None, tier read as a registered token, or None)."""
if not isinstance(track, dict) or track.get("wai_priors") is None:
return "absent", None
v = track["wai_priors"]
if not isinstance(v, list):
return "malformed", None
caps = track.get("wai_capabilities")
if not (isinstance(caps, list) and all(isinstance(c, str) for c in caps)):
caps = None
entries = []
for e in v:
if not isinstance(e, dict):
return "malformed", None
cap, digest, form, tier = e.get("capability"), e.get("sha256"), e.get("form"), e.get("tier")
if not isinstance(cap, str) or not isinstance(digest, str):
return "malformed", None
if len(digest) != 64 or any(ch not in HEX_LOWER for ch in digest):
return "malformed", None
if (form is not None and not isinstance(form, str)) or (tier is not None and not isinstance(tier, str)):
return "malformed", None
if caps is None or cap not in caps:
return "malformed", None
if any((x[0], x[1], x[2]) == (cap, digest, form) for x in entries):
return "malformed", None
if tier is not None and tier not in (DE, EC, NONE):
tier = NONE # an unregistered token reads as none (SPEC §3.1)
entries.append((cap, digest, form, tier))
return "ok", entries
def priors_object(entries, manifest):
"""§4.2, Mismatch: catalog_prior_mismatch where the object (its manifest) breaks the
catalog, else None. Its pin is conditioning.prior's sha256 and form; a pin whose
sha256 or form has the wrong type is listed by nothing."""
m = manifest if isinstance(manifest, dict) else {}
req = m.get("model_requirement") if isinstance(m.get("model_requirement"), dict) else {}
cap = req.get("capability") if isinstance(req.get("capability"), str) else ""
fb = req.get("fallback") if isinstance(req.get("fallback"), str) else None
cond = m.get("conditioning") if isinstance(m.get("conditioning"), dict) else {}
prior = cond.get("prior")
if prior is None:
pin = None
else:
p = prior if isinstance(prior, dict) else {}
digest, form = p.get("sha256"), p.get("form")
pin = (digest, form) if isinstance(digest, str) and (form is None or isinstance(form, str)) else ("", None)
names = [cap] + ([fb] if fb is not None else [])
def listed(c):
return pin is not None and any(e[0] == c and e[1] == pin[0] and e[2] == pin[1] for e in entries)
# 1. A pin no entry lists for the capability or for the fallback.
if pin is not None and not any(listed(c) for c in names):
return PRIORS_MISMATCH
# 2. A capability or fallback an entry names, with no pin, or a pin not listed for it.
for c in names:
if any(e[0] == c for e in entries) and not listed(c):
return PRIORS_MISMATCH
return None
def priors_expect(track, objects):
"""What §4.2 gives a track and its objects (see moq/priors.json's `about`)."""
st, entries = read_priors(track)
if st == "malformed":
return {"track": PRIORS_MALFORMED, "objects": None}
if st == "absent":
return {"track": None, "objects": [None for _ in objects]}
per = [priors_object(entries, m) for m in objects]
return {"track": next((x for x in per if x is not None), None), "objects": per}
def presentation(clock, unit):
return min(clock["t0_ms"] + unit * clock["unit_ms"] + clock["target_latency_ms"], U64)
@ -2114,6 +2196,13 @@ def verify_moq(d, fails):
got = check_catalog(c["catalog"], c["namespace"])
if got != c["expect"]:
fail(f"catalogs.json {c['name']}: {got} != {c['expect']}")
pcases = json.load(open(os.path.join(md, "priors.json")))["cases"]
if not pcases:
fail("priors.json has no case")
for c in pcases:
got = priors_expect(c["track"], c["objects"])
if got != c["expect"]:
fail(f"priors.json {c['name']}: {got} != {c['expect']}")
t = json.load(open(os.path.join(md, "trace.json")))
pr = [x["priority"] for x in t["tracks"]]
if not (all(pr[0] < x for x in pr[1:]) and all(a <= b for a, b in zip(pr[1:], pr[2:]))):

View file

@ -11,7 +11,8 @@
//! conforming sink reports at each step and holds at the end. It also writes
//! `schedule.json` (arrival traces and what a sink presents), `index.json`, and `moq/`:
//! the MSF catalog of a stage set carried over MoQ, catalogs around its check
//! (moq-streaming-format §4.1), and a trace of the set's objects through a relay that
//! (moq-streaming-format §4.1), track entries whose `wai_priors` (§4.2) does or does not
//! read with objects that do or do not match it, and a trace of the set's objects through a relay that
//! drops late layers by their delivery timeout, with what each subscriber's sink then
//! does. Every file is a function of those inputs, so a second run writes the same bytes.
//!
@ -39,15 +40,15 @@ use serde_json::{json, Value};
use wai::codecs::{
DeclaredTier, CAP_AUDIO_FLAC, CAP_AUDIO_INT_CODEC, CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_IMAGE_PNG,
CAP_NEURAL_INT_HYPER, CAP_PRIOR_SET, CAP_SPLAT_INT_CODEC, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_HYPER,
CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_SYNTH, CAP_PRIOR_SET, CAP_SPLAT_INT_CODEC, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_HYPER,
CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE,
};
use wai::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, Prior, Refines, Rendition, RenditionMeta, Span, Wai, WaiMulti};
use wai::int_payload::{decode_int, resolve, IntDecoded};
use wai::pin::HeldBytes;
use wai::staged::moq::{
check_catalog, check_catalog_in, depends_for, effective_timeout_ms, expired_at_hop, layer_timeout_ms, priorities_ok, receipts_track_json, SetRefusal,
SetTrack, StagedTrack,
check_catalog, check_catalog_in, depends_for, effective_timeout_ms, expired_at_hop, layer_timeout_ms, priorities_ok, receipts_track_json, CatalogPriors,
SetRefusal, SetTrack, StagedTrack,
};
use wai::staged::schedule::{deadline_misses, present, UnitClock};
use wai::staged::wir1::{self, Below, Next, Predictor, RefineParams, UnitKind, Wir1Cost};
@ -2492,10 +2493,147 @@ fn moq_files(all: &[Case]) -> Vec<(String, Vec<u8>)> {
vec![
("moq/catalog.json".into(), json_bytes(&moq_catalog())),
("moq/catalogs.json".into(), json_bytes(&moq_catalogs_json())),
("moq/priors.json".into(), json_bytes(&moq_priors_json())),
("moq/trace.json".into(), json_bytes(&trace)),
]
}
// ───────────────────────── Pinned parameter sets (moq-streaming-format §4.2) ─────────────────────────
/// Two parameter-set digests the cases pin: 64 lowercase hexadecimal digits each.
const PRIORS_A: &str = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1";
const PRIORS_B: &str = "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2";
/// The catalog entry of an on-demand track of packaging `wai` that declares `int_hyper`,
/// `int_synth` and PNG, with `priors` as its `wai_priors` where given.
fn priors_track(priors: Option<Value>) -> Value {
let mut t = json!({"namespace": "example/net", "name": "pictures", "packaging": "wai", "isLive": false, "role": "video",
"wai_capabilities": [CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG], "wai_receipts": false});
if let Some(p) = priors {
t["wai_priors"] = p;
}
t
}
/// An entry of `wai_priors`.
fn priors_entry(cap: &str, sha: &str, form: Option<&str>, tier: Option<&str>) -> Value {
let mut e = json!({"capability": cap, "sha256": sha});
if let Some(f) = form {
e["form"] = json!(f);
}
if let Some(t) = tier {
e["tier"] = json!(t);
}
e
}
/// A `WAI1` manifest (SPEC §3) of `cap`, falling back to `fb`, pinned to (`sha`, `form`)
/// at `tier`, or unpinned.
fn priors_manifest(cap: &str, fb: Option<&str>, pin: Option<(&str, Option<&str>, &str)>) -> Value {
let mut m = json!({"wai": "1.0", "media": "image", "intent": "replicate",
"model_requirement": {"capability": cap, "fallback": fb}, "conditioning": {"kind": "int"}});
if let Some((sha, form, tier)) = pin {
let mut p = json!({"id": "set", "sha256": sha, "determinism": "int-rans+int-synth"});
if let Some(f) = form {
p["form"] = json!(f);
}
p["tier"] = json!(tier);
m["conditioning"]["prior"] = p;
}
m
}
/// What §4.2 gives a track and its objects: `catalog_priors_malformed` where the member
/// does not read (and no object is checked); otherwise each object's verdict, and the
/// track's, the first object's refusal; nothing checked where the member is absent.
fn moq_priors_expect(track: &Value, objects: &[Value]) -> Value {
match CatalogPriors::read(track) {
Err(r) => json!({"track": r.code(), "objects": null}),
Ok(None) => json!({"track": null, "objects": objects.iter().map(|_| Value::Null).collect::<Vec<_>>()}),
Ok(Some(p)) => {
let per: Vec<Value> = objects.iter().map(|m| p.check_manifest(m).err().map_or(Value::Null, |r| json!(r.code()))).collect();
let track = per.iter().find(|v| !v.is_null()).cloned().unwrap_or(Value::Null);
json!({"track": track, "objects": per})
}
}
}
/// The cases: a track entry, the manifests of its objects, and what each states.
fn moq_priors_cases() -> Vec<(&'static str, &'static str, Value, Vec<Value>)> {
let (h, s, png, set) = (CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG, CAP_PRIOR_SET);
let (a, b) = (PRIORS_A, PRIORS_B);
let de = "decode-equivalence";
let track = |entries: Vec<Value>| priors_track(Some(Value::Array(entries)));
let pinned = |cap: &'static str, fb: Option<&'static str>, sha: &'static str, form: Option<&'static str>| priors_manifest(cap, fb, Some((sha, form, de)));
let unpinned = |cap: &'static str| priors_manifest(cap, None, None);
let mut no_caps = priors_track(Some(json!([priors_entry(h, a, Some(set), None)])));
no_caps.as_object_mut().expect("entry").remove("wai_capabilities");
vec![
// Members that read, and objects that match.
("listed", "the object's pin is the one listed for its capability", track(vec![priors_entry(h, a, Some(set), Some(de))]), vec![pinned(h, None, a, Some(set))]),
("absent", "no wai_priors: nothing is checked, pinned or not", priors_track(None), vec![pinned(h, None, a, Some(set)), unpinned(h)]),
("null", "wai_priors null is absent", priors_track(Some(Value::Null)), vec![pinned(h, None, b, None)]),
(
"tier_advisory",
"the listed tier differs from the pin's: advisory, no mismatch",
track(vec![priors_entry(h, a, Some(set), Some(de))]),
vec![priors_manifest(h, None, Some((a, Some(set), "entropy-consistency")))],
),
("tier_unregistered", "a tier token SPEC §3.1 does not register reads as none, and the member reads", track(vec![priors_entry(h, a, Some(set), Some("bit-exact"))]), vec![pinned(h, None, a, Some(set))]),
("form_null", "a null form, listed and pinned, is an absent one", track(vec![json!({"capability": h, "sha256": a, "form": null})]), vec![pinned(h, None, a, None)]),
("unknown_member", "a member of an entry that §4.2 does not define is ignored", track(vec![json!({"capability": h, "sha256": a, "form": set, "id": "int_hyper/q6"})]), vec![pinned(h, None, a, Some(set))]),
("fallback_unnamed", "a fallback no entry names needs nothing listed", track(vec![priors_entry(h, a, Some(set), None)]), vec![pinned(h, Some(png), a, Some(set))]),
("listed_for_fallback", "a pin listed only for the fallback, where it decodes", track(vec![priors_entry(s, a, Some(set), None)]), vec![pinned("wai.neural.unregistered", Some(s), a, Some(set))]),
("two_sets", "two sets for one capability: either pin matches", track(vec![priors_entry(h, a, Some(set), None), priors_entry(h, b, Some(set), None)]), vec![pinned(h, None, b, Some(set)), pinned(h, None, a, Some(set))]),
("unpinned_unnamed", "an unpinned object whose capability no entry names", track(vec![priors_entry(h, a, Some(set), None)]), vec![unpinned(s)]),
// An object that breaks the catalog: refused, and the track with it.
("mismatch_capability", "the pin is listed, but for a capability the object does not name", track(vec![priors_entry(s, a, Some(set), None)]), vec![pinned(h, None, a, Some(set))]),
("mismatch_digest", "the object's capability is listed with another digest", track(vec![priors_entry(h, b, Some(set), None)]), vec![pinned(h, None, a, Some(set))]),
("mismatch_form", "the pin declares a form; the entry lists none", track(vec![priors_entry(h, a, None, None)]), vec![pinned(h, None, a, Some(set))]),
("mismatch_form_dropped", "the entry lists a form; the pin declares none", track(vec![priors_entry(h, a, Some(set), None)]), vec![pinned(h, None, a, None)]),
("mismatch_unpinned", "the catalog names the capability; the object carries no pin", track(vec![priors_entry(h, a, Some(set), None)]), vec![unpinned(h)]),
(
"mismatch_fallback",
"the capability's set is listed, the fallback's is another",
track(vec![priors_entry(h, a, Some(set), None), priors_entry(s, b, Some(set), None)]),
vec![pinned(h, Some(s), a, Some(set))],
),
("mismatch_empty", "an empty list: an object that carries a pin breaks it", track(vec![]), vec![pinned(h, None, a, Some(set))]),
(
"mismatch_second_object",
"the first object matches, the second does not: the track is refused at the second",
track(vec![priors_entry(h, a, Some(set), None)]),
vec![pinned(h, None, a, Some(set)), pinned(h, None, b, Some(set))],
),
// A member that does not read: the sink does not subscribe.
("malformed_not_array", "wai_priors is an object", priors_track(Some(json!({"capability": h, "sha256": a}))), vec![]),
("malformed_entry", "an entry that is not an object", track(vec![json!(a)]), vec![]),
("malformed_no_capability", "an entry without capability", track(vec![json!({"sha256": a})]), vec![]),
("malformed_undeclared", "a capability the track's wai_capabilities does not list", track(vec![priors_entry("wai.neural.elic", a, None, None)]), vec![]),
("malformed_no_capabilities", "a track with no wai_capabilities to list the capability", no_caps, vec![]),
("malformed_uppercase", "a digest in upper-case digits", track(vec![priors_entry(h, &PRIORS_A.to_uppercase(), None, None)]), vec![]),
("malformed_short", "a digest of 62 digits", track(vec![priors_entry(h, &PRIORS_A[..62], None, None)]), vec![]),
("malformed_form", "a form that is not a string", track(vec![json!({"capability": h, "sha256": a, "form": 5})]), vec![]),
("malformed_tier", "a tier that is not a string", track(vec![json!({"capability": h, "sha256": a, "tier": true})]), vec![]),
("malformed_duplicate", "the same capability, digest and form twice", track(vec![priors_entry(h, a, Some(set), None), priors_entry(h, a, Some(set), Some(de))]), vec![]),
]
}
fn moq_priors_json() -> Value {
let cases: Vec<Value> = moq_priors_cases()
.into_iter()
.map(|(name, about, track, objects)| {
let expect = moq_priors_expect(&track, &objects);
json!({"name": name, "about": about, "track": track, "objects": objects, "expect": expect})
})
.collect();
json!({
"format": "wai-staged-moq-priors/1",
"about": "moq-streaming-format §4.2, Pinned parameter sets: a track's catalog entry, the manifests of objects on it in order, and what a sink gives them. `expect.track` is catalog_priors_malformed where wai_priors does not read (then no object is checked, and `expect.objects` is null); otherwise the first object's catalog_prior_mismatch, or null. `expect.objects` gives each object's verdict: catalog_prior_mismatch where it breaks the catalog, null otherwise, and null for every object of a track without wai_priors.",
"cases": cases,
})
}
/// The MoQ files, checked from what is committed: the catalog passes §4.1's check; each
/// catalog case gives its stated reasons; and the trace's timeouts, priorities and expected
/// outcome follow from its inputs and the case's committed envelopes and stage 0.
@ -2510,6 +2648,17 @@ fn verify_moq(dir: &Path, fail: &mut dyn FnMut(String)) {
fail(format!("moq/catalogs.json {}: the check gives other reasons", c["name"]));
}
}
let priors = read("moq/priors.json");
let priors_cases = priors["cases"].as_array().cloned().unwrap_or_default();
if priors_cases.is_empty() {
fail("moq/priors.json: no case".into());
}
for c in &priors_cases {
let objects = c["objects"].as_array().cloned().unwrap_or_default();
if moq_priors_expect(&c["track"], &objects) != c["expect"] {
fail(format!("moq/priors.json {}: the check gives other verdicts", c["name"]));
}
}
let trace = read("moq/trace.json");
let u = |v: &Value| v.as_u64().unwrap_or(u64::MAX);
let tracks = trace["tracks"].as_array().cloned().unwrap_or_default();

View file

@ -577,6 +577,7 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
wire("wai_receipts", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined),
wire("wai_receipt_alias", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined),
wire("wai_receipts_track", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined),
wire("wai_priors", WireKind::Catalog, MOQ_FORMAT, "§4.2", WireStatus::Defined),
wire("wai_staged", WireKind::Catalog, MOQ_FORMAT, "§4.1", WireStatus::Defined),
wire("wai-receipts", WireKind::Catalog, MOQ_FORMAT, "§4.1", WireStatus::Defined),
wire("WAI_RECEIPT", WireKind::Catalog, MOQ_FORMAT, "§5", WireStatus::Defined),

View file

@ -13,6 +13,10 @@
//! - [`effective_timeout_ms`] and [`expired_at_hop`] are MOQT's `OBJECT_DELIVERY_TIMEOUT`
//! rule (draft-ietf-moq-transport-22 §5.2) at one hop; [`layer_timeout_ms`] and
//! [`base_timeout_floor_ms`] are §4.1's bounds on the timeouts a publisher sets.
//! - [`CatalogPriors`] is a track's `wai_priors` (§4.2), on any track of packaging `wai`:
//! [`CatalogPriors::read`] reads it, giving [`PriorsRefusal::Malformed`] where it does
//! not read, and [`CatalogPriors::check`] / [`CatalogPriors::check_manifest`] check an
//! object against it before dispatch ([`PriorsRefusal::Mismatch`]).
//!
//! Pure functions of their inputs, no I/O: the corpus's MoQ trace
//! (`staged-conformance/moq/`) is replayed the same way here, by joule-moq's relay and
@ -23,6 +27,7 @@ use std::collections::{BTreeMap, BTreeSet};
use serde_json::{json, Map, Value};
use super::schedule::{presentation_ms, UnitClock};
use crate::codecs::DeclaredTier;
/// The `packaging` of a track whose objects are WAI envelopes (moq-streaming-format §3).
pub const PACKAGING: &str = "wai";
@ -436,6 +441,191 @@ fn check_receipts(es: &[Entry], cat: &Catalog<'_>) -> Result<(), SetRefusal> {
Ok(())
}
// ── Pinned parameter sets (§4.2) ────────────────────────────────────────────
/// Why a sink refuses a track over its `wai_priors` (moq-streaming-format §4.2).
///
/// cbindgen:ignore
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PriorsRefusal {
/// The member does not read: the sink does not subscribe to the track.
Malformed,
/// An object breaks the catalog: the sink refuses the object, and the track with it.
Mismatch,
}
impl PriorsRefusal {
/// Every reason, in §4.2's order.
///
/// cbindgen:ignore
pub const ALL: [PriorsRefusal; 2] = [Self::Malformed, Self::Mismatch];
/// The token §4.2 names.
pub fn code(self) -> &'static str {
match self {
Self::Malformed => "catalog_priors_malformed",
Self::Mismatch => "catalog_prior_mismatch",
}
}
/// The reason whose token is `code`.
pub fn from_code(code: &str) -> Option<PriorsRefusal> {
Self::ALL.into_iter().find(|r| r.code() == code)
}
}
/// One entry of a track's `wai_priors` (§4.2): a pin an object on the track carries, for
/// one capability, and the tier the publisher offers its set at.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogPrior {
/// A capability the track's `wai_capabilities` lists.
pub capability: String,
/// The pin's `sha256`: 64 lowercase hexadecimal digits.
pub sha256: String,
/// The pin's `form`; `None` where the pin declares none.
pub form: Option<String>,
/// The listed tier, as a sink reads it: `None` where none is listed, and
/// [`DeclaredTier::None`] for a token SPEC §3.1 does not register. Advisory: a sink
/// claims the tier its own decode earns, never this one.
pub tier: Option<DeclaredTier>,
}
impl CatalogPrior {
/// Whether this entry lists the pin (`sha256`, `form`) for `capability`.
pub fn lists(&self, capability: &str, sha256: &str, form: Option<&str>) -> bool {
self.capability == capability && self.sha256 == sha256 && self.form.as_deref() == form
}
/// The entry as JSON; an absent `form` or `tier` is not written.
pub fn to_json(&self) -> Value {
let mut o = Map::new();
o.insert("capability".into(), json!(self.capability));
o.insert("sha256".into(), json!(self.sha256));
if let Some(f) = &self.form {
o.insert("form".into(), json!(f));
}
if let Some(t) = self.tier {
o.insert("tier".into(), json!(t.token()));
}
Value::Object(o)
}
}
/// A track's `wai_priors` (§4.2), read.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CatalogPriors {
pub entries: Vec<CatalogPrior>,
}
/// 64 lowercase hexadecimal digits, as SPEC §3.1 writes a pin's `sha256`.
fn is_sha256_hex(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
/// An OPTIONAL string member: `Ok(None)` absent or `null`, `Err` for any other type.
fn opt_str(o: &Map<String, Value>, k: &str) -> Result<Option<String>, PriorsRefusal> {
match o.get(k) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(s)) => Ok(Some(s.clone())),
Some(_) => Err(PriorsRefusal::Malformed),
}
}
impl CatalogPriors {
/// The `wai_priors` of a track's catalog entry (§4.2, *Reading*): `Ok(None)` where it
/// is absent or `null`; otherwise its entries, or [`PriorsRefusal::Malformed`] where it
/// is not an array of objects, an entry lacks `capability` or `sha256` or holds a
/// member of the wrong type, a `sha256` is not 64 lowercase hexadecimal digits, two
/// entries share `capability`, `sha256` and `form`, or an entry's `capability` is not
/// one the entry's `wai_capabilities` lists. A member of an entry §4.2 does not define
/// is ignored.
pub fn read(track: &Value) -> Result<Option<CatalogPriors>, PriorsRefusal> {
let m = PriorsRefusal::Malformed;
let list = match track.get("wai_priors") {
None | Some(Value::Null) => return Ok(None),
Some(Value::Array(a)) => a,
Some(_) => return Err(m),
};
let caps: Option<Vec<&str>> =
track.get("wai_capabilities").and_then(Value::as_array).and_then(|a| a.iter().map(Value::as_str).collect());
let mut entries: Vec<CatalogPrior> = Vec::with_capacity(list.len());
for v in list {
let o = v.as_object().ok_or(m)?;
let capability = o.get("capability").and_then(Value::as_str).ok_or(m)?.to_string();
let sha256 = o.get("sha256").and_then(Value::as_str).filter(|s| is_sha256_hex(s)).ok_or(m)?.to_string();
let form = opt_str(o, "form")?;
let tier = opt_str(o, "tier")?.map(|t| DeclaredTier::from_token(&t).unwrap_or(DeclaredTier::None));
if !caps.as_ref().is_some_and(|c| c.contains(&capability.as_str())) {
return Err(m);
}
if entries.iter().any(|e| e.lists(&capability, &sha256, form.as_deref())) {
return Err(m);
}
entries.push(CatalogPrior { capability, sha256, form, tier });
}
Ok(Some(CatalogPriors { entries }))
}
/// The member as JSON, entries in order.
pub fn to_json(&self) -> Value {
Value::Array(self.entries.iter().map(CatalogPrior::to_json).collect())
}
/// Whether some entry names `capability`.
pub fn names(&self, capability: &str) -> bool {
self.entries.iter().any(|e| e.capability == capability)
}
/// Whether some entry lists the pin (`sha256`, `form`) for `capability`.
pub fn lists(&self, capability: &str, sha256: &str, form: Option<&str>) -> bool {
self.entries.iter().any(|e| e.lists(capability, sha256, form))
}
/// §4.2, *Mismatch*: check one object, whose envelope names `capability` and
/// `fallback` and carries `pin` (its `sha256` and `form`) or none, before it is
/// dispatched. [`PriorsRefusal::Mismatch`] where it carries a pin that no entry lists
/// for its capability or for its fallback; or where an entry names its capability, or
/// its fallback, and it carries no pin, or a pin no entry lists for that capability.
/// Passing proves nothing about the pin: a sink still verifies it under SPEC §3.1.
pub fn check(&self, capability: &str, fallback: Option<&str>, pin: Option<(&str, Option<&str>)>) -> Result<(), PriorsRefusal> {
let named: Vec<&str> = std::iter::once(capability).chain(fallback).collect();
let listed = |c: &str| pin.is_some_and(|(sha, form)| self.lists(c, sha, form));
if pin.is_some() && !named.iter().any(|c| listed(c)) {
return Err(PriorsRefusal::Mismatch);
}
if named.iter().any(|c| self.names(c) && !listed(c)) {
return Err(PriorsRefusal::Mismatch);
}
Ok(())
}
/// [`CatalogPriors::check`] over an envelope's manifest (SPEC §3): its
/// `model_requirement.capability` and `.fallback`, and the `sha256` and `form` of its
/// `conditioning.prior`, where that is present and not `null` (a `null` `form` is an
/// absent one, SPEC §3.1). A member of the wrong type makes a manifest malformed,
/// which no sink dispatches (SPEC §3.1). Here such a capability reads as the empty
/// string, and a pin whose `sha256` or `form` has the wrong type as a pin no entry
/// lists.
pub fn check_manifest(&self, manifest: &Value) -> Result<(), PriorsRefusal> {
let req = manifest.get("model_requirement");
let capability = req.and_then(|r| r.get("capability")).and_then(Value::as_str).unwrap_or("");
let fallback = req.and_then(|r| r.get("fallback")).and_then(Value::as_str);
let pin = match manifest.get("conditioning").and_then(|c| c.get("prior")) {
None | Some(Value::Null) => None,
Some(p) => {
let sha = p.get("sha256").and_then(Value::as_str);
match (sha, p.get("form")) {
(Some(s), None | Some(Value::Null)) => Some((s, None)),
(Some(s), Some(Value::String(f))) => Some((s, Some(f.as_str()))),
// No entry's digest is empty (`read` requires 64 digits).
_ => Some(("", None)),
}
}
};
self.check(capability, fallback, pin)
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -685,8 +875,156 @@ mod tests {
let i = text[at..].find(&tok).unwrap_or_else(|| panic!("{tok} is not in §4.1 after the one before it"));
at += i + tok.len();
}
for name in ["\"wai_staged\"", "`wai_receipt_alias`", "`wai_receipts_track`", "\"wai-receipts\""] {
for name in ["\"wai_staged\"", "`wai_receipt_alias`", "`wai_receipts_track`", "\"wai-receipts\"", "`wai_priors`"] {
assert!(text.contains(name), "{name}");
}
let at42 = text.find("### 4.2 Pinned parameter sets").expect("§4.2");
let mut at = at42;
for r in PriorsRefusal::ALL {
let tok = format!("`{}`", r.code());
let i = text[at..].find(&tok).unwrap_or_else(|| panic!("{tok} is not in §4.2 after the one before it"));
at += i + tok.len();
}
}
const A: &str = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1";
const B: &str = "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2";
const HYPER: &str = "wai.neural.int_hyper";
const SYNTH: &str = "wai.neural.int_synth";
const SET: &str = "wai.prior.set";
/// A track entry declaring `int_hyper`, `int_synth` and PNG, with `priors` as its
/// `wai_priors`.
fn priors_track(priors: Value) -> Value {
json!({"namespace": "example/net", "name": "pic", "packaging": "wai", "isLive": false,
"wai_capabilities": [HYPER, SYNTH, "wai.image.png"], "wai_receipts": false, "wai_priors": priors})
}
fn entry(cap: &str, sha: &str, form: Option<&str>) -> Value {
let mut e = json!({"capability": cap, "sha256": sha});
if let Some(f) = form {
e["form"] = json!(f);
}
e
}
fn read(priors: Value) -> Result<Option<CatalogPriors>, PriorsRefusal> {
CatalogPriors::read(&priors_track(priors))
}
/// A manifest for `cap`, falling back to `fb`, pinned to (`sha`, `form`) or unpinned.
fn manifest(cap: &str, fb: Option<&str>, pin: Option<(&str, Option<&str>)>) -> Value {
let mut m = json!({"wai": "1.0", "media": "image", "intent": "replicate",
"model_requirement": {"capability": cap, "fallback": fb}, "conditioning": {"kind": "int"}});
if let Some((sha, form)) = pin {
let mut p = json!({"id": "set", "sha256": sha, "determinism": "int", "tier": "decode-equivalence"});
if let Some(f) = form {
p["form"] = json!(f);
}
m["conditioning"]["prior"] = p;
}
m
}
/// §4.2, *Reading*: the member reads where it is an array of entries of their types,
/// and every other value is `catalog_priors_malformed`.
#[test]
fn catalog_priors_read_and_refuse_each_malformation() {
assert_eq!(CatalogPriors::read(&json!({"wai_capabilities": [HYPER]})), Ok(None), "absent");
assert_eq!(read(json!(null)), Ok(None), "null is absent");
assert_eq!(read(json!([])), Ok(Some(CatalogPriors::default())), "an empty list: the objects carry no pin");
let ok = read(json!([{"capability": HYPER, "sha256": A, "form": SET, "tier": "decode-equivalence", "id": "x"}])).unwrap().unwrap();
assert_eq!(
ok.entries,
vec![CatalogPrior { capability: HYPER.into(), sha256: A.into(), form: Some(SET.into()), tier: Some(DeclaredTier::DecodeEquivalence) }],
"an undefined member (`id`) is ignored"
);
assert_eq!(CatalogPriors::read(&priors_track(ok.to_json())), Ok(Some(ok.clone())), "round trip");
let unregistered = read(json!([{"capability": HYPER, "sha256": A, "tier": "bit-exact"}])).unwrap().unwrap();
assert_eq!(unregistered.entries[0].tier, Some(DeclaredTier::None), "an unregistered tier reads as none, and is no malformation");
let null_form = read(json!([{"capability": HYPER, "sha256": A, "form": null, "tier": null}])).unwrap().unwrap();
assert_eq!((null_form.entries[0].form.as_deref(), null_form.entries[0].tier), (None, None), "null members are absent ones");
let m = Err(PriorsRefusal::Malformed);
for (bad, why) in [
(json!({}), "not an array"),
(json!("a1"), "not an array"),
(json!([5]), "an entry that is not an object"),
(json!([{"sha256": A}]), "no capability"),
(json!([{"capability": 7, "sha256": A}]), "a capability that is not a string"),
(json!([{"capability": "wai.neural.elic", "sha256": A}]), "a capability wai_capabilities does not list"),
(json!([{"capability": HYPER}]), "no sha256"),
(json!([{"capability": HYPER, "sha256": A.to_uppercase()}]), "upper-case digits"),
(json!([{"capability": HYPER, "sha256": &A[..62]}]), "62 digits"),
(json!([{"capability": HYPER, "sha256": format!("{}g", &A[..63])}]), "a digit that is not hexadecimal"),
(json!([{"capability": HYPER, "sha256": A, "form": 5}]), "a form that is not a string"),
(json!([{"capability": HYPER, "sha256": A, "tier": true}]), "a tier that is not a string"),
(json!([entry(HYPER, A, Some(SET)), entry(HYPER, A, Some(SET))]), "the same entry twice"),
] {
assert_eq!(read(bad.clone()), m, "{why}: {bad}");
}
// The same digest under two forms, or for two capabilities, is two entries.
assert!(read(json!([entry(HYPER, A, Some(SET)), entry(HYPER, A, None), entry(SYNTH, A, Some(SET))])).unwrap().is_some());
let no_caps = json!({"name": "pic", "packaging": "wai", "wai_priors": [entry(HYPER, A, None)]});
assert_eq!(CatalogPriors::read(&no_caps), m, "no wai_capabilities to list the capability");
for r in PriorsRefusal::ALL {
assert_eq!(PriorsRefusal::from_code(r.code()), Some(r));
}
}
/// §4.2, *Mismatch*: an object whose pin the catalog does not list, or lists
/// differently, is refused; a catalog entry never stands in for a pin; a listed tier
/// is advisory and decides nothing.
#[test]
fn catalog_priors_refuse_each_mismatch() {
let p = |v: Value| read(v).unwrap().unwrap();
let mismatch = Err(PriorsRefusal::Mismatch);
let hyper_a = p(json!([{"capability": HYPER, "sha256": A, "form": SET, "tier": "decode-equivalence"}]));
// Listed: passes, whatever tier the pin states.
assert_eq!(hyper_a.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), Ok(()));
let mut ec = manifest(HYPER, None, Some((A, Some(SET))));
ec["conditioning"]["prior"]["tier"] = json!("entropy-consistency");
assert_eq!(hyper_a.check_manifest(&ec), Ok(()), "a listed tier is advisory: a different pin tier is no mismatch");
// A fallback the catalog does not name needs nothing listed.
assert_eq!(hyper_a.check_manifest(&manifest(HYPER, Some("wai.image.png"), Some((A, Some(SET))))), Ok(()));
// Capability: the digest is listed, but for a capability the object does not name.
let synth_a = p(json!([entry(SYNTH, A, Some(SET))]));
assert_eq!(synth_a.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "capability");
// Digest.
let hyper_b = p(json!([entry(HYPER, B, Some(SET))]));
assert_eq!(hyper_b.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "digest");
// Form, either way.
let hyper_a_noform = p(json!([entry(HYPER, A, None)]));
assert_eq!(hyper_a_noform.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "form declared, none listed");
assert_eq!(hyper_a.check_manifest(&manifest(HYPER, None, Some((A, None)))), mismatch, "form listed, none declared");
let mut null_form = manifest(HYPER, None, Some((A, None)));
null_form["conditioning"]["prior"]["form"] = Value::Null;
assert_eq!(hyper_a_noform.check_manifest(&null_form), Ok(()), "a null form is an absent one");
// Unpinned: the catalog names the capability, the envelope carries no pin.
assert_eq!(hyper_a.check_manifest(&manifest(HYPER, None, None)), mismatch, "unpinned");
assert_eq!(hyper_a.check_manifest(&manifest(SYNTH, None, None)), Ok(()), "an unpinned capability the catalog does not name");
// Fallback: the capability's set is listed, the fallback's is another.
let both = p(json!([entry(HYPER, A, Some(SET)), entry(SYNTH, B, Some(SET))]));
assert_eq!(both.check_manifest(&manifest(HYPER, Some(SYNTH), Some((A, Some(SET))))), mismatch, "fallback");
let both_a = p(json!([entry(HYPER, A, Some(SET)), entry(SYNTH, A, Some(SET))]));
assert_eq!(both_a.check_manifest(&manifest(HYPER, Some(SYNTH), Some((A, Some(SET))))), Ok(()));
// A pin listed only for the fallback passes: it decodes there.
assert_eq!(synth_a.check_manifest(&manifest("wai.neural.unregistered", Some(SYNTH), Some((A, Some(SET))))), Ok(()));
// An empty list: no object may carry a pin.
let none = p(json!([]));
assert_eq!(none.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "empty list, pinned object");
assert_eq!(none.check_manifest(&manifest(HYPER, None, None)), Ok(()));
// Two sets for one capability: either pin passes.
let two = p(json!([entry(HYPER, A, Some(SET)), entry(HYPER, B, Some(SET))]));
assert_eq!(two.check_manifest(&manifest(HYPER, None, Some((B, Some(SET))))), Ok(()));
// A mistyped pin member is listed by nothing.
let mut bad_form = manifest(HYPER, None, Some((A, None)));
bad_form["conditioning"]["prior"]["form"] = json!(5);
assert_eq!(hyper_a_noform.check_manifest(&bad_form), mismatch, "a form of the wrong type");
let mut bad_sha = manifest(HYPER, None, Some((A, None)));
bad_sha["conditioning"]["prior"]["sha256"] = json!(5);
assert_eq!(hyper_a_noform.check_manifest(&bad_sha), mismatch, "a digest of the wrong type");
// The typed form gives the same verdicts.
assert_eq!(hyper_a.check(HYPER, None, Some((A, Some(SET)))), Ok(()));
assert_eq!(hyper_a.check(HYPER, None, None), mismatch);
}
}