diff --git a/quest/m1/README.md b/quest/m1/README.md index 5688f5a2000..f22fb51599d 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -50,6 +50,12 @@ explicit exception to the public model audit. Added 2026-10-16: refresh, a breaking `Demand` change, so it lands before the cut. +Added 2026-10-22 (maintainer, from an external API audit): small breaks +that would otherwise force another breaking release (the FFI encoder config, +FromStr, dead items, `SessionClosed`, unprefixed C++), the hang draft's +warmup rule for the shipping dual decoder, the binding migration guide fixes, +and the API-diff tooling. Public routes now wait on cluster routing. + Added 2026-10-20 (maintainer): the probe target, native probing, and the native rung disable moved up from the [m2 probe epic](/quest/m2/probe/README.md), so Rust, FFI, and OBS publishers recover a stopped rendition in this release; @@ -69,6 +75,7 @@ lite-07 stream type here. - [Rust request credit](/quest/m1/rs-request-credit.md) - a moq-net request fails at once without stream credit instead of waiting - [Generated C bindings](/quest/m1/c/README.md) - C generated from moq-ffi ships as `moq-c` 0.8.0 and replaces the hand-written libmoq - [Exact timestamps in every binding](/quest/m1/binding-timestamps.md) - bindings carry `{value, scale}` timestamps instead of lossy microseconds +- [Binding encoder config](/quest/m1/binding-encoder-config.md) - binding encoders take an exact rational framerate, and the publication track name moves to options only the producers take - [moq-transport passthrough](/quest/m1/ietf-extension/README.md) - object IDs, properties, end status, datagram objects, and unknown track properties survive moq-transport -> moq-relay (and a relay cluster) -> moq-transport; moxygen's moq-test sections 1-7 pass nightly, except cases that use a subgroup other than 0 - [Refresh](/quest/m1/refresh/README.md) - a join asks for a fresh group once the current group's deltas exceed a publisher-advertised byte ratio, at every hop; replaces `Demand::used`/`unused` and moq-json's `delta_ratio` - [Announcement takeover](/quest/m1/announce-takeover.md) - a new independent route explicitly continues instances served through its predecessor, with shared Rust/JS semantics and lite-07 wire @@ -117,6 +124,12 @@ lite-07 stream type here. - [moq-net internals](/quest/m1/net-internals.md) - no per-group session clones, no transport type parameter in lite and IETF internals, and the `Local` check measured - [Main is no slower than release](/quest/m1/release-perf/README.md) - every benchmark where main is slower than release is fixed or accepted with a reason, checked by a final paired run before the cut - [Subscribe update churn](/quest/m1/subscribe-update-churn.md) - a viewer sends SUBSCRIBE_UPDATE only on a meaningful change, measured after the publisher stall fix +- [Unprefixed C++ free functions](/quest/m1/cpp-free-function-prefix.md) - generated free functions drop `moq_`, and `moq.hpp` deletes its five pass-through wrappers before C++ first publishes +- [Error cleanups](/quest/m1/error-cleanups.md) - delete the unused `Error::SessionClosed`; raw transport failures already map to `Unroutable` +- [FromStr parsers](/quest/m1/from-str-trait.md) - hang, moq-auth, and moq-msf implement `FromStr` instead of inherent `from_str` +- [Drop dead items](/quest/m1/drop-dead-items.md) - the audit's uncalled public items and test helpers are removed +- [Warmup in the hang draft](/quest/m1/warmup-draft.md) - the draft lets a dual-decoder consumer present before `warmup`, matching the shipping `decode::Consumer` +- [API-diff check](/quest/m1/api-diff.md) - a recipe diffs every published package's API against its last release, run on PRs into release and against main before the cut - [Interop coverage](/quest/m1/interop-coverage/README.md) - the riskiest moq-transport features moq-net exposes get cross-implementation tests over the even drafts 14 to 22 - [Runner nightly](/quest/m1/interop-runner-nightly.md) - a nightly job runs the upstream interop runner against our relay and clients, so newly registered implementations with a published image are tested without a change here - [CI workflow audit](/quest/m1/audit-ci.md) - every workflow and job gives a signal someone sees, at a cost that signal justifies, or is fixed, moved, or deleted diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 47499d7e397..a223e5ad187 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -100,5 +100,5 @@ Rejected in planning: - [Claim request type](/quest/m1/public-model/claim-request-type.md) - removes `accept` from application requests; [the origin announces](/quest/m1/api-audit/announce-owner.md) narrows `Prepared` beside it - [Readers follow restarts](/quest/m1/sink-restart-follow.md) - moves readers onto the follow operation that [two broadcast resolutions](/quest/m1/api-audit/resolve.md) names -- [API-diff check on release PRs](/quest/m2/release/migrate-audit.md) - the backstop that catches breaks this epic misses +- [API-diff check on release PRs](/quest/m1/api-diff.md) - the backstop that catches breaks this epic misses - [Main is no slower than release](/quest/m1/release-perf/README.md) - the other release-readiness check diff --git a/quest/m1/api-audit/migrate.md b/quest/m1/api-audit/migrate.md index a59bc45b586..e35e053117b 100644 --- a/quest/m1/api-audit/migrate.md +++ b/quest/m1/api-audit/migrate.md @@ -20,6 +20,9 @@ Decided 2026-10-22 (API audit). Known gaps, verify each against - Bindings: merge the contradictory `finish` entries ("removed (#4152)" and "the new `finish` lets go (#6157)") into one: `finish()` now returns a linger handle; call `close()` for the old behavior. +- Bindings: `rs/moq-ffi/MIGRATE.md` says a raw track is timed by default, + but `MoqTrackInfo.timescale` defaults to `None` (untimed) while release + defaulted to microseconds; the net change is timed to untimed by default. - Bindings: `MoqAudioInit.track` and `MoqVideoInit.track` moved to `MoqMediaTarget::Named`, with no entry. - `cpp/moq/MIGRATE.md`: about 130 lines for a package never released; empty diff --git a/quest/m1/api-diff.md b/quest/m1/api-diff.md new file mode 100644 index 00000000000..33a947379f5 --- /dev/null +++ b/quest/m1/api-diff.md @@ -0,0 +1,44 @@ +# [L] API-diff check on release PRs + +## Goal + +A `just` recipe diffs each published package's public API against its last +published version, and every PR into `release` runs it. A backport fails on +any break; the breaking cut PR reports the diff for the +[migration audit](/quest/m2/release/migrate-audit.md). + +## Plan + +Decided 2026-10-16 (maintainer interview), split out of the migration audit +and moved to m1 on 2026-10-22 (maintainer, from an external API audit) so +the report runs locally against `main` while m1's breaks are still landing, +instead of first appearing at the cut: + +- Commit `!` markers undercount: a 2026-10-16 sample estimated 12-15 + unmarked breaks on top of 87 marked ones, a floor since signature-only + changes escape the heuristic. This check is the backstop; PRs into `main` + get no CI check. +- Runs on PRs into `release` only: backports must keep the published API + (CONTRIBUTING), and the cut PR gets a full audit. +- Backports fail on any detected break. The cut PR only reports: Rust + versions are bumped by release-plz after the cut lands, so the diff cannot + gate on a version bump there. +- One `just` recipe, three backends: cargo-semver-checks for Rust crates, + emitted `.d.ts` API reports for `@moq/*` packages, and a `moq.h` plus + UniFFI interface diff for the bindings. Prefer maintained tools over + hand-rolled diffing. Wire it into the workflow that runs on PRs into + `release`. +- Baseline is the last published artifact, not a git tag: `@moq/*` and + `moq-rs` publish without tags. Rust compares against crates.io + (cargo-semver-checks' default), JS against the npm tarball's declarations, + and the FFI interfaces against the published `moq-ffi` crate and the last + released C archive's `moq.h`. +- Coverage: the report covers Rust crates, `@moq/*` declarations, and the + generated FFI interfaces. A break only in a hand-written binding wrapper + (`py/moq-rs`, `kt/moq`, `dart/moq`, `go/wrapper`, `swift`, `cpp/moq`, + `cpp/obs`) or a binary's flags, env vars, or config keys escapes it; the + migration audit reads those by hand. Per-language wrapper checks are out + of scope until a missed break shows they are needed. + +Run it against `main` when it lands and put the unmarked breaks it finds in +the PR description, so their `MIGRATE.md` entries land before the cut. diff --git a/quest/m1/binding-encoder-config.md b/quest/m1/binding-encoder-config.md new file mode 100644 index 00000000000..ac8c22b3a9d --- /dev/null +++ b/quest/m1/binding-encoder-config.md @@ -0,0 +1,41 @@ +# [M] Binding encoders take exact rates and codec-only settings + +## Goal + +A binding caller can encode 30000/1001 video exactly, and a codec-only +encoder accepts nothing it ignores. `MoqVideoEncoderInput.framerate` becomes +a rational rate instead of a `u32` the encoders turn into `Rate(n, 1)`, and +the publication `track` name leaves `MoqVideoEncoderOutput` and +`MoqAudioEncoderOutput` for options only `MoqVideoProducer::new` and +`MoqAudioProducer::new` take. + +## Plan + +Decided 2026-10-22 (maintainer, from an external API audit): one quest for +both, since they break the same records and every wrapper, so callers +migrate once with the cut. + +- Rate: `rs/moq-ffi/src/video.rs` builds `moq_video::Rate::new(framerate, 1)` + in `video_encoder_config` (the codec-only `MoqVideoEncoder`) and again + inline in `MoqVideoProducer::new`; `moq_video::Rate` already holds a + numerator and denominator. Expose it as a numerator/denominator record and + build the rate once. Audio's integer `sample_rate` is exact and stays. +- Publication options: `track` is the only publish-only field today. + `video_encoder_config` and `audio_settings` never read it. Nest it in a + publication options record taken by the producers, beside the existing + `catalog` and `bandwidth` arguments, rather than adding a fourth argument. + Keep the `audio`/`video` namespaces. Propose the record and field names + with a recommendation in the PR before shipping them. +- Wrappers alias the uniffi records, so update `py/moq-rs`, `kt/moq`, + `swift`, `dart/moq`, `go/wrapper`, `cpp/moq`, their tests and `doc/lib/*` + per the Cross-Package Sync table. The generated C picks the records up; + the hand-written `rs/moq-c` has no raw encoder, so it is untouched. +- [OBS moq-video adapter](/quest/m2/obs-moq-video/adapter.md) later adds a + preset to the codec-only config; this shape is the one it extends. + +Public API: breaking in moq-ffi and every binding wrapper, each with a +`MIGRATE.md` entry. Wire: none. + +## Related + +- [Exact timestamps in every binding](/quest/m1/binding-timestamps.md) - the other exact-time break to the same bindings in this cut diff --git a/quest/m2/cpp-free-function-prefix.md b/quest/m1/cpp-free-function-prefix.md similarity index 82% rename from quest/m2/cpp-free-function-prefix.md rename to quest/m1/cpp-free-function-prefix.md index 4ad5746bf2b..6bfc28bb2d9 100644 --- a/quest/m2/cpp-free-function-prefix.md +++ b/quest/m1/cpp-free-function-prefix.md @@ -12,7 +12,10 @@ types already drop `Moq` (`strip_type_prefix` in `cpp/moq/uniffi.toml`), so ## Plan Decided with the maintainer on 2026-10-17: the goal as drafted, the follow-up -#5727 deferred. +#5727 deferred. Moved to m1 on 2026-10-22 (maintainer, from an external API +audit): the C++ package is unpublished, so stripping the prefix before its +first publication avoids shipping both the `moq::moq_*` names and their +wrappers. The option belongs in the uniffi-bindgen-cpp fork, released and pinned the way #5727 pinned `strip_type_prefix` (every place that PR moved the pin). diff --git a/quest/m2/drop-dead-items.md b/quest/m1/drop-dead-items.md similarity index 78% rename from quest/m2/drop-dead-items.md rename to quest/m1/drop-dead-items.md index ab6128a8b96..7d704a0fd7d 100644 --- a/quest/m2/drop-dead-items.md +++ b/quest/m1/drop-dead-items.md @@ -11,7 +11,10 @@ since [#5678](https://github.com/moq-dev/moq/pull/5678). ## Plan From [moq-dev/moq#6166](https://github.com/moq-dev/moq/pull/6166). Decided -2026-10-21 with the maintainer: m2. Verify each still has no caller across +2026-10-21 with the maintainer: m2. +Moved to m1 on 2026-10-22 (maintainer, from an external API audit): the +cut is breaking anyway, so this break ships with it instead of forcing +another breaking release later. Verify each still has no caller across the repo, bindings included, before removing it; keep any that gained one. The test helper lives in `model/track.rs`, which many open PRs churn, so expect merge conflicts there. diff --git a/quest/m2/error-cleanups.md b/quest/m1/error-cleanups.md similarity index 78% rename from quest/m2/error-cleanups.md rename to quest/m1/error-cleanups.md index 853c8ec8e8b..71e13ca42a0 100644 --- a/quest/m2/error-cleanups.md +++ b/quest/m1/error-cleanups.md @@ -7,6 +7,10 @@ deleted. ## Plan +Moved to m1 on 2026-10-22 (maintainer, from an external API audit): the +cut is breaking anyway, so this break ships with it instead of forcing +another breaking release later. + Decided with the maintainer on 2026-10-17: the goal as drafted, from the follow-ups of [#5729](https://github.com/moq-dev/moq/pull/5729), widened the same day to Rust after review found Rust sends `Internal` too. @@ -24,3 +28,7 @@ decided 2026-10-22 (maintainer): keep the removal and construct `StreamError::Session(SessionError::Cancel)` there directly. Removing a public variant is a breaking Rust change: add the `rs/moq-net/MIGRATE.md` entry. Wire: none. + +## Related + +- [One clean close](/quest/m1/api-audit/clean-close.md) - collapses the clean session-close spellings this removal touches diff --git a/quest/m2/from-str-trait.md b/quest/m1/from-str-trait.md similarity index 74% rename from quest/m2/from-str-trait.md rename to quest/m1/from-str-trait.md index 379ce8264ee..1047831fb09 100644 --- a/quest/m2/from-str-trait.md +++ b/quest/m1/from-str-trait.md @@ -11,7 +11,10 @@ inherent `pub fn from_str`, so `str::parse` works and the From the lint audit in [moq-dev/moq#6159](https://github.com/moq-dev/moq/pull/6159), which kept the inherent methods because the switch breaks callers. Decided 2026-10-21 with -the maintainer: do it in m2. Today's inherent methods are in +the maintainer: do it in m2. +Moved to m1 on 2026-10-22 (maintainer, from an external API audit): the +cut is breaking anyway, so this break ships with it instead of forcing +another breaking release later. Today's inherent methods are in `rs/hang/src/catalog/root.rs`, `rs/moq-auth/src/key.rs`, `rs/moq-auth/src/set.rs`, and `rs/moq-msf/src/lib.rs`; check for more. Remove the inherent method rather than keeping it beside the trait. diff --git a/quest/m1/public-model/README.md b/quest/m1/public-model/README.md index 4e28d52ecf6..2d3df7a7f04 100644 --- a/quest/m1/public-model/README.md +++ b/quest/m1/public-model/README.md @@ -64,7 +64,7 @@ Inventory on `main` at b4eab0db1: only inside `model/`. - The 106 split into ingest (landed in [moq-dev/moq#6111](https://github.com/moq-dev/moq/pull/6111)), [serving](/quest/m1/public-model/serve.md), - [routes and answering](/quest/m1/public-model/answer.md), and the + [answering](/quest/m1/public-model/answer.md) (with [routes](/quest/m1/public-model/routes.md) split out later), and the passthrough carve-out (listed in each child where it shares a type). - The 16 test-only items (`group::Info::produce`, `group::Producer::is_aborted` and `is_finished`, `track::Info::timed`, `track::Producer::new`, @@ -104,7 +104,8 @@ Work owned by this README, after the children land: - [Claim request type](/quest/m1/public-model/claim-request-type.md) - accepting an application claim's request fails to compile instead of panicking - [Public serving](/quest/m1/public-model/serve.md) - a custom publisher reads a track's start, cursor, and group expiry for its wire through public calls -- [Public routes and answering](/quest/m1/public-model/answer.md) - a custom relay ingests routes, answers broadcast and track requests on demand, and advertises without echoing a peer's routes back +- [Public answering](/quest/m1/public-model/answer.md) - a custom relay answers broadcast and track requests on demand and closes them once unused +- [Public routes](/quest/m1/public-model/routes.md) - a custom relay ingests routes under a peer's identity and advertises without echoing them back, once cluster routing lands - [Public model after ranges, handover, and the JS tail](/quest/m1/public-model/deferred.md) - fetch and track-info answering, the cache peeks, JS `fork`/`mirror`, and JS `endTrack`, once the quests that reshape them land ## Related diff --git a/quest/m1/public-model/answer.md b/quest/m1/public-model/answer.md index fdafa2a343f..6e702b6f6fd 100644 --- a/quest/m1/public-model/answer.md +++ b/quest/m1/public-model/answer.md @@ -1,12 +1,11 @@ -# [L] Public routes and answering +# [L] Public answering ## Goal A custom relay or session does what the built-in ones do with an origin, -through public moq-net and @moq/net calls only: ingest a peer's routes under -the peer's identity, answer broadcast and track requests on demand and close -them once unused without racing a returning reader, and advertise routes to -a peer without echoing the peer's own routes back. +through public moq-net and @moq/net calls only: answer broadcast and track +requests on demand and close them once unused without racing a returning +reader. ## Plan @@ -17,20 +16,6 @@ recommendations below are the planning quest's, and the ones marked Rust items (`rs/moq-net/src/model/`), as they stood at b4eab0db1: -- `origin::Hop::from_wire` (IETF cluster): public as the decode-side - constructor that accepts 0 (`Hop::UNKNOWN`), beside `Hop::new`. -- `origin::Cost::charged` (both subscribers, IETF cluster): public. - `Cost::UNKNOWN` goes; it equals `Cost::default()`. -- **Announcing identity** (`origin::Route::via`, `Route::source`, - `Route::with_via`; both subscribers, server). **Decision**: - - ✅ Replace. The identity moves onto the origin handle a session - announces through, as `origin::Producer::via(hop)` beside the public - `Producer::peer` (the name `via` stays), so every route it ingests - carries it and a `Route` can never forward it. `Route.via` and - `Route.source` stay private fields the handle sets. - - A public `Route::with_via`, which a caller could forward by mistake. -- `origin::Consumer::excluding` (both publishers, client): public; - split-horizon toward a peer, the counterpart of the identity above. - `origin::Producer::create_source` (both subscribers): public under a role name (for example `answer_source`). It mints a broadcast with the origin's cache policy and stats that answers a `Dynamic` request without entering @@ -62,18 +47,6 @@ Rust items (`rs/moq-net/src/model/`), as they stood at b4eab0db1: - Keep them private. - `origin::Dynamic::withdrawn` (lite subscriber): public, as the explicit peer withdrawal that differs from a session simply dropping its handle. -- **Peer-route escape hatches** (`origin::Producer::dynamic_route`, - `origin::Dynamic::update_route`, `origin::Request::accept`; JS - `Origin.Producer.dynamicRoute`, `Dynamic.updateRoute`, `Request.accept`). - Settled as the interim in [#5678](https://github.com/moq-dev/moq/pull/5678): - an application claim (`dynamic(prefix)`) carries no route and answers - through `Request::prepare` then `Prepared::publish`, and these stay - `#[doc(hidden)] pub` / `@internal` for the sessions, tests, and benches that - model a peer's route (a covering claim with hops, cost, or an epoch, - answered with a broadcast the session already routes). Make them private - (`pub(crate)`, and private in JS) once the identity handle above and the - answer source give a peer's route a public shape, and move the - out-of-crate tests and benches onto it. - `origin::interest_prefixes` (both subscribers): remove the need through Rust lazy discovery ([moq-dev/moq#6165](https://github.com/moq-dev/moq/pull/6165)), which gives Rust the origin's watched interests the way JS has them; make that public in @@ -100,11 +73,6 @@ Rust items (`rs/moq-net/src/model/`), as they stood at b4eab0db1: JS items (`js/net/src/`): -- `wire.ts` `OriginProducer.receive` (`connection/forward.ts`): mirror the - identity decision. A received route is a public `Origin.Producer.dynamic` - on a handle marked with the peer's identity, so `receive` goes and - `Origin.Request.accept` stops special-casing session-delivered broadcasts - (`Broadcast.fromSession`). - `wire.ts` `OriginProducer.requests`, `changed`, `blind`, `answer`, `routes`, `origin.ts` `RequestSlot`, `internal.ts` `coveringPrefixes` (the blind-answer loop in `connection/forward.ts`). **Decision**: @@ -147,15 +115,6 @@ JS items (`js/net/src/`): is public with Lazy discovery; `attach(discovery)` goes with the `discovery` option Lazy discovery removes; `accepts` is a check on the public `allowed()` patterns. -- `wire.ts` `OriginConsumer.advertised`, `local`, `demand`, with - `Advertised`, `Advertisements`, `sameInstance`, and `internal.ts` - `presented`, `hiddenBelow`, `scopeHead`, `scopeCaptures`, `scopeOverlaps` - (both publishers and subscribers): replace with the Rust shape. A - publisher advertises from the public `announced()` stream of an - `excluding(peer)` consumer and resolves with the public request taking an - epoch, as Rust does. The helpers stay private to `origin.ts`. Check that - the public announce event carries what `sameInstance` and a scoped - route's claim need; if it does not, that is track metadata and goes public. - `wire.ts` `Broadcast.requested` (with its `served` flag) and `overrideBroadcastWire` (both subscribers). Part of the JS answering decision above: ✅ public, mirroring Rust's @@ -167,16 +126,6 @@ JS items (`js/net/src/`): moved to [the deferred quest](/quest/m1/public-model/deferred.md), which waits on Model ranges. - `wire.ts` `OriginConsumer.broadcasts`: registered and never read; delete. -- `hop.ts` `HopSchema`, `randomHop`, `UNKNOWN_HOP`, `MAX_HOPS`, - `Cost.zero`, `Route.default`, `Route.normalize`, `routesEqual`: public - values under `Origin`, mirroring Rust's `Hop::new`, `Hop::UNKNOWN`, - `Route::default` and `PartialEq`. `Hop` is branded, so a third party - cannot build `Route.hops` without a cast today. Today `Route.default` is - one shared mutable object (`{ hops: [], cost: 0n }`), so a caller's - `hops.push` would pollute every later route. Publish it as a factory - returning a fresh route (`Route.empty()`, like Rust's `Route::default()`), - and move the internal readers onto it, rather than exporting the shared - object. - `announced.ts` `Announce.Producer`: remove the need; a session's route ingestion goes through the origin. - `wireOf`, `registerWire`, `trackOf`, `registerTrackConsumer`: stay @@ -189,4 +138,5 @@ cover the behavior. ## Related +- [Public routes](/quest/m1/public-model/routes.md) - route ingest and advertising, split out of this quest to follow cluster routing - [Public model after ranges, handover, and the JS tail](/quest/m1/public-model/deferred.md) - the track info and fetch answers, split out of this quest diff --git a/quest/m1/public-model/claim-request-type.md b/quest/m1/public-model/claim-request-type.md index cb9e1bdf30b..91c37ff4a85 100644 --- a/quest/m1/public-model/claim-request-type.md +++ b/quest/m1/public-model/claim-request-type.md @@ -8,7 +8,7 @@ Rust and to typecheck in TypeScript, instead of panicking (Rust) or throwing only `prepare` and `reject`; the session-forwarded claim from `dynamic_route` / `dynamicRoute` keeps `accept`. Behavior is otherwise unchanged: the escape hatches stay hidden until -[Public routes and answering](/quest/m1/public-model/answer.md) gives them a +[Public routes](/quest/m1/public-model/routes.md) gives them a public shape. ## Plan @@ -38,4 +38,4 @@ change. ## Related -- [Public routes and answering](/quest/m1/public-model/answer.md) - later removes the hidden peer-claim API this quest narrows +- [Public routes](/quest/m1/public-model/routes.md) - later removes the hidden peer-claim API this quest narrows diff --git a/quest/m1/public-model/deferred.md b/quest/m1/public-model/deferred.md index 97b9c5a6ae4..ab1cc6bce6b 100644 --- a/quest/m1/public-model/deferred.md +++ b/quest/m1/public-model/deferred.md @@ -48,4 +48,4 @@ built-in sessions moved onto the public calls. - [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - replaces per-group fetch requests, the peeks, and `fork` - [JS ranges](/quest/m1/subscribe-ranges/js.md) - the JS `Track.Request`/`Track.Dynamic` range requests that answer track info and fetches -- [Public routes and answering](/quest/m1/public-model/answer.md) - the public `Broadcast.requested` handout this completes +- [Public answering](/quest/m1/public-model/answer.md) - the public `Broadcast.requested` handout this completes diff --git a/quest/m1/public-model/routes.md b/quest/m1/public-model/routes.md new file mode 100644 index 00000000000..0b95b164003 --- /dev/null +++ b/quest/m1/public-model/routes.md @@ -0,0 +1,88 @@ +# [M] Public routes + +## Goal + +A custom relay or session does what the built-in ones do with routes, +through public moq-net and @moq/net calls only: ingest a peer's routes under +the peer's identity, and advertise routes to a peer without echoing the +peer's own routes back. + +## Plan + +Split out of [Public answering](/quest/m1/public-model/answer.md) on +2026-10-22 (maintainer, from an external API audit): every item below hangs +off the `Hop`/`Cost` route model that [Routes and +announces](/quest/m1/cluster-routing/routes.md) replaces with node ids, +seqnos, and a down-only metric in this same release. Publishing them first +would freeze machinery already scheduled for removal, so this waits for that +quest and then reruns the inventory against the route layer it lands. + +The items and decisions below are as they stood at b4eab0db1. Keep each +decision's intent: the announcing identity lives on the origin handle and a +`Route` can never forward it, a publisher advertises split-horizon from a +consumer that excludes the peer, and the peer-route escape hatches go +private once a peer's route has a public shape. Expose the roles that survive +the route layer under their new types, and drop items whose need it removes +(for example `Hop::from_wire` and `Cost::charged` if `Hops` and saturating +`Cost` are gone). Rules, carve-outs, and the inventory method are in the +[epic](/quest/m1/public-model/README.md). + +- `origin::Hop::from_wire` (IETF cluster): public as the decode-side + constructor that accepts 0 (`Hop::UNKNOWN`), beside `Hop::new`. +- `origin::Cost::charged` (both subscribers, IETF cluster): public. + `Cost::UNKNOWN` goes; it equals `Cost::default()`. +- **Announcing identity** (`origin::Route::via`, `Route::source`, + `Route::with_via`; both subscribers, server). **Decision**: + - ✅ Replace. The identity moves onto the origin handle a session + announces through, as `origin::Producer::via(hop)` beside the public + `Producer::peer` (the name `via` stays), so every route it ingests + carries it and a `Route` can never forward it. `Route.via` and + `Route.source` stay private fields the handle sets. + - A public `Route::with_via`, which a caller could forward by mistake. +- `origin::Consumer::excluding` (both publishers, client): public; + split-horizon toward a peer, the counterpart of the identity above. +- **Peer-route escape hatches** (`origin::Producer::dynamic_route`, + `origin::Dynamic::update_route`, `origin::Request::accept`; JS + `Origin.Producer.dynamicRoute`, `Dynamic.updateRoute`, `Request.accept`). + Settled as the interim in [#5678](https://github.com/moq-dev/moq/pull/5678): + an application claim (`dynamic(prefix)`) carries no route and answers + through `Request::prepare` then `Prepared::publish`, and these stay + `#[doc(hidden)] pub` / `@internal` for the sessions, tests, and benches that + model a peer's route (a covering claim with hops, cost, or an epoch, + answered with a broadcast the session already routes). Make them private + (`pub(crate)`, and private in JS) once the identity handle above and the + answer source give a peer's route a public shape, and move the + out-of-crate tests and benches onto it. +- `wire.ts` `OriginProducer.receive` (`connection/forward.ts`): mirror the + identity decision. A received route is a public `Origin.Producer.dynamic` + on a handle marked with the peer's identity, so `receive` goes and + `Origin.Request.accept` stops special-casing session-delivered broadcasts + (`Broadcast.fromSession`). +- `wire.ts` `OriginConsumer.advertised`, `local`, `demand`, with + `Advertised`, `Advertisements`, `sameInstance`, and `internal.ts` + `presented`, `hiddenBelow`, `scopeHead`, `scopeCaptures`, `scopeOverlaps` + (both publishers and subscribers): replace with the Rust shape. A + publisher advertises from the public `announced()` stream of an + `excluding(peer)` consumer and resolves with the public request taking an + epoch, as Rust does. The helpers stay private to `origin.ts`. Check that + the public announce event carries what `sameInstance` and a scoped + route's claim need; if it does not, that is track metadata and goes public. +- `hop.ts` `HopSchema`, `randomHop`, `UNKNOWN_HOP`, `MAX_HOPS`, + `Cost.zero`, `Route.default`, `Route.normalize`, `routesEqual`: public + values under `Origin`, mirroring Rust's `Hop::new`, `Hop::UNKNOWN`, + `Route::default` and `PartialEq`. `Hop` is branded, so a third party + cannot build `Route.hops` without a cast today. Today `Route.default` is + one shared mutable object (`{ hops: [], cost: 0n }`), so a caller's + `hops.push` would pollute every later route. Publish it as a factory + returning a fresh route (`Route.empty()`, like Rust's `Route::default()`), + and move the internal readers onto it, rather than exporting the shared + object. + +Tests: each public call gets a mock-time test in its language, and the +built-in sessions move onto the public calls, so their existing suites +cover the behavior. + +## Required + +- [Routes and announces](/quest/m1/cluster-routing/routes.md) - replaces the hop and cost model these items expose +- [Public answering](/quest/m1/public-model/answer.md) - the answer source the escape hatches' replacement builds on diff --git a/quest/m1/time/wall.md b/quest/m1/time/wall.md index d3feec906e8..8e0ba847691 100644 --- a/quest/m1/time/wall.md +++ b/quest/m1/time/wall.md @@ -24,9 +24,20 @@ The remaining public `SystemTime`s, as of - moq-cli and moq-bench: private wall times, which can move freely. `Wall` covers 1970 to 2554, so a catalog wall past that range is an error, as -it already is on Windows. A third-party API on `SystemTime`, such as str0m's -sender reports in moq-rtc, converts at its boundary with `From` and -`TryFrom`. +it already is on Windows. A third-party API on `SystemTime`, such as +str0m's sender reports in moq-rtc, converts at its boundary with +`From` and `TryFrom`. + +Expiries saturate instead (decided 2026-10-22, maintainer, from an external +API audit of [#6298](https://github.com/moq-dev/moq/pull/6298)): a +certificate `not_after` or an auth expiry past 2554 becomes the latest +representable `Wall`, and one before 1970 becomes `Wall::UNIX_EPOCH`, so +`tls::PeerIdentity::expiry` and the auth claims stay infallible and rcgen's +default year-4096 certificates keep working. No representable `now` can compare differently against the +saturated value, so this loses nothing; refusing them would turn a +representation limit into a validity rule for valid identities. Presentation +walls (catalogs, timelines, HLS) still refuse, since their value is shown or +used for arithmetic, not only compared. Public API: breaking in hang, moq-mux, moq-hls, moq-quic, moq-relay, and moq-tokio, each with a `MIGRATE.md` entry. Wire: none. diff --git a/quest/m1/warmup-draft.md b/quest/m1/warmup-draft.md new file mode 100644 index 00000000000..cf5c3eb9e55 --- /dev/null +++ b/quest/m1/warmup-draft.md @@ -0,0 +1,29 @@ +# [XS] The hang draft lets a dual decoder present before warmup + +## Goal + +`drafts/draft-lcurley-moq-hang.md` matches the shipping `decode::Consumer`: +a consumer running a parallel max-delay decoder MAY present the min-delay +decoder's pictures before the group start plus `warmup`, replacing them once +the max-delay decoder catches up. Today the draft says a consumer MUST NOT +present frames stamped before that point, while +`rs/moq-video/src/decode/consumer.rs` deliberately shows refresh stripes +after a join or skip. + +## Plan + +The rule was decided 2026-10-22 (maintainer) in +[Plan: recovery boundaries](/quest/m2/intra-refresh/plan-boundary.md), keeping +the dual decoder merged in [#5842](https://github.com/moq-dev/moq/pull/5842). +Split out to m1 the same day (maintainer, from an external API audit): the +decoder ships in this cut, so its contract should too. A consumer without a +second decoder keeps the MUST NOT, which is what the `consumer-warmup` +transcoder relies on. Say what a caller can rely on: pictures before warmup +may show stripes and are replaced, never that they are clean. + +Publication ordering for a later, longer sweep stays in the plan quest. +js/watch runs a single decoder and today ignores video `warmup`; the +[dual decoder](/quest/m2/intra-refresh/dual-decoder.md) owns that. + +Public API: none. Wire: the hang draft's presentation rule; validate with +`just drafts check`. diff --git a/quest/m2/README.md b/quest/m2/README.md index 74762130db0..23d8c0c388e 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -42,7 +42,6 @@ ranked just below deterministic TS legs, which it requires. - [Vulkan Video encode on AMD](/quest/m2/vulkan-encode.md) - H.264 and H.265 from an external Vulkan image on RADV, hand-rolled on ash, proven on an RX 9070 - [VA-API encodes an external Vulkan image](/quest/m2/vaapi-vulkan-import.md) - an explicit-sync DMA-BUF reaches Intel's encoder, proven on Arrow Lake - [C++ FFI reuse](/quest/m2/cpp-ffi-reuse.md) - C++ packages link the shared default-feature FFI build once it is proven compatible -- [Unprefixed C++ free functions](/quest/m2/cpp-free-function-prefix.md) - generated free functions drop `moq_`, and `moq.hpp` deletes its five pass-through wrappers - [Native compression flush](/quest/m2/native-compression-flush.md) - report the per-frame nonfinal flush requirement in existing browser discussions - [Native synchronous codecs](/quest/m2/native-compression-sync.md) - report the stateful small-frame synchronous API requirement @@ -199,9 +198,6 @@ ranked just below deterministic TS legs, which it requires. - [IETF request order](/quest/m2/ietf-request-order.md) - d14/15 requests take their ID after opening, legacy `control.sending()` races are gone, and SUBSCRIBE_NAMESPACE never closes over its own close - [JS IETF request streams](/quest/m2/js-request-stream.md) - `RequestStream` owns deadlines and cancellation, and TRACK trailing bytes close the session like Rust - [Join after a quick finish](/quest/m2/ietf-join-after-finish.md) - a pre-draft-20 joining FETCH that arrives after its subscription finished is still answered, within a bound the peer already negotiates -- [Error cleanups](/quest/m2/error-cleanups.md) - delete the unused `Error::SessionClosed`; raw transport failures already map to `Unroutable` -- [FromStr parsers](/quest/m2/from-str-trait.md) - hang, moq-auth, and moq-msf implement `FromStr` instead of inherent `from_str` -- [Drop dead items](/quest/m2/drop-dead-items.md) - the audit's uncalled public items and test helpers are removed - [ashpd uses maintained D-Bus crates](/quest/m2/ashpd-maintained.md) - wait for a stable zbus 6 port, then remove the portal dependency exceptions - [Askama uses a maintained TOML parser](/quest/m2/askama-maintained.md) - wait for a stable parser migration, then remove the generator dependency exception - [Dependency and alternatives audit](/quest/m2/audit-deps.md) - dependencies are current, deduplicated, needed, and the best maintained option diff --git a/quest/m2/intra-refresh/README.md b/quest/m2/intra-refresh/README.md index 08ceca2a431..04175280121 100644 --- a/quest/m2/intra-refresh/README.md +++ b/quest/m2/intra-refresh/README.md @@ -52,7 +52,7 @@ make these presentation guarantees true even when sweep lengths change. - [Join earlier group](/quest/m2/intra-refresh/join-earlier-group.md) - max-delay decoders, native and browser, start a group before the live edge even when the live edge arrives first - [Dual decoder](/quest/m2/intra-refresh/dual-decoder.md) - js/watch runs a min-delay and a max-delay decoder and shows the max-delay picture whenever it has caught up - [Play dual decoder](/quest/m2/intra-refresh/play-dual-decoder.md) - `moq play`'s paced pipeline reuses that pair -- [Plan: recovery boundaries](/quest/m2/intra-refresh/plan-boundary.md) - settle publication ordering and amend the draft so a dual-decoder player may present before `warmup` ends +- [Plan: recovery boundaries](/quest/m2/intra-refresh/plan-boundary.md) - settle publication ordering for a later, longer sweep - [Consumer warmup](/quest/m2/intra-refresh/consumer-warmup.md) - the transcoder decodes the groups covering `warmup` as history before each group, except at a true IDR - [H.264 import](/quest/m2/intra-refresh/h264-import.md) - the splitter keeps `recovery_frame_cnt` and import publishes `warmup` from it - [H.265 import](/quest/m2/intra-refresh/h265-import.md) - the splitter reads the recovery-point SEI so an HEVC intra-refresh stream forms groups and publishes `warmup` diff --git a/quest/m2/intra-refresh/plan-boundary.md b/quest/m2/intra-refresh/plan-boundary.md index d3f8643c621..67711df357f 100644 --- a/quest/m2/intra-refresh/plan-boundary.md +++ b/quest/m2/intra-refresh/plan-boundary.md @@ -17,11 +17,12 @@ recall pictures a subscriber has already displayed using the old value. Decided 2026-10-22 (maintainer): precision is settled; the hang draft rounds a fractional-millisecond `warmup` up -([#5212](https://github.com/moq-dev/moq/pull/5212)). This quest amends the -draft's `warmup` rule so a consumer running a parallel max-delay decoder MAY -present min-delay pictures before the group start plus `warmup`, which keeps -the merged dual decoder in `decode::Consumer` -([#5842](https://github.com/moq-dev/moq/pull/5842)). Publication ordering for +([#5212](https://github.com/moq-dev/moq/pull/5212)). The draft's `warmup` rule +is amended so a consumer running a parallel max-delay decoder MAY present +min-delay pictures before the group start plus `warmup`, which keeps the +merged dual decoder in `decode::Consumer` +([#5842](https://github.com/moq-dev/moq/pull/5842)); that amendment moved to +[its own m1 quest](/quest/m1/warmup-draft.md) so it ships with the decoder. Publication ordering for a later, longer sweep remains to settle. Ask the maintainer to choose between an exact per-group boundary and a safe diff --git a/quest/m2/release/README.md b/quest/m2/release/README.md index b7a3bb4a310..85b49e91ffa 100644 --- a/quest/m2/release/README.md +++ b/quest/m2/release/README.md @@ -27,7 +27,7 @@ production stability proven merely because upstream artifacts published. ## Required -- [API-diff check](/quest/m2/release/migrate-audit.md) - PRs into release diff the public API; backports refuse breaks, the cut confirms MIGRATE.md coverage +- [Migration audit](/quest/m2/release/migrate-audit.md) - the cut confirms every break has a MIGRATE.md entry stating the net published-to-final change - [API shape review](/quest/m2/release/api-review.md) - every new or changed public API reviewed for a better shape before the cut - [Feature review](/quest/m2/release/feature-review.md) - every major work line since the last release judged end to end before the cut - [Cut and publish](/quest/m2/release/cut.md) - independently executable after m1; release-triggered publication diff --git a/quest/m2/release/api-review.md b/quest/m2/release/api-review.md index e39e1897cef..a85ac07bc26 100644 --- a/quest/m2/release/api-review.md +++ b/quest/m2/release/api-review.md @@ -18,7 +18,7 @@ Only `/quest-delete` ends the recurrence. Starts once m1 lands, so the APIs it reviews are the ones that ship; the cut waits for it. API shape is the maintainer's call (decided 2026-10-19), so this run changes no code. Inventory the diff against the last published version -by running the [API-diff check](/quest/m2/release/migrate-audit.md)'s recipe +by running the [API-diff check](/quest/m1/api-diff.md)'s recipe locally against `main`, since its CI report only appears on the cut PR, and hand diff the surfaces it cannot see. For each export that is awkward, judge it against AGENTS.md's Public API rules: names mirrored across languages, no @@ -31,4 +31,4 @@ moq-net exposes; this review judges the shape of what ships. ## Required - [Breaking release readiness](/quest/m1/README.md) - the release's code is final before it is reviewed -- [API-diff check](/quest/m2/release/migrate-audit.md) - the recipe that inventories every public API change +- [API-diff check](/quest/m1/api-diff.md) - the recipe that inventories every public API change diff --git a/quest/m2/release/cut.md b/quest/m2/release/cut.md index a412b1b9a40..dac36696dfc 100644 --- a/quest/m2/release/cut.md +++ b/quest/m2/release/cut.md @@ -33,6 +33,6 @@ the new `/quest/m1/README.md`, and the cut also requires both reviews. Only ## Required - [Breaking release readiness](/quest/m1/README.md) - code, protocol and package proofs -- [API-diff check](/quest/m2/release/migrate-audit.md) - every break on the cut PR has its MIGRATE.md entry +- [Migration audit](/quest/m2/release/migrate-audit.md) - every break on the cut PR has its MIGRATE.md entry - [API shape review](/quest/m2/release/api-review.md) - every new or changed public API was reviewed for a better shape, and approved changes landed - [Feature review](/quest/m2/release/feature-review.md) - every major work line since the last release was judged end to end, and its follow-ups landed diff --git a/quest/m2/release/migrate-audit.md b/quest/m2/release/migrate-audit.md index 3c0e33cf0bc..f908a6e6809 100644 --- a/quest/m2/release/migrate-audit.md +++ b/quest/m2/release/migrate-audit.md @@ -1,54 +1,40 @@ -# [L] API-diff check on release PRs +# [M] Migration audit of the breaking cut ## Goal -Every PR into `release` runs a check that diffs each published package's -public API against its last published version. A backport fails on any break; on -the breaking cut PR the diff is reported for the auditor, who confirms every -break, including those in surfaces the diff cannot see, has its `MIGRATE.md` -entry before the cut merges. +Before the breaking cut merges, every break it ships, including those in +surfaces the [API-diff check](/quest/m1/api-diff.md) cannot see, has its +`MIGRATE.md` entry, and each entry states the net change from the last +published version. ## Plan -Decided 2026-10-16 (maintainer interview): +Decided 2026-10-16 (maintainer interview); the diff tooling moved to its own +m1 quest on 2026-10-22. -- Commit `!` markers undercount: a 2026-10-16 sample estimated 12-15 - unmarked breaks on top of 87 marked ones, a floor since signature-only - changes escape the heuristic. This check is the backstop; PRs into `main` - get no CI check. -- Runs on PRs into `release` only: backports must keep the published API - (CONTRIBUTING), and the cut PR gets a full audit. -- Backports fail on any detected break. The cut PR only reports: Rust - versions are bumped by release-plz after the cut lands, so the diff cannot - gate on a version bump there. -- One `just` recipe, three backends: cargo-semver-checks for Rust crates, - emitted `.d.ts` API reports for `@moq/*` packages, and a `moq.h` plus - UniFFI interface diff for the bindings. Prefer maintained tools over - hand-rolled diffing. Wire it into the workflow that runs on PRs into - `release`, and add the audit step to CONTRIBUTING.md's Releases section. -- Baseline is the last published artifact, not a git tag: `@moq/*` and - `moq-rs` publish without tags. Rust compares against crates.io - (cargo-semver-checks' default), JS against the npm tarball's declarations, - and the FFI interfaces against the published `moq-ffi` crate and the last - released C archive's `moq.h`. -- Coverage: the report covers Rust crates, `@moq/*` declarations, and the - generated FFI interfaces. A break only in a hand-written binding wrapper - (`py/moq-rs`, `kt/moq`, `dart/moq`, `go/wrapper`, `swift`, `cpp/moq`, - `cpp/obs`) or a binary's flags, env vars, or config keys escapes it, so the - cut's audit also reads the diff of those paths by hand. Per-language wrapper - checks are out of scope until a missed break shows they are needed. -- For this cut, fill every gap the report and the manual pass show in the - matching `MIGRATE.md`. Known gaps: breaking releases cut after the +- Run the API-diff report on the cut PR and read the diff of the + hand-written binding wrappers (`py/moq-rs`, `kt/moq`, `dart/moq`, + `go/wrapper`, `swift`, `cpp/moq`, `cpp/obs`) and of each binary's flags, + env vars, and config keys by hand. Add the audit step to CONTRIBUTING.md's + Releases section. +- Fill every gap the report and the manual pass show in the matching + `MIGRATE.md`. Known gaps: breaking releases cut after the 2026-09-23 train have no transition section yet (moq-json 0.4 -> 0.5 and possibly moq-audio and moq-video), because their breaks predate the backfill's merge base. The [compression migration](/quest/m1/compress/migrate.md) deletes moq-flate before this cut: audit the migration from its published API to ordinary tracks with `compress`, rather than promising a new moq-flate transition release. +- Each `## Unreleased` section says how to migrate from the published + version to the candidate (decided 2026-10-22, maintainer, from an external + API audit). Collapse entries a later change reversed or reshaped (an API + removed then reintroduced, a default changed twice) into the net change, + and drop the intermediate history. - Compile-check the code in every `MIGRATE.md` section the cut ships against the candidate. Nothing else builds it. - Required by the cut, after m1's breaking work has landed. ## Required +- [API-diff check](/quest/m1/api-diff.md) - the report this audit reads - [Breaking release readiness](/quest/m1/README.md) - all breaking work and the MIGRATE.md convention have landed