Files
alkblobs/docs/reviews/003-family-wire-gap-review.md
T
glm-5.3-flash 927736cf13 docs(review): 003 closed with the ADR-020 pointer; overview keying story aligned to the registry
- review-003: frontmatter flipped open -> closed with the acceptance
  record (ADR-020 accepted 2026-10-18, commit eea472b; OQ-20 resolved
  by it — the §Decision 2 re-entry door this review's disposition
  named to open). In-body closure annotations on the Addressing
  conversation and Disposition sections: the ask table is fully
  dispositioned (F-1..F-4 inside ADR-020), the implementation wave is
  the remaining work and gates on nothing here.
- overview.md: the keying story aligned to the registry posture —
  "one canonical content hash" was the pre-ADR-018/020 shape; the
  registry of git-family derivations with no default put (ADR-020's
  invariant) is what this crate is now. The alkgit bullet cites the
  ADR-018 extension. Spec stays `stable` (wording alignment to decided
  ADRs, no shape change).

Verification: docs-only; link depths checked (docs/reviews ->
../architecture/...), confusable-character sweep clean.
2026-10-18 16:35:37 +00:00

34 KiB

status, last_updated, reviewed_artifacts, tool, reviewer, scope_note
status last_updated reviewed_artifacts tool reviewer scope_note
closed Opened 2026-10-18 — operator-raised design review of the wave-13 family round's ops-surface scoping (ADR-018 Section 2 + c1-git-object-family-impl). Report-only: findings + recommended shape for the addressing session; nothing decided here. The addressing session re-verifies the anchors and re-derives the decision; its likely outcome is ADR-020 (the door ADR-018 itself named) plus the implementation round behind it. Amended same day: publication facts corrected (0.1.0 published 2026-10-18 carries both the put_git_object facade method and the opt-in ops feature code - zero known consumers, not an unpublished surface), the consumer horizon recorded (alkgit active, a second consumer waiting on solidification), and POC-X demoted to measurement material - the design derives from the registry invariant, not from how the POC routed around the gap. Amended same day (second): the addressing conversation converged - the Addressing conversation section's D-1 through D-6 are the decided direction ADR-020 drafts from, superseding the recommended-shape candidates; the review stays open until ADR-020's acceptance. Closed 2026-10-18 (third amendment, same day): **ADR-020 is accepted** (docs/architecture/decisions/020-family-explicit-wire-payloads.md; docs(adr): 020 — commit eea472b) — drafted from D-1 through D-6 verbatim, resolving [OQ-20](../architecture/open-questions.md) (the §Decision 2 re-entry door this review's disposition named to open); the record corrections landed with it (ADR-018 §§2/6 superseded banners, ADR-012/014/010 pointers, ops-surface.md / store-api.md / hashing-and-keys.md re-flipped reviewed). The disposition completes here: what remains is the implementation wave (decided-but-sequenced work; decomposition seams named in ADR-020 §Decision 9), which is review work no longer. Nothing in this review decided design; every decision lives in ADR-020.
The wave-13 record: docs/architecture/decisions/018-git-object-derivation-family.md (Section 2 in particular), docs/reviews/002-first-consumer-review.md (C-1 + the disposition), docs/architecture/ops-surface.md (the digest wire form's family posture section), docs/architecture/store-api.md, docs/architecture/hashing-and-keys.md
src/ anchors verified this session against main's working tree: src/ops/wire.rs (digest_ingress, DIGEST_WIRE_PATTERN, the put error shapes), src/ops/put.rs (the put_establish/put_handle/ finish_put flow), src/ops/fetch.rs (the fetch ingress), src/ops/specs.rs (stat/have/delete ingress), src/hashing.rs (from_git_oid_hex + its contract doc), src/store/put.rs (put / put_git_object / put_known_as routing), src/keys.rs (the physical encoding), src/store/fleet.rs (the family's fleet threading tests)
The consumer-side evidence of record: /workspace/@alkdev/alkgit/docs/research/pocx-findings.md (POC-X, completed 2026-10-18 — the reproduction tests/ops_gap.rs in /workspace/alkgit-pocx hits the gap end to end, and the finding text already names its upstream shape), substrate-recomposition.md (the have/need diff posture)
desktop review of the wave-13 record + the consumer POC findings, with targeted source verification of every anchor named above operator-escalated design review seat — the finding arrived from the operator (the wave-13 round's outcome did not meet the coherence bar); this session records it as a review so the addressing session starts from the record, matching review 002's posture covers one question: can the crate put an entry class it stores that its network op family cannot address? The store-side family work (registry, derivation, put paths, GC, fleet) is out of scope — it was verified whole and is not re-litigated.

Review 003 — The Family Wire Gap (the ops surface cannot address what the facade can store)

Purpose

Review-002 C-1 ended with ADR-018: the git-object derivation family (registry bytes 0x03/0x04, preimage-identity hashing, the loose-object form as the stored preimage) landed across the store surface — put paths, fleet threading, verification-on-get, GC, dedup.

This review records the operator's finding about how the round closed: ADR-018 §Decision 2 deliberately scoped the network ops surface (blobs/fetch, blobs/stat, blobs/have, blobs/delete, blobs/put) to the blob family only for 0.1, on a named re-entry door ("a named consumer requirement for non-blob object transfer over the wire opens a new ADR"). The landed crate therefore stores a class of entries its network op family cannot see at all. The operator's verdict — adopted by this review's scope: a put surface that writes entries the protocol cannot address is a half surface, and the coherence bar is the crate's own, not a consumer's preference. "Half works only in the in-process configuration" is not a deployable shape and not an acceptable resting state of the crate.

The review makes that finding precise, examines how the decision passed its gates, and frames the shape of the correction for the addressing ADR (ADR-020, candidate) — with two constraints the operator fixed in advance: the correction must not weld the wire to git (the next consumers are non-git compositions, e.g. a virtual filesystem), and length-based family guessing remains rejected as always.

Background: what Section 2 decided and where it landed

ADR-018 §Decision 2, verbatim posture: the digest wire schema is ^[0-9a-f]{40}$|^[0-9a-f]{64}$, the ingress (digest_ingress → Digest::from_git_oid_hex) is pinned blob-family only — a git-object oid on the wire "answers the miss for its blob-family-shaped key — honest, because the family-explicit key bytes differ by tag" — and the put handler's offer cross-check hashes the canonical blob derivation. The re-entry door: a family-explicit wire form (typed or prefixed digest field) needs a named consumer requirement plus a new ADR; length-guessing is never the answer.

The implementation landed exactly that, docs included (ops-surface.md's family-posture section; the ops module doc; from_git_oid_hex's contract doc). Behavior was verified unchanged; the wave-13 gate stamped the one-way door's bytes against the ADR line by line. No part of this review disputes the store-side landing — it was executed to spec. The question this review raises is whether the spec's §Decision 2 should have existed in that form at all.

F-1 [design gap, the core finding] — Family entries are store-reachable but protocol-invisible

The write side is whole. put_git_object routes the identical machinery as the canonical put — dispatch, dedup, pins, GC, both fleet forms (the joint entry+pin tx and the pg-lo consolidation) — per ADR-018 §Decision 7 and its fleet threading tests (src/store/fleet.rs :1460 / :2362). Every family entry in a pool is a first-class entry: listed by the sweep, deduped by construction, verified on every get, reclaimable by GC. Nothing about the put path is a stub.

The serving side is a wall. Every ops entry point keys through digest_ingress (src/ops/wire.rs:75) → Digest::from_git_oid_hex (src/hashing.rs:315), pinned to the blob family by length-routed decoding. Concretely, for a family entry that demonstrably exists in the pool:

  • blobs/fetch answers BLOB_MISSING;
  • blobs/stat answers the miss;
  • blobs/have answers present: false for the entry the very node holds;
  • blobs/put cannot land one even offered: the tee hasher hard-codes the canonical blob derivation (src/ops/put.rs:333) and the store arm calls store.put (src/ops/put.rs:320), so even a schema-passing family oid would cross-check against the wrong derivation and (if it committed at all) commit under a blob-family key — the wrong bytes-under-wrong-address outcome.

The "honest miss" framing does not survive contact with the rest of the same crate. ADR-018 §Decision 2 justifies the wire miss as honest ("the family-explicit key bytes differ by tag"). But the miss answers state the same crate's facade creates: any deployment that composes put_git_object in-process — which is the one consumption shape this round existed to serve — produces pool entries that the node's own network op family permanently cannot see, fetch, or replicate. A miss is honest when the caller asks about state the sink has no policy to hold. It is not honest when the sink holds the state and the same crate's serving half refuses to look it up. The store and its protocol are one product; a class of entries visible to one half and invisible to the other is an incoherence, not a scope.

The consumer measured exactly this, end to end. POC-X (/workspace/alkgit-pocx, completed 2026-10-18 — one day after the wave-13 gate) contains tests/ops_gap.rs: a git object stored under its own oid; the ops ingress maps the 40-hex oid onto the blob-family key (different tag byte) and blobs/have misses the entry that visibly exists. The finding's own text names the structural shape: wire-expressible transfer over blobs/* needs a family-explicit ingress, with put/fetch following, because blobs/put verifies under the canonical blob derivation and even offered git loose objects cannot land by oid.

F-2 [decision process] — How an incoherence passed as a scoping statement

This is the part the operator called laziness; it deserves a precise name. Three failures compound, in increasing importance:

  1. The re-entry door inverted the crate's own deferral policy. The Schrödinger's-code rule (open-questions.md) exists to stop this crate from designing for consumers that do not exist. ADR-018 §Decision 2 applied it in the opposite direction: to defer completing a surface this crate itself ships, gating the fix on a named consumer requirement. That is a category misuse. Coherence — the property that the pool's addressable classes and the wire's addressable classes are the same set — is the crate's own requirement, independent of who is listening. Nothing on the wire needed a consumer to be decided; the consumer requirement was always only about the form (typed field vs prefixed hex) — a presentation choice the ADR escalated into a scoping exemption.

  2. The premise was factually shaky when stamped. §Decision 2's "no named wire consumer for 0.1" was written while review-002's author — the consumer of record — had already run the consumption POCs; the ADR itself names "replication is the anticipated shape". And the consumer's evidence has now landed the reproduction (POC-X's tests/ops_gap.rs), one day after the gate. Fairness requires recording what the evidence also says: POC-X's recommendation was protocol-native replication at v1 (git's own have/negotiation riding the GitSession plane), the blobs plane recorded as the alternative — so alkgit may never become a blobs-wire consumer for git objects, and the previous agent's "the named consumer was standing at the gate" overstates it. But the conclusion does not soften, because the coherence argument was never about alkgit: the consumer hitting the wall and routing around it measures the wall, and the wall is what the crate ships.

  3. Nobody dissented at a gate built for dissent. The wave-13 gate read the one-way door's bytes "line by line" — against the ADR, and the ADR said so, so the bytes passed. A gate validates conformance to the record; nothing validated the record. The scoping was named in every doc (the S-5 discipline held), which is why it read as considered rather than missed — but a decision being loud is not the test of it being right. This review is the correction to that: the finding arrives through the operator, and the record takes it.

