- 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.
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. |
|
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/fetchanswersBLOB_MISSING;blobs/statanswers the miss;blobs/haveanswerspresent: falsefor the entry the very node holds;blobs/putcannot land one even offered: the tee hasher hard-codes the canonical blob derivation (src/ops/put.rs:333) and the store arm callsstore.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:
-
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.
-
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. -
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_DIGESTwith 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).
The recommended shape (for the addressing session to re-derive — not adopted as-is)
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:
- ADR-020 — the family-explicit payloads under the unchanged op
set. The op family is closed (F-3's hard edge): one
fetch, onestat, onehave, onedelete, oneput— 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).
- The store layer takes no changes. Keys, verification, GC,
fleet,
statsemantics, and the sweep already serve family keys; the entire gap is insrc/ops/plus the facade's put-name (F-4). The already-frozen key bytes are untouched. - 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.rsshape); the addressing task re-homes its substance into this repo's ops tests. - 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.
- 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.