wai/ietf: draft-charlot-moq-wai-packaging-00 (review only, not submitted) + wai_priors #383
13 changed files with 5684 additions and 10 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
2016
wai/ietf/draft-charlot-moq-wai-packaging-00.txt
Normal file
2016
wai/ietf/draft-charlot-moq-wai-packaging-00.txt
Normal file
File diff suppressed because it is too large
Load diff
1601
wai/ietf/draft-charlot-moq-wai-packaging-00.xml
Normal file
1601
wai/ietf/draft-charlot-moq-wai-packaging-00.xml
Normal file
File diff suppressed because it is too large
Load diff
98
wai/ietf/nmsf-comment.md
Normal file
98
wai/ietf/nmsf-comment.md
Normal 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/`).
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1284
wai/staged-conformance/moq/priors.json
Normal file
1284
wai/staged-conformance/moq/priors.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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:]))):
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue