Files
alkblobs/tasks/facade-put-as-seam.md
T
glm-5.3-flash 441b2aa836 feat(store): the one derivation-explicit put seam — put_as alone (ADR-020 §Decision 5, wave 14)
- put_as(algorithm, len: Option<u64>, stream) is the facade's sole
  public write surface; put, put_git_object, put_known, and the
  matches! family restriction are deleted outright (no shim, 0.x)
- put_in_batch takes the algorithm parameter (batch, algorithm, len,
  stream); the one seam routes every member
- Digest::derive, Key::derive, Digest::from_git_oid_hex deleted;
  explicit construction is Algorithm::derive / Key::from_bytes over
  the physical encoding
- the put_unknown fleet arm honors its threaded algorithm parameter
  (the put.rs:360 re-hardcode), pinned by a live pg-lo fleet test
- the unknown-length arm retained per §Decision 3; its identity-family
  form pinned (buffer + staged routes land the known-length key)
- the seam-guard test transforms: blob algorithms are legal at the
  seam; put_as lands the canonical blob derivation (git vectors)
- caller sweep: ~250 sites across src/, tests/, and the conformance
  columns; the ops put handler's store arm rides the seam's blob arm
  canonically until the put-handler task (§Decision 4)
- fuzz key_encoding target: the from_git_oid_hex arm dies with the
  constructor; seed regeneration a no-op diff; corpus-replay gate green

Verified: cargo test (default), cargo test --all-features server-less
and against the live pglo-poc harness, both clippy variants (-D
warnings), cargo fmt --check, cargo doc --no-deps, the fuzz
corpus-replay gate.
2026-10-18 22:29:16 +00:00

14 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
facade-put-as-seam The facade's one derivation-explicit put seam — put_as alone; the fossil surfaces deleted completed
wire-key-hex-ingress
broad medium project implementation
wave-14
review-003
adr-020
store
facade
api-break

Description

ADR-020 §Decision 5's facade unification (the second seam of §Decision 9's order; depends on the wire task because it deletes Digest::from_git_oid_hex, whose last src/ callers the wire task removes). The facade's write surface becomes put_as(algorithm, len: Option<u64>, stream) → (Key, Pin) alone:

  • put and put_git_object are deleted outright — no shim (0.x, zero in-repo callers, one out-of-repo POC composition; every publish the family-named method survived would teach the approaching consumers the wrong shape). The matches! restriction (src/store/put.rs:159-164) simply ceases to exist — convention 12 satisfied by construction.
  • put_in_batch takes the algorithm parameter — put_in_batch(batch, algorithm, len, stream); the member put runs the one seam.
  • The one seam's semantics (ADR-020 §Decision 5, verbatim contract): algorithm is a required input. Preamble-carrying members (the blob family) hash preamble-then-content — the known-length path's BlobHasher::new(algorithm, len) preamble initialization as today. Identity-family members take the preimage verbatim with len as the stored-length check (the no-preamble hasher arms as today). The unknown-length arm (None) is retained per §Decision 3: preamble-carrying members restart the derivation pass over preamble + staged bytes (today's shape); the identity-family restart-reuse is the permitted simple implementation — correct today, single-pass-capable if ever measured worth it. No canonical default is blessed anywhere.
  • The put.rs:360 re-hardcode fixed: put_unknown's pg-lo consolidation arm passes Algorithm::GitBlobSha256 while ignoring its own threaded algorithm parameter — the threading exists and is honored (the defect was invisible only because the sole caller happened to pass the constant).
  • Digest::derive deleted (src/hashing.rs:270 — the silent canonical constructor): explicit construction is Algorithm::derive(algorithm, content, len).
  • Key::derive folds with it (src/keys.rs:50 — the same silent canonical constructor one wrapper up). The ADR's §Decision 5 sentence names Digest::derive; §Decision 1's invariant decides the Key form — it is an address-forming surface with a hidden default ("never a hidden default"), and a Key-level re-wrap of Algorithm::derive adds nothing over Key::from(Algorithm::derive( ...)). Its callers are tests plus one #[cfg(test)] helper (src/local.rs:899) — the sweep is cheap. Flag to the operator at the gate if this reading is contested: it is a decomposition-time application of the invariant, not a new decision.
  • Digest::from_git_oid_hex deleted (src/hashing.rs:315 — the length-routed constructor, the last length-implied address-forming surface): family-explicit construction from oid bytes is Key::from_bytes over the physical encoding.
  • The caller sweep — every in-repo caller of the deleted surfaces: src/store/tests.rs, src/store/fleet.rs (the family fleet tests' put_git_object calls → put_as), src/store/construct.rs and src/store/sweep.rs and src/local.rs (Key::derive test helpers), src/facade.rs and src/hashing.rs and src/keys.rs (Digest::derive in tests), tests/conformance/rows.rs (the family rows' put_git_object → put_as), tests/conformance/sqlite_column.rs (Key::derive), tests/git_interop.rs (put_git_object → put_as; the seam-guard test put_git_object_rejects_blob_algorithms transforms — the blob algorithms are now legal at the seam; its replacement pins that put_as(GitBlobSha256, ...) lands under the canonical blob derivation, i.e. the old put's behavior is the seam's blob arm), src/ops/tests.rs (Digest::derive sites).
  • The public surface (src/lib.rs re-exports + the crate-root //! docs): put_as/put_in_batch exported; put/ put_git_object/from_git_oid_hex gone. put_as's rustdoc is contract text (store-api.md §Write path's shape — the full spec restatement is record-corrections'; the rustdoc must not contradict).
  • The fuzz key_encoding target (fuzz/shared/src/key_encoding.rs): the Digest::from_git_oid_hex arm dies; Digest::derive sites become Algorithm::derive; the target's doc comment's surface list updated. Seeds: run python3 fuzz/gen_fuzz_seeds.py and record the disposition — the generator may reference the deleted constructor's input shapes (the 40/64-hex oid batteries); per the corpus policy the committed seeds regenerate if the input shape changed, and a no-op diff is the expected outcome if they don't. The corpus-replay gate (cargo test --manifest-path fuzz/shared/Cargo.toml) must stay green either way.

Reads unchanged, by principle (ADR-020 §Decision 6): get/stat/ list/has/delete/read_range signatures are untouched — the asymmetry is the design; this task does not re-audit the read path.

Acceptance Criteria

  • The public write surface is put_as + put_in_batch (with the algorithm parameter) alone; put, put_git_object, Digest::from_git_oid_hex, Digest::derive, Key::derive are absent from the crate (grep-clean across src/, tests/, and the fuzz sidecar's compiled surface)
  • put_as covers every registered algorithm: the blob family preamble arm and the identity-family verbatim arm byte-identical to the pre-round behaviors (the conformance family rows and the git-interop CLI-validated tests green unchanged in substance)
  • The unknown-length arm retained: put_as(algorithm, None, ...) for a preamble member restarts over preamble + staged bytes; for an identity member the restart-reuse form lands the same key — pinned by test
  • The put.rs:360 re-hardcode fixed — the threaded algorithm is honored (pinned by a test that drives the pg-lo unknown arm under a non-default algorithm, or by the threading's direct assertion if the arm is env-gated)
  • The fuzz key_encoding target compiles against the new surface; the seed disposition recorded; the corpus-replay gate green
  • Gates green: the full AGENTS block — cargo test (default), cargo test --all-features server-less and against the live pg harness (ALKBLOBS_PG_HOST=localhost, ALKBLOBS_PG_PORT=15432, ALKBLOBS_PG_USER=postgres, ALKBLOBS_PG_PASSWORD=poc, ALKBLOBS_PG_DB=blobs — the pglo-poc container; server-less runs skip cleanly), both clippy variants, cargo fmt --check, cargo doc --no-deps, the fuzz corpus-replay gate

References

  • docs/architecture/decisions/020-family-explicit-wire-payloads.md (§Decision 5 — this task's contract; §Decision 1 — the invariant that decides the Key::derive fold; §Decision 3 — the retained unknown-length arm; §Decision 6 — reads unchanged)
  • docs/reviews/003-family-wire-gap-review.md (F-4; D-2, D-4)
  • docs/architecture/store-api.md (the write path's WHAT — record corrections restates it)
  • src/store/put.rs (put :98, put_git_object :153, the matches! :159, put_known :222, put_known_as :241, put_unknown :350, the :360 hardcode); src/hashing.rs (:270 Digest::derive, :315 from_git_oid_hex); src/keys.rs :50 (Key::derive); src/lib.rs; tests/conformance/rows.rs; tests/git_interop.rs; fuzz/shared/src/key_encoding.rs; fuzz/gen_fuzz_seeds.py

Notes

The seam's landed shape (src/store/put.rs): put_as(algorithm, len: Option<u64>, stream) → (Key, Pin) is the one public write method — put, put_git_object, and the internal put_known deleted outright (the matches! restriction died with them; convention 12 by construction). put_in_batch takes (batch, algorithm, len, stream); both arms route through it, not through a deleted surface anywhere. The rustdoc is the store-api.md §Write path contract verbatim in substance (both arms, the three numbered unknown-length rules, pin-before-publish, kv-only rejection, fleet forms, the stat semantic).

The put.rs:360 re-hardcode fixed: put_unknown now passes its threaded algorithm parameter to fleet_put_pglo_unknown (the threading existed; the defect was invisible because put was the only caller and passed the constant). Pinned by a new pg-lo fleet test (fleet_pglo_unknown_arm_honors_the_threaded_algorithm, env-gated but driven live at the gate): put_as(GitObjectSha256, None, ...) over the declared threshold lands the family's plain-hash oid — under the old hardcode the key would carry the blob tag and preamble derivation.

The unknown-length arm's retention pinned per ADR-020 §Decision 5/§Decision 3: unknown_length_identity_family_put_lands_the_known_length_key (new, src/store/tests.rs) — put_as(GitObjectSha1, None, ...) lands the known-length key on both the buffer (kv) and staged-bytes (large) routes; the preamble member's restart-over-preamble + staged bytes was already pinned by unknown_length_put_round_trips_on_both_tiers and the git-interop both-tiers test, unchanged in substance.

The seam-guard test transformed as the task specified: the blob algorithms are now legal at the seam, so put_git_object_rejects_blob_algorithms → put_as_blob_algorithm_lands_under_the_canonical_derivation — put_as under either blob member lands the git-computed known-answer vector (the old put's behavior is the seam's blob arm).

The derivation constructors: Digest::derive, Key::derive, and Digest::from_git_oid_hex are gone; explicit construction is Algorithm::derive(algorithm, content, len), and family-explicit construction from oid bytes is Key::from_bytes over the physical encoding ([registry byte] + oid bytes). The interop test file gained one helper (oid_key) implementing exactly that, so every external-oid round-trip leg keeps its shape. The deleted from_git_oid_hex hex battery in src/hashing.rs was replaced by a physical-encoding battery (tag validation, truncation, wrong-tag rejects).

Caller sweep shape: ~250 sites across src/, tests/, and the conformance columns switched mechanically (.put(...) → .put_as(Algorithm::GitBlobSha256, ...), put_git_object calls → put_as(alg, Some(len), ...), Key::derive/Digest::derive → Algorithm::GitBlobSha256.derive(...)), with per-module Algorithm imports added where the sweep introduced the name. The one non-mechanical caller: src/ops/put.rs's store arm now calls put_as(Algorithm::GitBlobSha256, offered_len, reader) — the interim canonical routing until the put-handler task (ADR-020 §Decision 4) parameterizes the tee hasher and store arm per offer algorithm; the no-side-door audit's verb set was refreshed (put → put_as).

fuzz key_encoding target: the from_git_oid_hex arm died with the constructor (HexVerdict/hex_verdict/assert_hex_invariants/ assert_hex_battery removed); Digest::derive sites became Algorithm::GitBlobSha256.derive; Key::derive sites became Key::from(Algorithm::GitBlobSha256.derive(...)); the structured path re-points its string handling at Key::from_bytes (no string parser remains); the target's doc comment's surface list updated. Seed disposition: the generator's output does not depend on the deleted constructor (the hex-form seeds are raw byte inputs either way — the 40/64-hex oid batteries' bytes are unchanged); the regeneration was a no-op diff on the committed corpus (verified, zero working-tree entries), and the corpus-replay gate is green. A comment-only update in fuzz/gen_fuzz_seeds.py records the disposition ("hex-ish string forms ... no string parser accepts these anymore; they persist as raw key-parser inputs for corpus stability").

Doc sweep in code: lib.rs root docs, src/store/mod.rs and src/store/fleet.rs module docs, fleet/family test doc comments and names (fleet_kv_put_as_lands_through_the_joint_tx, fleet_pglo_put_as_lands_through_the_consolidation_tx), put_git_object_put_get_round_trip → put_as_family_member_round_trips_under_the_oid_key in tests/git_interop.rs — no code doc names the deleted surface as current. README/store-api/hashing-and-keys restatement stays record-corrections' scope; store-api.md already describes put_as (the rustdoc was written against it and does not contradict).

Verification (all green): cargo test (default: 156 lib / 54 conformance mem+sqlite columns / 7 git-interop / 2 readme); cargo test --all-features server-less (clean skips: 343 / 125 / 7 / 3) and against the live pglo-poc harness (343 / 125 / 7 / 3 — the pg columns ran for real); both clippy variants with -D warnings; cargo fmt --check; cargo doc --no-deps; the fuzz corpus-replay gate. One transient parallelism flake under the first full harness run (cancelled_acquire_cannot_strand_the_advisory_lock, a wall-clock cadence test untouched by this task) passed on the full re-run and in isolation — the recorded run-discipline posture, not a regression.

Summary

The facade's write surface is one derivation-explicit seam, put_as(algorithm, len: Option<u64>, stream) → (Key, Pin), with put_in_batch taking the algorithm parameter: put, put_git_object, Digest::derive, Key::derive, and Digest::from_git_oid_hex are deleted outright (grep-clean across src/, tests/, and the fuzz sidecar's compiled surface), the matches! family restriction ceases to exist, the unknown-length arm is retained with its semantics pinned for both derivation families, the put.rs:360 re-hardcode is fixed and pinned by a live pg-lo fleet test, and the fuzz key_encoding target compiles against the new surface with a no-op seed-regeneration disposition. Every registered algorithm writes and reads byte-identically to the pre-round behaviors (the conformance family rows and the CLI-validated git-interop tests are unchanged in substance), and the full AGENTS gate block is green.