From 48564a8f49e6934440743a986834957c6b13dedb Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Thu, 13 Aug 2026 08:31:24 +0000 Subject: [PATCH] docs(review 001): decompose Unit 6 into Units 6-10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original Unit 6 was the largest unit by item count and mixed trivial doc fixes with real protocol-semantics work (P-03/P-04/P-06/ P-07) in one commit. Decomposed into five focused units, each independently shippable, ordered by risk and dependency: - Unit 6 — convention + doc cleanup (C-09, C-20-rem, C-22-rem, C-24, P-10, P-11): mechanical; `cargo doc` clean, producer/consumer naming, ADR-046 §3 text amendment, EVENT_* constants in pump_sink. - Unit 7 — small correctness fixes (C-19-misc, C-21, C-23, P-13): write_header returns Result not panic, honest error envelopes, mux pump map leak, derive_alpn_from_op_name dead branch, Box::leak Arc refactor, REQ-CH-04 counter, opaque-wrap chunk_tx. - Unit 8 — Pub protocol semantics (P-03, P-04, P-07): publish_schema per-chunk validation via alktype, initiator call.error terminates the stream, early handler return short-circuits the feed. - Unit 9 — abort-cancels-Pub (P-06): drop the handler future on abort (not just join! to completion); needs a cancellation token per in-flight sink; depends on Unit 8's pump_sink changes. - Unit 10 — stubs + substrate + doc renumber (C-10, C-11, C-14, C-15, C-26): honestly stub the deferred behaviors with OQs, remove stale doc claims, renumber alknet ADR refs to alkcall ADR-001..047. Also records which original Unit 6 items were already absorbed by Units 1-5 (P-09, C-20's reassembly deliberation, C-22's env tautology, C-19's next_id/demux_sender/mux-error-mapping) so the units below cover only what remains. Verified 2026-08-13 against tree f25d0a6 (post-Units 1-5): every remaining item re-checked in source; the `cargo doc` warning count (2, down from 4 — the register_openable links were fixed in Unit 3) and file:line references updated to the current tree. --- ...001-pub-and-channels-integration-review.md | 381 ++++++++++++++---- 1 file changed, 302 insertions(+), 79 deletions(-) diff --git a/docs/reviews/001-pub-and-channels-integration-review.md b/docs/reviews/001-pub-and-channels-integration-review.md index 5ae3b63..575f847 100644 --- a/docs/reviews/001-pub-and-channels-integration-review.md +++ b/docs/reviews/001-pub-and-channels-integration-review.md @@ -953,91 +953,314 @@ promises. - Test: C-25 #4 #5 — open-at-cap-then-drop-transport-then-reopen, and the concurrent-open race test. -## Unit 6 — Cleanup: comments, doc links, stale spec docs, stubs (C-09, C-10, C-11, C-14, C-15, C-19, C-20, C-21, C-22, C-23, C-24, C-26, P-03, P-04, P-06, P-07, P-09, P-10, P-11, P-13) +## Units 6-10 — the post-integration long tail -**Goal:** conventions satisfied, docs consistent, the remaining -decided-but-unimplemented behaviors either implemented or explicitly -deferred with an OQ. -**Scope:** This is the largest unit by item count but the lowest risk; -do it last, after the integration is end-to-end functional. Items: -- **C-20:** remove the ~50 lines of abandoned deliberation in - `reassembly.rs:205-255` and the pervasive inline `//` comments - across the channels module that aren't carrying a non-obvious - correctness constraint. Fix the actively-wrong comments (mux EOF - claim, abort-cancels claim, wrong-side peer-EOF claim, "SAFETY:" - comment marking no `unsafe`). -- **C-09:** fix the remaining `cargo doc` warnings (`default_policy` - link, `env is both a module and a macro`). -- **C-26:** update `channel-client.md`, `channels-wire.md`, - `channels-adapter.md` to the post-047 model (remove - `open_channel(..., direction)`, `ChannelDirection`, - `ResourceEntry.access`, the generic `channel/open` handler, - `channel:unknown_alpn`; renumber stale alknet ADR refs to alkcall - ADR-001..047). -- **C-21:** make `write_header` return `Result<_, ChunkError>` instead - of panicking. -- **C-22:** remove the filler tests (`client.rs:131-136`, `env.rs:155`) - or replace them with real assertions. -- **C-23:** give `dispatch_requested`'s Sink/Stream error envelopes a - real request id (or document why empty is correct). -- **C-24:** replace "client→server streaming" in - `call-protocol.md:335` with producer/consumer phrasing; amend - ADR-046 §3 to match §6 (P-11). -- **C-19 misc:** fix `next_id` wrap, the unbounded resources-snapshot - loop, the mux pump map leak / duplicate registration, the wrong - `ManagerError` mapping on mux register failure, the misleading - `drop(state.demux_sender.clone())`, the `derive_alpn_from_op_name` - multi-segment bug + dead branch, the `Box::leak` `Arc` refactor, - the `policy.rs:127-140` check-and-reserve rollback trap (document or - add a rollback API), the `Demux::stats()` error counter (REQ-CH-04). -- **P-03:** implement `publish_schema` per-chunk validation using - `alktype` (the dependency is already declared and unused), and wire - it through `spec_to_json` / `operation_spec_schema()` / - `rebuild_spec_for` so imported Pub ops keep the schema. Or, if - deferred, file an OQ and remove the misleading doc comment at - `spec.rs:181-186`. -- **P-04:** make `pump_sink` match `call.error` and inject the - initiator's `CallError` as an `Err` item that terminates the stream - (per ADR-046 §6). -- **P-06:** implement abort-cancels-Pub. This likely needs the - single-stream call mode from Unit 2 (so an abort on the same stream - can reach the running pump) plus a mechanism to drop the handler - future on abort (the doc comments at `dispatch.rs:494-498` describe - the target shape). Decide whether to fix the cross-stream path - (`abort()` opening a new stream) or deprecate it in favor of - same-stream abort. -- **P-07:** fix the handler-returns-early stall — when the handler - returns, the feed should be short-circuited and the response written - immediately (don't `join!` to completion). Use `tokio::select!` or - feed the handler's completion back into the feed loop. -- **P-09:** fix `make_sink_forwarding_handler` to stream (not - `collect`), and to terminate on `Err` (not `filter_map` it away). - Depends on P-01/P-02. -- **P-10:** use the `EVENT_*` wire constants in `pump_sink` instead of - string literals. -- **P-13:** opaque-wrap `SinkDispatch::chunk_tx` before crates.io. -- **C-10 / C-11:** either implement `channel/control` routing and live - `resources/subscribe`, or file OQs and make the stubs honest (return - `unimplemented`-style errors, not fake success). -- **C-14 / C-15:** either implement the QUIC-native substrate and the - full `ChannelClient` API (`open_channel`, `Channel`, - `subscribe_resources`), or file OQs and remove the misleading doc - comments / nonexistent method references. +The original Unit 6 was the largest unit by item count and mixed +trivial doc fixes with real protocol-semantics work (P-03/P-04/P-06/ +P-07). It has been decomposed into five focused units, ordered by risk +and dependency. Several original Unit 6 items were already absorbed by +Units 1-5 during their implementation: -## Suggested sequencing +- **P-09** fell out of Unit 1's P-01/P-02 streaming rework + (`make_sink_forwarding_handler` now streams + terminates on `Err`). +- **C-20's** biggest item — the ~50 lines of abandoned deliberation in + `reassembly.rs` — landed with Unit 4's backpressure rewrite + (reassembly.rs was rewritten from scratch). +- **C-22's** `env.rs:155` tautology test was removed in Unit 3. +- **C-19's** `next_id` wrap-to-0, the misleading + `drop(state.demux_sender.clone())`, and the `manager.rs` mux-register + error mapping were fixed in Units 4/5. +- **C-13** (drain-before-close) and **C-06** (all 4 teardown paths) + landed in Unit 5. + +The units below cover what remains. Each is independently shippable. + +## Unit 6 — Convention + doc cleanup (C-09, C-20-rem, C-22-rem, C-24, P-10, P-11) + +**Goal:** conventions satisfied, `cargo doc` clean, no actively-wrong +comments, producer/consumer naming consistent in docs. +**Files:** `src/protocol/dispatch.rs`, `src/client/from_call.rs`, +`src/channels/operations.rs`, `src/channels/mod.rs`, +`docs/architecture/call-protocol.md`, +`docs/architecture/operation-registry.md`, +`docs/architecture/decisions/046-publish-operation-type-and-handler-kind-sink.md`. +**Scope:** +- **P-10:** `pump_sink` matches the string literals `"call.published"`, + `"call.completed"`, `"call.aborted"` (`dispatch.rs:475,485,486`) + instead of the `EVENT_PUBLISHED`/`EVENT_COMPLETED`/`EVENT_ABORTED` + constants the rest of the file imports (`wire.rs:12-17`). Pure + refactor hazard; no behavior change. +- **C-20 remainder:** the wrong "SAFETY:" comment at + `from_call.rs:271` (marks no `unsafe` block — `derive_alpn_from_op_name` + returns `Option`, the leak happens in `leak_alpn`). Reword to + a plain note about the `'static` lifetime requirement. The mux EOF + claim, abort-cancels claim, and wrong-side peer-EOF claim were fixed + with Unit 4's reassembly rewrite — verify they're gone and remove + any remaining stray inline `//` comments in the channels module that + aren't carrying a non-obvious correctness constraint. +- **C-09:** fix the 2 remaining `cargo doc` warnings (verified 2026-08-13): + - `unresolved link to default_policy` (`operations.rs:50` — the + `[`default_policy`]` intra-doc link resolves to `super::policy::default_policy`; + use the full path `[`super::policy::default_policy`]` or re-export). + - `env is both a module and a macro` (`channels/mod.rs:30` — the + `[`env`]` link in the module doc is ambiguous; qualify it as + `[`env`][self::env]` or use the full path in the prose). +- **C-22 remainder:** remove the filler `PhantomData` test at + `client.rs:291-294` (`let _ = std::marker::PhantomData::;` + — asserts nothing). The `env.rs:155` tautology is already gone. +- **C-24:** replace "client→server streaming" with producer/consumer + phrasing in `call-protocol.md` (the Pub reference-table line) and + `operation-registry.md` (the ADR-046 reference-table line). No public + API names offend; this is doc-only. +- **P-11:** amend ADR-046 §3's `SinkHandler` type so the stream item + type matches §6. §3 (`decisions/046-...md:146`) declares + `Pin + Send>>`; §6 (`:268`) declares + `Pin> + Send>>`. The + code uses §6's shape uniformly (`registration.rs:32-40`, aliased as + `PublishStream`). §3 is the one to amend (§6's `Result`-carrying shape + is correct — the handler must see initiator errors). The ADR's + Door-type section explicitly marks the concrete stream item type a + two-way-door detail, so this is a text amendment, not a design change. +**Acceptance gate:** `cargo doc --no-deps` emits 0 warnings; +`cargo clippy --all-targets -- -D warnings` clean; `cargo fmt --check` +clean; `cargo test` green. + +## Unit 7 — Small correctness fixes (C-19-misc, C-21, C-23, P-13) + +**Goal:** no panic paths in library code, no misleading public fields, +honest error envelopes, latent bugs in `derive_alpn_from_op_name` and +the mux pump map fixed. +**Files:** `src/channels/wire.rs`, `src/channels/mux.rs`, +`src/channels/operations.rs`, `src/client/from_call.rs`, +`src/protocol/dispatch.rs`, `src/registry/spec.rs`. +**Scope:** +- **C-21:** `write_header` (`wire.rs:110-114`) slices `out[..CHUNK_HEADER_LEN]` + and panics on short buffers. The doc comment at `:106-107` declares + the panic. Return `Result<_, ChunkError::HeaderTooShort>` instead + (the variant already exists for the parse side, `wire.rs:67-68`). + Update `write_chunk` (`:138-150`) and all callers (they pass + `[0u8; 8]` today, so the `Result` will always be `Ok` — but the API + is `&mut [u8]` and the contract should be typed, not panic-documented). +- **C-23:** `dispatch_requested`'s Sink and Stream error arms use + `String::new()` as the request id (`dispatch.rs:236,242`), producing + envelopes with an empty id. Pass the real `request_id` (the `Once` + arm at `:298` already does this correctly via `invoke`). +- **P-13:** `SinkDispatch::chunk_tx` is `pub` (`dispatch.rs:76-79`), + exposing the `futures::mpsc::Sender` type and the 64-slot buffer size + as effective public API. Make the field private (or `pub(crate)`) + before crates.io; `handle_stream` and `pump_sink` are the only + readers and they're in the same module. +- **C-19 (mux pump map leak + duplicate registration):** `mux.rs:159` + inserts each pump's `JoinHandle` into `self.pumps` and never removes + finished ones — they accumulate for the connection's lifetime. + Duplicate `register(channel_id)` silently overwrites the old handle + while the old pump keeps running (two pumps, one channel id). Fix: + detect a finished pump before insert (poll the `JoinHandle` or use + `is_finished()`), and reject duplicate registration (return an error + the caller maps to `ChannelExists`). +- **C-19 (`derive_alpn_from_op_name`):** `from_call.rs:291-303` takes + only the first path segment (`rest.split('/').next()`), so + `channels/custom/proto/sub` derives `alknet/custom` instead of the + full ALPN `custom/proto`. The `segment.starts_with("alknet/")` branch + at `:297` is dead (a single segment can't contain `/`), and + `segment == "alknet"` yields the nonsense ALPN `"alknet"`. Fix: strip + the known `/sub`|`/pub` suffix from `rest` instead of taking the + first segment, so multi-segment ALPNs survive. The `channels/tty/sub` + common case already works; this fixes the latent non-`alknet/*` + multi-segment case (no live callers yet, but the `Arc` refactor + in Unit 8 will make this path reachable). +- **C-19 (`Box::leak` per discovery):** `from_call.rs:314-316` leaks + a `String` to `'static` on every `leak_alpn` call. Bounded per unique + ALPN in theory, but leaks on every *rediscovery* of every marked op. + Refactor `ChannelOpenSpec::alpn` from `&'static str` to + `Cow<'static, str>` (or `Arc`) so `leak_alpn` can return an + owned value. This is a small public-API change to `ChannelOpenSpec` + (`spec.rs:38-46`); no deployments exist, and the ADR-047 §2 shape + said `&'static str` was "for now" (the `Box::leak` was the workaround + for that constraint). The `Cow`/`Arc` is the deferred refactor. + (If this grows beyond a quick edit, split it into its own unit.) +- **C-19 (REQ-CH-04 error counter):** the demux's lenient + unknown-channel drop (`manager.rs:389-394`) only `debug!`-logs; there + is no counter or stats surface. Add a simple `AtomicU64` dropped-chunks + counter to `ChannelManager` with a `dropped_unknown_chunks()` accessor + for observability. (Low priority — file an OQ if the counter surface + isn't worth the API surface today.) +- **C-19 (resources-snapshot unbounded loop):** `operations.rs:278-285` + iterates `0..u32::MAX` with a per-iteration mutex lock, breaking when + `resources.len() >= manager.open_count()`. With sparse ids (monotonic + after churn) or a channel closing mid-iteration, this can iterate + millions of times. Replace with an iterator over the channel map's + keys (add a `ChannelManager::channel_ids() -> Vec` accessor) so + the snapshot is O(open channels), not O(max id). (This is also + touched by Unit 9's `resources/subscribe` rewrite — coordinate or + fold the loop fix into Unit 9.) +**Acceptance gate:** `cargo test` green; `cargo clippy --all-targets +-- -D warnings` clean; no panics reachable from `write_header`; the +`derive_alpn_from_op_name` test covers a multi-segment non-`alknet/*` +ALPN. + +## Unit 8 — Pub protocol semantics (P-03, P-04, P-07) + +**Goal:** the publish path's decided-but-unimplemented behaviors land: +per-chunk schema validation, initiator `call.error` terminates the +stream, and an early handler return short-circuits the feed. +**Files:** `src/registry/spec.rs`, `src/registry/discovery.rs`, +`src/client/from_call.rs`, `src/protocol/dispatch.rs`. +**Scope:** +- **P-03:** `OperationSpec::publish_schema` (`spec.rs:181-186`) is + declared, has a builder (`with_publish_schema`, `:236-239`), defaults + to `None`, and is **never read**. `pump_sink` forwards chunks as-is + (`dispatch.rs:475-484`). It is also missing from `spec_to_json` + (`discovery.rs:197-217`), from `operation_spec_schema()` + (`discovery.rs:102-161`), and from `from_call`'s `rebuild_spec_for` + (`from_call.rs:205-282`) — so an imported Pub op loses the schema. + Implement: (a) add `publish_schema` to `spec_to_json` (emit as + `"publish_schema"` when `Some`), `operation_spec_schema()` (add the + property to the schema), and `rebuild_spec_for` (parse it back); (b) + in `pump_sink`, validate each `call.published` chunk's `input` + against `publish_schema` before yielding it — on validation failure, + inject an `Err(CallError::invalid_input(...))` and terminate the + stream (matching the `SinkHandler` contract). Use `alktype`'s + `validation::build_validator` (the dependency is declared and unused; + `jsonschema` is transitively available). When `publish_schema` is + `None`, chunks are yielded as-is (current behavior). +- **P-04:** `pump_sink` matches only `"call.published"`, `"call.completed"`, + `"call.aborted"` and drops everything else into a debug-log ignore + branch (`dispatch.rs:492-497`). A `call.error` frame from the + initiator is silently ignored. Per ADR-046 §6, an initiator-side + `call.error` should inject the initiator's `CallError` as an `Err` + item that terminates the stream. Match `"call.error"` in the + `pump_sink` event match, parse the `CallError` from the payload, send + `Err(call_error)` into `chunk_tx`, and break the feed. +- **P-07:** `pump_sink` uses `tokio::join!(feed_fut, handler)` + (`dispatch.rs:510`) and only writes the response after **both** + complete. If the `SinkHandler` returns early (e.g. rejects after chunk + 1), the feed loop notices only when its next `chunk_tx.send` fails — + a paused or long initiator never learns of the early error. Fix: + use `tokio::select!` (or race the handler against the feed) so that + when the handler completes, the feed is short-circuited and the + response is written immediately. Dropping `chunk_tx` on the handler + side signals the feed to stop. Add a test: a sink handler that + returns after 1 chunk; an initiator that publishes 3; assert the + response arrives without waiting for all 3 chunks. +**Acceptance gate:** a test that registers a Pub op with a +`publish_schema`, publishes a chunk that violates it, and asserts the +handler receives an `Err` and the stream terminates; a test that an +initiator `call.error` terminates the publish stream; a test that an +early-returning handler's response is not deferred behind a slow feed. + +## Unit 9 — Abort-cancels-Pub (P-06) + +**Goal:** an initiator can actually cancel an in-flight Pub; the +handler future is dropped on abort, not just `join!`-ed to completion. +**Files:** `src/protocol/dispatch.rs`, `src/protocol/connection.rs`, +possibly `src/protocol/pending.rs`. +**Scope:** This is the one Unit 6 item with a design decision, so it +gets its own unit. The single-stream call mode from Unit 2 already +routes `call.aborted` for an in-flight sink's `request_id` to the +matching `chunk_tx` (`dispatch.rs:684-693`), injecting an `Err` — but +the handler future is spawned separately (`dispatch.rs:670-680`) and +is **not** dropped on abort; it runs to completion and its response is +written to the wire after the abort. The stream-per-request +`pump_sink` path (`dispatch.rs:447-517`) `join!`s the handler to +completion regardless of abort. Decisions: +- **Single-stream path:** wire a cancellation token (or `oneshot`) per + in-flight sink so `call.aborted` both injects the `Err` **and** + aborts the spawned handler task. The handler's `JoinHandle` is + already stored implicitly via `tokio::spawn` — keep it in the + `in_flight_sinks` map alongside `chunk_tx` so abort can `.abort()` it. +- **Stream-per-request path:** `pump_sink` needs the same: on + `call.aborted` for this `request_id`, drop `chunk_tx` (feed gets EOF) + **and** drop the handler future (use `select!` with an abort signal + instead of `join!`). The cross-stream `abort()` path + (`connection.rs:398-415`) opens a new stream and only mutates + `PendingRequestMap` — on the responder, a different `handle_stream` + task receives it and calls `handle_abort`, which doesn't reach the + running `pump_sink`. The single-stream mode makes the cross-stream + abort path less relevant for channel 0; document whether + stream-per-request abort is worth fixing or should be deprecated in + favor of single-stream. +- **Also (from P-06):** during a sink pump, `call.aborted` frames for + **other** request IDs on the same stream are dropped by the + id-mismatch guard before the type match (`dispatch.rs:466-473`). On + the single-stream path this is handled (the outer loop routes by + `request_id` first); on the stream-per-request path it's a latent + bug. Verify the single-stream path is correct and decide whether the + stream-per-request path needs the multiplexing fix or just + deprecation. +**Acceptance gate:** a test that aborts an in-flight publish and +asserts the handler future is cancelled (the handler observes a +drop/cancellation, not just an `Err` injected into the stream it's no +longer reading); a test that the aborting initiator receives +confirmation and the responder's resources are released. + +## Unit 10 — Stubs, substrate modes, and spec-doc renumbering (C-10, C-11, C-14, C-15, C-26) + +**Goal:** the remaining decided-but-deferred behaviors are honestly +stubbed (no fake success) with OQs filed; the spec docs are consistent +with the post-047 model and the alkcall ADR numbering. +**Files:** `src/channels/operations.rs`, `src/channels/adapter.rs`, +`src/channels/client.rs`, `docs/architecture/channel-client.md`, +`docs/architecture/channels-adapter.md`, +`docs/architecture/channels-wire.md`, `docs/architecture/open-questions.md`. +**Scope:** This is the lowest-risk unit and can land last. It is mostly +docs + OQ filing. +- **C-10 / C-11:** `channel/control` (`operations.rs:234-262`) returns + `{"ok": true}` for a silently-discarded `message`; there is no + control-handle concept. `channel/resources/subscribe` + (`operations.rs:268-289`) is `futures::stream::once(...)` (one-shot, + not live), emits currently-open channels' ALPNs (not the set of + openable ALPNs aggregated from ALPN-crate resource enumerators), and + has the unbounded `0..u32::MAX` loop (see Unit 7). Either implement + (control-handle routing; live subscription aggregated from + enumerators) or file OQs and make the stubs honest: return an + explicit `unimplemented`-style `CallError` (code + `channel:control_not_implemented` / `channel:resources_not_implemented`) + so callers fail loudly, not with fake success. Recommend: file OQs + (the control-handle and resource-enumerator surfaces are real design + work, not cleanup) and make the stubs honest. +- **C-14 / C-15:** only the in-line substrate mode is implemented + (`adapter.rs:199-259` accepts one bidi stream once). The QUIC-native + multi-stream substrate ("accept remaining bidi streams, read headers + off each") is not implemented. `ChannelClient` has `open_channel` + (Unit 5) but not `subscribe_resources` or the `Channel { channel_id, + source }` struct from ADR-043. Either implement or file OQs and + remove the misleading doc comments / nonexistent method references. + Recommend: file OQs (QUIC-native substrate + the full `ChannelClient` + API are features, not fixes) and remove the stale doc claims + (`client.rs` doc comment referencing a nonexistent + `manager.open_channel_stream`). +- **C-26:** update `channel-client.md`, `channels-adapter.md`, + `channels-wire.md` to the post-047 model: remove `open_channel(..., + direction)`, `ChannelDirection`, `ResourceEntry.access`, the generic + `channel/open` handler, `channel:unknown_alpn` (replaced by + per-ALPN ops in ADR-047 §3). Renumber stale alknet ADR refs + (071/075/076/093/094/079/080 and others >047) to their alkcall + renumbered equivalents (ADR-001..047), or mark them as + "alknet source ADR, not yet ported" where no alkcall equivalent + exists. This is a doc-only pass; verify against + `docs/architecture/decisions/` (which only goes to 047). +**Acceptance gate:** stubs return explicit errors (not `{"ok": true}`); +`cargo doc --no-deps` has no broken links to nonexistent methods; the +channels spec docs no longer describe the pre-047 `channel/open` +handler or `ChannelDirection`; OQs filed for C-10/C-11/C-14/C-15. + +## Suggested sequencing (post-Units 1-5) ``` -Unit 1 (Pub end-to-end) → unblocks the Pub flagship use cases -Unit 2 (channel 0 single-stream) → unblocks all channel-0 integration tests -Unit 3 (register_openable) → needs a §4 spec decision first; unblocks opening channels -Unit 4 (backpressure) → independent; can run in parallel with 2/3 -Unit 5 (ledger + adoption) → needs a §5 spec decision first; depends on 3 -Unit 6 (cleanup) → last; after the integration is end-to-end functional +Unit 6 (convention + doc cleanup) → no deps; mechanical; do first +Unit 7 (small correctness fixes) → no deps; independent +Unit 8 (Pub protocol semantics) → no deps beyond Units 1-5; P-03 uses alktype +Unit 9 (abort-cancels-Pub) → depends on Unit 8's pump_sink changes (shared file) +Unit 10 (stubs + substrate + doc renumber) → no deps; can land any time ``` -Units 1, 2, 4 can proceed immediately with no spec decisions. Units 3 -and 5 each need one written decision (ADR-047 §4 affirm/amend; ADR-047 -§5 odd/even-or-adoption). Unit 6 is the long tail. +Units 6, 7, 8, 10 are independent and can proceed in any order (or in +parallel across separate branches). Unit 9 touches the same `pump_sink` +code as Unit 8, so land 8 before 9 to avoid merge conflicts. None of +Units 6-10 require a spec decision (the ADR-047 §4 and §5 decisions +were made in Units 3 and 5). Units 1-5 are complete (commits +`502488c` through `f25d0a6`); the integration is end-to-end functional. ## On the baseline objective