feat(store): the put seam — two internal paths, the three numbered rules, Batch, kv-only rejection

- `put(len: Option<u64>, stream: impl AsyncRead + Unpin + Send) -> (Key, Pin)`:
  known-length one-pass flow; unknown-length buffer-to-threshold flow with
  the mid-stream fresh-stage flush (ADR-003 option (d), three numbered rules:
  flush-and-continue, derivation over the eventual stored artifact as a
  whole via buffer form and StagingCursor read-back form, zero residue on
  stream error)
- staged-put seam amended (operator sign-off): `put_staged()` keyless
  handout, `StagingCursor::commit(self, key)` publishes under the
  store-core-resolved key — the canonical key completes at stream end and
  the wave-1 handout shape could not express the commit-rename flow
- pin-before-publish rides the arbitration lock on both paths and both
  tiers; dedup re-puts refcount the existing entry
- Batch seam: `Store::batch()` + `put_in_batch()`, members publish pin-
  before-publish, group pins drop together on `Batch::commit` (new public
  verb) or drop
- kv-only rejection (ADR-012 §6.2): known-length over-threshold rejects
  immediately (stream never read), unknown-length at mid-stream overflow;
  zero residue; error carrier `Io(String)` (named design point)
- declared-len mismatch rejects `Verification` zero-residue (named design
  point); stream shape generic-no-dyn (named design point)
- incremental BlobHasher (pinned byte-equal to Algorithm::derive);
  AnyStaging cursor dispatch; git-interop put-path leg against
  git hash-object; eleven put-path behavior tests

Verification: cargo test (default/--features mem/--all-features) green;
clippy --all-targets -D warnings green in all three feature shapes;
cargo fmt --check green; cargo doc --no-deps at the 24-warning baseline.
This commit is contained in:
glm-5.3-flash committed 2026-10-11 23:00:14 +00:00
1 parent 889be4cada
commit 539dabcbf7
8 files changed
+1723 -105

No files matched your search

+36 -23
View File
@@ -108,6 +108,17 @@ pub(crate) trait ReadCursor: Send {
/// on discard with zero residue (the stage-hygiene invariant,
/// store-api.md §Write path; exact-count tested per engine).
///
/// **The key resolves at commit, not at handout (named design point,
/// the wave-2 amendment of the wave-1 handout shape).** The canonical
/// key is the derivation over preamble + content, and it completes at
/// stream end — after the staging cursor already exists (put paths
/// stream content once into the cursor while hashing; ADR-003's put
/// paths). So `commit` carries the store-core-resolved key and the
/// stage's identity before that point is only the engine-internal
/// stage file/tx (unique per `put_staged` handout). Publish is the
/// CAS first-writer-wins entry under that committed key exactly as
/// [`Backend::put`].
///
/// Every engine implements the staged form: kv-tier bodies are the
/// degenerate in-memory stage committed through the engine's whole-value
/// row write (the store core selects the form by tier policy, ADR-010
@@ -121,15 +132,18 @@ pub(crate) trait StagingCursor: ReadCursor {
/// "restart the stream" requirement anywhere).
async fn append(&mut self, chunk: &[u8]) -> Result<(), Error>;
/// Publish the entry: the stage's bytes become the CAS entry in the
/// Publish the entry: the stage's bytes become the CAS entry under
/// `key` — the store-core-resolved canonical key (the derivation
/// completes at stream end; see the type's design point) — in the
/// engine's put shape (commit-rename on `local`, the in-tx `lowrite`
/// batch on pg-lo, the row write on kv bodies). This is the staged
/// form's terminal step — the store core never re-uses the cursor
/// after the call. On failure the stage is **already discarded** —
/// zero residue by the stage-hygiene invariant; there is no
/// post-failure discard to drive (the consuming call took the
/// cursor).
async fn commit(self) -> Result<(), Error>;
/// batch on pg-lo, the row write on kv bodies). CAS first-writer-wins:
/// a losing race against an existing entry is dedup (`Ok`), never a
/// failure. This is the staged form's terminal step — the store core
/// never re-uses the cursor after the call. On failure the stage is
/// **already discarded** — zero residue by the stage-hygiene
/// invariant; there is no post-failure discard to drive (the
/// consuming call took the cursor).
async fn commit(self, key: &[u8]) -> Result<(), Error>;
/// Discard the stage: every non-commit path converges here, zero
/// residue. Driven explicitly by the store core (stream error,
@@ -189,11 +203,12 @@ pub(crate) trait Backend: Send + Sync {
async fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Error>;
/// The staged put's cursor handout (ADR-010 §1). The store core
/// selects the form before content flows: the large tier's
/// stage-then-commit-rename contract, kv engines' degenerate
/// in-memory stage. See [`StagingCursor`] for the contract every
/// body converges on.
async fn put_staged(&self, key: &[u8]) -> Result<Self::Staging, Error>;
/// selects the form before content flows (the cursor's identity
/// before commit is the engine-internal stage only — the canonical
/// key resolves at [`StagingCursor::commit`], the type's design
/// point). See [`StagingCursor`] for the contract every body
/// converges on.
async fn put_staged(&self) -> Result<Self::Staging, Error>;
/// Unconditional delete — arbitration has already happened above the
/// trait (the facade decides and owns the refusal: `GcRefuse` never
@@ -442,7 +457,6 @@ mod tests {
}
struct TestStaging {
key: Vec<u8>,
staged: Vec<u8>,
rows: Arc<Mutex<BTreeMap<Vec<u8>, Vec<u8>>>>,
fail_commit: bool,
@@ -464,13 +478,13 @@ mod tests {
Ok(())
}
async fn commit(mut self) -> Result<(), Error> {
async fn commit(mut self, key: &[u8]) -> Result<(), Error> {
if self.fail_commit {
self.staged.clear();
return Err(Error::Io("engine commit failure (test double)".to_owned()));
}
let value = std::mem::take(&mut self.staged);
self.rows.lock().entry(self.key.clone()).or_insert(value);
self.rows.lock().entry(key.to_vec()).or_insert(value);
Ok(())
}
@@ -508,9 +522,8 @@ mod tests {
Ok(())
}
async fn put_staged(&self, key: &[u8]) -> Result<Self::Staging, Error> {
async fn put_staged(&self) -> Result<Self::Staging, Error> {
Ok(TestStaging {
key: key.to_vec(),
staged: Vec::new(),
rows: self.rows.clone(),
fail_commit: self.fail_commit,
@@ -603,7 +616,7 @@ mod tests {
let backend = TestBackend::default();
let key = b"\x01staged-key".to_vec();
let mut staging = backend.put_staged(&key).await.unwrap();
let mut staging = backend.put_staged().await.unwrap();
staging.append(&CONTENT[..8]).await.unwrap();
staging.append(&CONTENT[8..16]).await.unwrap();
staging.append(&CONTENT[16..]).await.unwrap();
@@ -620,7 +633,7 @@ mod tests {
assert_eq!(bytes, CONTENT, "staged artifact readable as a whole");
assert_eq!(digest, slice_digest(CONTENT));
staging.commit().await.expect("commit publishes");
staging.commit(&key).await.expect("commit publishes");
assert!(backend.has(&key).await.unwrap(), "entry published");
let cursor = backend
@@ -650,7 +663,7 @@ mod tests {
let backend = TestBackend::default();
let key = b"\x01discard-key".to_vec();
let mut staging = backend.put_staged(&key).await.unwrap();
let mut staging = backend.put_staged().await.unwrap();
staging.append(b"residue").await.unwrap();
staging.discard().await;
@@ -676,9 +689,9 @@ mod tests {
};
let key = b"\x01failure-key".to_vec();
let mut staging = backend.put_staged(&key).await.unwrap();
let mut staging = backend.put_staged().await.unwrap();
staging.append(CONTENT).await.unwrap();
let err = staging.commit().await.unwrap_err();
let err = staging.commit(&key).await.unwrap_err();
assert!(matches!(err, Error::Io(_)), "engine media failure");
assert!(