Decision framing. Not a publish-block retroactively — the publisher's hold lifted on an record whose §Decision 2 was at the time internally consistent (the put surface existed, the docs named the scoping, the door was typed). The finding is that the resting state is wrong and gets worse with every day of use: each week the facade acquires family-entry deployments, the wire gap grows from a scoping note into a live deployment hazard. The consumer horizon is real and near: alkgit is the first consumer under active work (it consumes the published facade surfaces; its ops-gap exercise runs out-of-repo through the POC compositions), and a second consumer is waiting for the surface to solidify before starting — decisions made under a "zero callers today" view amplify into both as rework and publish churn. The window's shape, corrected: 0.1.0 is published (2026-10-18) and the published artifact carries both the public put_git_object facade method and the ops feature code (opt-in, non-default) with the blob-only wire form pinned in it — the precise statement is zero known consumers, not an unpublished surface. There is no semver weight (0.x may break at a minor bump; the correction lands in 0.2.0, no shim, no dual-form period, no deprecation dance owed to anyone), but the published artifact documents a wire form and a method this correction declares false — registry rot until 0.2.0 lands. The one-way door that actually closed (key encoding, 0x03/0x04) is untouched by this finding and stays closed.

F-3 [substrate posture] — The correction must speak registry vocabulary, not git vocabulary

