### Staged tracks over MoQ: a track per layer, a receipts track per track, delivery timeouts per hop, chains that recover

- **Re-read against the drafts.** [moq-streaming-format](extensions/moq-streaming-format.md)
  names the revisions it was last read against, `draft-ietf-moq-transport-22`,
  `draft-ietf-moq-msf-01` and `draft-ietf-moq-cmsf-01`, and cites their sections. The
  re-read corrected: MSF's packaging values (`catalog` is a track name, not a value); what
  orders delivery (MOQT, subscriber priority first, then publisher priority, MOQT
  §5.1.2); how properties are carried (MOQT's track and object properties, of which MSF
  defines one); `SUBGROUP_DELIVERY_TIMEOUT`, which runs from the subgroup's FIN to its
  acknowledgement, not from an object's arrival; that MSF maps every object to a stream
  of its own, so a staged object never travels as a datagram; and that MOQT-22 replaced
  the Joining FETCH with fill fetch streams.
- **§4.1 Staged tracks.** A stage set is carried one track per layer: base tracks and a
  track for each layer index, each object one whole envelope. Each entry carries
  `wai_staged` (`set`, `layer`, `lead_ms`). A set's extent is its tracks, layers 0 to the
  highest with a track at each, so an MSF delta update that adds, removes or clones a
  track changes it alone; `lead_ms` is read only while the set is live. Tracks are named
  by namespace and name (a namespace inherited as MSF says), unique in a catalog, one
  namespace to a set; the check needs the catalog track's namespace, and a checker that
  does not know it refuses a set that mixes declared and inherited namespaces; `renderGroup` is an integer that fits 64 unsigned bits and
  `depends` an array of strings, each naming a track of the catalog in the entry's own
  namespace; a layer track depends on a track of every lower layer and on none of its
  own or above. A sink checks a set's entries in a fixed order and reports the first
  failure (`staged-malformed`, `staged-set-incomplete`, `staged-depends`,
  `staged-grouping`, `staged-lead`, `staged-receipts`), treating a failing set's layer
  tracks as absent. On a live track each group carries one object in a subgroup of its
  own. The base track's publisher priority is first among the set's (lower numbers first),
  though a subscriber's own priority comes before it. A layer object's
  `OBJECT_DELIVERY_TIMEOUT` is at most its slack to the last unit it covers, settable
  per object; a base's never shorter than the time to its last unit.
  Timeouts run per hop and never decide lateness; a sink's longer target latency cannot
  extend a publisher's timeout. A subscriber caps its stage, and its receipts, by
  subscription.