The operator's second constraint, adopted: the wire must not learn git. This has a hard edge the addressing session must start from: the op set is closed — blobs/fetch, blobs/stat, blobs/have, blobs/delete, blobs/put, and nothing else. There are no family-suffixed op variants (blobs/fetch_git_object-shaped names are rejected on sight); the correction below is entirely payload-parameterization under the existing names, the same way the facade is one get/stat/list surface over every registered algorithm today. The mechanism ADR-018 landed is already registry-generic — an Algorithm enum with registry bytes, per-algorithm digest lengths, derivation-parameterized put/verify paths. Git appears only as the first registered consumer of a preimage-identity family (its loose form happens to be the preimage). The crate's contract with a virtual-filesystem consumer is the same abstract shape: register an algorithm, put/get/serve by (algorithm, digest), verify on read with the key's own algorithm. What would weld the crate is any wire or API surface that names git where the registry is the actual abstraction:

  • Wire form. The family-explicit digest field should name the algorithm — the registry's identity, not git's. Candidates the addressing ADR weighs from: (a) a structured field ({digest: hex, algorithm: "..."}) where the algorithm string is a canonical registry name; (b) an algorithm-prefixed hex string. Both are family-explicit, rejects-never-guesses (no length inference anywhere — the algorithm is explicit or the form is rejected), and identical for every future registry member. The ADR should also rule on whether the bare-hex blob forms keep an implicit default-family reading or become fully explicit; with zero published wire consumers the coherence argument (one rule, no special cases) weighs against keeping the special case merely because it is old.
  • The error surface. Ingress rejections carry the offending form (INVALID_DIGEST with details) — unchanged mechanically, but the message text should describe the family-explicit contract, not the blob-family hex lengths.
  • The stat semantic rides per-entry, unchanged — ADR-018's preimage-length rule is already algorithm-parameterized; the wire doc states it per algorithm, and nothing here re-opens it.
  • Out of scope but named: whatever algorithm a VFS consumer needs (identity-hash content addressing, or anything else) is its own registry admission — a new ADR with its own byte — and that is the correct door for it. The wire form decided here must simply not need revisiting when that door opens; that property is the whole point of choosing registry vocabulary now.

F-4 [API shape, to be decided] — Why a family-named put method at all

The operator's challenge, recorded verbatim: why is there anything other than the basic put methods, and why a special path. The honest answer has two layers.

The split put is forced by the derivation, not by git. A put that does not know its algorithm cannot verify anything — content can hash under every registered derivation, so the caller naming the derivation is what keeps rejects-never-guesses true at the put boundary. put(len, stream) naming the canonical blob derivation, and put_git_object(algorithm, len, stream) naming family members, are both that one requirement expressed twice — the second time with a consumer's name on it.

The family-named method is the residue to remove, not extend. The operator's position, adopted: no consumer-named methods in the store's public API — put_git_object and any per-family successors it would inevitably breed are themselves the defect class, and duplicating that split on the wire would be the same mistake twice. The general mechanism already exists internally: put_known_as(algorithm, len, stream) (src/store/put.rs) is derivation-parameterized over every registered algorithm — ADR-018 §Decision 7's own "derivation-parameterized sites, not family-branching copies" discipline, applied to the facade itself. The public put_git_object adds nothing over a derivation-explicit put except a git name and a matches! restriction (src/store/put.rs :159-164) that would need editing for every future registry member — exactly the per-consumer branching the discipline forbids elsewhere. The addressing ADR (as §Decision 6's amendment) should land the unification: one derivation-explicit seam (put_as(algorithm, len, stream)-shaped; identity-family members take the preimage verbatim with len as the stored-length check, preamble-carrying members use len for the preamble as today), with the canonical put unchanged and put_git_object removed outright — a deprecated shim is compatibility weight owed to precisely zero callers. Blast radius, read against the consumer horizon: zero in-repo callers, one out-of-repo POC composition (the wave-13 record itself is the only call site of record), and two consumers approaching — none of whom should ever meet a family-named method. Removal now pays the rework here; keeping the name amplifies it into both consumers' code and into every publish between.

Wiring note the ADR inherits: the ops put handler is the same unification on the wire arm — once the offer names the algorithm (F-3), the tee hasher parameterizes per Algorithm (the BlobHasher already carries all four members) and the store arm routes the matching facade seam (src/ops/put.rs:320/333 are the two hardcoded sites).