- **Receipts.** `wai_receipt_alias` (§4) is the `track_alias` a track's receipts sign:
  MOQT's Track Alias is chosen per session by each publisher on the path, so it is not
  that number past the first hop. Every alias in a catalog is an integer, compared as a
  number. `wai_receipts_track` names each media track's own **receipts track**
  (`wai-receipts`, at the media track's priority, no delivery timeout), whose group *g*
  carries the receipts of the media track's group *g*. No two tracks share an alias or a
  receipts track: a rendition of a layer, a clone among them, overrides both and has a
  chain of its own. Each receipt says which MOQT object it is about — `group_id` the
  MOQT Group ID, `object_id` the Object ID, the publisher's key and the track's alias —
  and a subscriber refuses one that does not (`moqt_mismatch`, a new jwp-receipts §3.3
  reason) before verifying it, holding nothing of it. A subscriber sets no delivery
  timeout on its subscription to a receipts track. A MOQT relay that carries receipts as
  opaque payload is not a jwp-receipts §4 relay; the subscriber verifies every receipt
  (jwp-receipts §4.1 amended).
- **Recovery.** A missing receipt is recovered by FETCH of its receipts-track group. A
  subscriber states its bound in counts: receipts held live for each chain (a time only
  beside it, never alone), held as history for each chain, and held across its chains.
  Past the live count the held receipts are refused (`parent_not_recovered`, a new
  jwp-receipts §3.3 reason) with their layers, the chain re-anchors at its next validly
  signed group, and the layers of the groups the anchor skips are refused. A subscriber
  that joins mid-stream declares a join and anchors at its first group. Each anchor
  leaves a gap, which a group linking to either end fills as history, in any order — the
  ascending order of a FETCH or a fill fetch stream included — and never as a fork;
  history waits under its own count. A group admitted before, offered again with the
  same bytes (MOQT can deliver one object twice, by a subscription and a fill), changes
  nothing (`accept again`), never `chain_break`, and its objects verify against it as
  against the head. jwp-receipts §3 step 3 defines holding,
  re-anchoring, joining and history, and says what an anchor does not prove; §6.1 says
  how anchors are reported and why the bounds hold across chains.
- **Other text.** staged-delivery §8, §9 and §13; jwp-receipts §4.4 and §5.9;
  http-delivery §10.2 and Appendix C. The registry lists `wai_staged`,
  `wai_receipt_alias`, `wai_receipts_track` and `wai-receipts` as wire identifiers.
- **Reference implementation.** `staged::moq` (feature `staged`): `StagedTrack`,
  `SetTrack::to_catalog_json`, `receipts_track_json`, `check_catalog` and
  `check_catalog_in`, `depends_for`, `live_object_rules_ok`, `priorities_ok`,
  `effective_timeout_ms`, `expired_at_hop`, `layer_timeout_ms`, `base_timeout_floor_ms`.
  `jwp_delivery::ReceiptChains` gains `with_hold`, `with_join`, `admit`, `expire`,
  `take_events` and `anchors`, and for MoQ `for_moq` (every count at most
  `MAX_HELD_GROUPS`) and `admit_on`; `HoldBound` states live, history and total counts
  (`from_json` refuses a statement without them); `ObjectReceipt::check_moqt`. Each
  group's signature is verified once, and a held group is offered again only when a
  group it links to is admitted (`verifications`, `reoffers`, `held_total` count them).
  Without recovery it behaves as before. `tests/staged_moq.rs` runs the corpus trace
  with signed receipts through `for_moq` under each subscriber's stated bound,
  per-track chains, dropped objects checked by inclusion and named by the relay's
  `delivery-gap` claims, and sealed `stage` claims.
- **Corpora.** `staged-conformance/moq/`: the set's MSF catalog; catalogs around §4.1's
  check, among them type cases, namespace inheritance under the catalog track's
  namespace, another and none known, MSF delta updates (a clone of a layer refused
  unless it overrides its alias and receipts track) and conversion to on demand; and a
  trace of the set through one relay to four subscribers, two layer objects dropped on
  timeout and one forwarded within its timeout and late, one subscriber's receipt lost,
  recovered by re-anchoring and the gap then fetched and filled as history, and one
  subscriber joining mid-stream. `rate_port.json`:
  the `rate` predictor beside the f64 interpolation of the layered-PCM ladder idea,
  equal wherever that form is exact, differing (pinned) where it rounds next to a half.
  `jwp-conformance/` gains recovery vectors — among them a gap fetched in ascending
  order after a re-anchor, a joiner's ascending fill under a live bound of 4, the
  history bound passed, the bound across chains, and a group delivered twice — appended
  after every existing one. `staged_verify.py` and `jwp_verify.py` check all of it independently;
  `chain_gap_differential.py` lets both chain verifiers recover.
- **joule-moq** (`joule-code/`): `staged` (the `wai_staged` member, the layer read from the
  envelope, the prototype `mrl_*` header compared and never obeyed), `ReceiptChains` per
  key and track, verifying each group's signature itself and knowing a group admitted
  before by its bytes, with the same recovery and bounds, and a `MockRelay` that forwards staged tracks on
  a caller-driven clock: per-hop timeouts, capping by subscription, one object to a live
  group, a receipts track per media track, `RelayStats` keyed by track, group and object,
  and send times past the clock refused. Its test `staged_tracks_drop_on_timeout_keep_receipts`
  runs the same trace.
- No existing envelope, receipt or claim byte changes. Two existing jwp-conformance
  vectors change their expected outcome: an exact replay of a group the chain admitted
  (`chain_head_predecessor_replayed`, and g1 in `chain_legacy_replay_loop`) is now
  `accept again`, not `chain_break`.