Superseded 2026-10-18: the addressing conversation derived and decided the shape — the resolution of record is the "Addressing conversation" section below, whose D-1 ruled on this section's candidates. Kept as the pre-conversation state.

The door ADR-018 §Decision 2 itself named, opened by the coherence finding itself — F-1's defect is real with no consumer at all, and F-2.1 records that the consumer requirement was always only about the form. POC-X's role below is measurement and material only: its reproduction measured the wall, and its ops_gap shape becomes the pinning test's substance. The design derives from the registry invariant (convention 12) and the closed-op-set posture, never from how a downstream POC routed around the gap. What the ADR owns:

  1. ADR-020 — the family-explicit payloads under the unchanged op set. The op family is closed (F-3's hard edge): one fetch, one stat, one have, one delete, one put — what changes is what the payloads may say, not what ops exist. Decisions it owns:
    • the digest wire form (F-3's candidates), the ingress contract, and the schema-gate/ingress agreement (the S-5 discipline — the gate and the ingress must disagree with no one; both change together);
    • whether bare-hex blob ingress survives as an implicit default or the form becomes uniformly explicit (no compatibility weight exists; decide on coherence);
    • the put offer shape: algorithm-naming (or family-explicit digest), the cross-check's derivation parameterized, the put response echoing the algorithm so callers address what landed;
    • the ops put arm's store routing (put vs the derivation-explicit seam) and the tee hasher's parameterization (F-4's wiring note);
    • the F-4 put-seam unification (as §Decision 6's amendment or separate implementation task).
  2. The store layer takes no changes. Keys, verification, GC, fleet, stat semantics, and the sweep already serve family keys; the entire gap is in src/ops/ plus the facade's put-name (F-4). The already-frozen key bytes are untouched.
  3. The pinning test: a facade-published family entry must be fetchable, stat-able, and have-servable over the ops surface, and a wire-offered family put must land under its own oid — the reproduction exists (POC-X's tests/ops_gap.rs shape); the addressing task re-homes its substance into this repo's ops tests.
  4. Record corrections: ADR-018 gains a pointer (its §Decision 2's re-entry door resolves into ADR-020 — with the F-2 finding recorded in the new ADR's context, including the honest note that the consumer's own v1 recommendation routes around the blobs plane); ops-surface.md and the module docs drop the blob-family-scoping posture on the same commit.
  5. alkgit's ledger updates itself: the wire-expressible replication option goes from "requires upstream ADR" to "expressible today"; their substrate-native v1 posture stands unchanged as the consumer's routing choice — the upstream ask ledger notes it as available, not as needed.

Addressing conversation — the derived shape (2026-10-18, the design session of record)

Same day as this review's opening: the operator plus an outsider reviewer (fresh to the codebase, reading the anchors against main) re-derived and decided the shape. This section is the resolution record — D-1 through D-6 are decided direction ADR-020 drafts from, not candidates. Findings F-1 through F-4 stand as the analysis of record. Nothing here is implemented; the ADR is the gate, and the implementation round rides it. (Recorded 2026-10-18, same day: ADR-020 is accepted — drafted from this section verbatim — and OQ-20 is resolved by it; the decomposition seams below are the ones ADR-020 §Decision 9 carries. The implementation wave is the remaining work.)

The root statement the conversation converged on. An address is formed only under a named derivation: the same bytes have multiple equally valid addresses, one per registry member (hashing.rs's cross-family vector pins it — the blob loose form hashes byte-identically under both sha-1 arms; only the key tag differs). The algorithm is therefore a required input at every address-forming or address-matching boundary — never an option, never a hidden default, never length-implied. Every defect in the current surface is that one fact in different clothes: put hardcodes it (src/store/put.rs:227), put_git_object consumer-names it (the matches! at src/store/put.rs:159-164), the wire length-implies it (unexpressible in principle now that two registry members share each digest length), and put_known_as — the one correct seam — is private. The put_unknown fleet arm's re-hardcode (src/store/put.rs:360, passing Algorithm::GitBlobSha256 while ignoring its own threaded algorithm parameter) is the same defect inside the machinery: invisible only because the sole caller happens to pass the constant.

D-1 [wire form, decided] — the wire speaks keys. The digest wire form is the physical key encoding verbatim: [registry byte] + digest bytes, hex-encoded (42 or 66 chars), parsed by the same rejects-never-guesses decoder the persistence boundary already trusts (Key::from_bytes-shaped). Decided over the structured {algorithm, digest} candidate. Rationale of record: the wire's audience is node-to-node sync — every human- or agent-facing interaction rides a facade above the ops plane with its own human-shaped vocabulary (the git CLI; the sftp/sshfs gateway shapes the VFS consumer plans), so the structured form's readability has no audience, while its costs (a second identity namespace beside the registry bytes, canonical-spelling drift across implementations, per-field schema growth) are permanent. With the tag byte in the payload, length becomes a check ("tag 0x03 carries exactly 40 hex of digest") and never a guess — the ambiguity that made bare 40-hex fatal dissolves rather than being managed, and the wire couples only to bytes already frozen as the crate's hardest one-way door. Bare hex dies: no implicit default-family reading survives (zero wire consumers; decided on coherence — one rule, no special cases kept for age). The schema pattern becomes ^[0-9a-f]{42}$|^[0-9a-f]{66}$; gate and ingress change together (the S-5 discipline).

D-2 [facade, decided] — one derivation-explicit put seam; the default was a fossil. The facade's write surface is put_as(algorithm, len: Option<u64>, stream) alone. put and put_git_object are deleted outright — zero in-repo callers, one out-of-repo POC call site, 0.x: no shim is owed to anyone, and every publish the family-named method survives teaches the approaching consumers the wrong shape. The "canonical default" question resolved against keeping one: "the most common case" was never evidence — it was residue from the single-algorithm registry, when put defaulted to nothing because nothing else existed. The two approaching consumers (git and the VFS) have no dominance ordering between them (the VFS — the consumer this path was inspired by — is the likely higher-volume one), and a default is only ever justified by dominance; with none, a blessed default is git privilege relocated from the wire to the facade, the F-3 violation in a new seat. Every real caller already holds its algorithm (alkgit from the pack entry, the VFS from its registry member); the caller that cannot name one is precisely the caller that cannot form an address. put_in_batch takes the algorithm parameter. Digest::derive's silent canonical constructor folds to the explicit Algorithm::derive form (minor; rides the same round). Convention 12 is satisfied by construction: parameterized over the registry, never enumerated — the matches! restriction simply ceases to exist.

D-3 [reads, decided] — no changes, by principle. Reads name an existing address — the algorithm is intrinsic to the Key — while writes and wire offers form or claim an address, so the algorithm must arrive with the intent. The asymmetry is the design, not a gap; ADR-020 carries the sentence so the read path is not re-audited for a symmetry that is not supposed to exist.

D-4 [unknown length, decided] — the facade absorbs unidentified streams; the wire only handles identified claims. The principle line, verbatim for the ADR: the wire never discovers addresses; the facade can. The facade keeps the Option<u64> arm — "stream in, get the key back" is a legitimate in-process posture for edge producers, and the store is the right layer for it: the hard part is not the hashing but the crash-safe staging hygiene (buffer-to-threshold, stage-file flush, discard on every failure path), which every outer-layer workaround re-implements — the VFS POC's branch-on-write-commit-when-finished is that exact cost, already paid once. This is retention of built-and-verified machinery for a named approaching consumer who has felt its absence, not speculative design for an absent one; the Schrödinger's-code rule objects to the latter, not to this. The wire mandates the full claim — key hex and len, both mandatory in the put offer: no wire peer is structurally unable to know (a sync offerer holds the entry it offers; a relay's upstream identified it), so the put handler's deferred cross-check arm (the unknown-length post-check, src/ops/put.rs:445/482) is deleted, the tee hasher always opens at stream start, and claim checking has exactly one moment — pre-EOF — for every family (identity-family hashing never waits on len; the check timing becomes a question the Algorithm answers, not the offer). The recorded consequence, a sentence the ADR carries explicitly rather than by implication: an unmeasured remote stream buffers at its own layer before offering, forever — empty under the known architecture (the VFS server is local to its consumers; server-to-server sync is entry-holder to entry-holder). The put_unknown machinery itself is already derivation-generic save the put.rs:360 hardcode; restart-reuse for identity members is the permitted simple implementation (correct today, single-pass-capable if ever measured worth it).

D-5 [the litmus] — VFS admission is one byte and one ADR. The self-check the conversation applied to D-1 and D-2: when the VFS consumer's algorithm arrives, its entire ask of this crate is a registry byte under its own ADR (F-3's named door) — no wire changes, no facade changes, no new methods. Any design that demands a per-consumer touch beyond that is the defect class re-entering.

D-6 [what ADR-020 owns, updated] — the invariant statement (the addressable classes of the facade and the wire are both exactly the registry; the crate never picks a derivation for its caller); the delivery-form asymmetry statement (D-4's principle line, with its recorded consequence); F-2's process record verbatim (the door inversion; the gate validated bytes-against-record while nothing validated record-against-invariant — the invariant above is the durable fix: the next gate has something to push against); the wire shapes (digest fields as tag-prefixed key hex everywhere — fetch/stat/have/delete/put; the put offer's key hex + len both mandatory; the put response echoing the key so the caller addresses what landed in the vocabulary it offered); the put handler changes (ingress, tee hasher parameterization, store-arm routing to the one seam, the deleted post-check arm); the facade unification and the put.rs:360 fix; the record corrections (ADR-018 §Decision 2's pointer into ADR-020; ops-surface.md and the ops module docs drop the blob-only scoping; store-api.md's write path restated around the one seam). The store layer otherwise takes no changes — F-3's scope note stands verified.

Decomposition seams (handles for the wave session, not a decomposition): the ADR-020 draft is the gate — nothing lands ahead of it, and D-1 makes this the crate's first wire-format commitment, so the first wire-format ADR discipline applies. Behind it the work splits along natural module boundaries: the key-hex wire encoding + ingress + schemas (src/ops/wire.rs and the ops specs; no facade dependency) → the facade seam unification (put.rs, the facade re-exports, store-api.md) → the put handler and response shapes (src/ops/put.rs; depends on both) → the pinning tests (POC-X's ops_gap substance re-homed into this repo's ops tests) → the record corrections riding the ADR's acceptance commit.

What this review asks for, in one line each

# Ask When Blocking?
F-1 Family-explicit payloads under the unchanged op names (ingress + schema + put offer/store arm across fetch/stat/have/delete/put; no new ops) ADR-020 + implementation session yes — the resting state is a half surface; the wire has zero consumers now, which is exactly why the cost is at its lifetime minimum
F-2 The §Decision 2 finding recorded in ADR-020's context (the door-inversion; the gate-validated-the-bytes-not-the-record note) inside ADR-020 no — record only
F-3 The wire speaks registry vocabulary (algorithm-named digests), never git vocabulary inside ADR-020 yes — this is the non-welding constraint made structural
F-4 The put-seam unification decision (derivation-explicit public put; family-named method folded or kept as wrapper) ride ADR-020 or its task no — shape question, cheap now, more expensive after the first real caller

Resolutions of record (2026-10-18 — the section above; the ask texts stay as the historical record): F-1 → D-1 + D-4 (family-explicit payloads under the unchanged op set; the wire form decided as tag-prefixed key hex, the wire put offer's claim made mandatory). F-2 → D-6 (carried into ADR-020's context verbatim). F-3 → D-1 (registry vocabulary in the strongest form — the wire speaks the frozen registry-byte encoding itself). F-4 → D-2 (folded, not wrapped: put_git_object and the canonical default both removed; one put_as seam).

Disposition space

Per the operator's framing: findings and recommended shapes only. The conversation half of the addressing session has happened (the derived-shape section; the operator participated and the decisions are of record). What remains is record-shaped and mechanical: the drafting session opens OQ-20 (§Decision 2's door, resolving into ADR-020), drafts the ADR from D-1 through D-6 — the decided direction, not F-3's candidates — and the implementation round lands along the decomposition seams, or dispositions any item with a pointer to where its record lives. The blast radius is at its floor today: two known consumers approaching (alkgit active, a second waiting on solidification) and a published-but-consumerless wire surface, so the rework is paid here rather than amplified into consumer code and publish churn — and a store layer this review verified needs nothing.

Closed 2026-10-18, same day: the drafting step happened — ADR-020 is accepted and OQ-20 resolved by it (the disposition as recorded in this file's frontmatter). The review's ask table above is fully dispositioned: F-1/F-2/F-3 inside ADR-020; F-4 likewise (the D-2 fold). The implementation round — not this review — is what remains, and it gates on nothing here.