From b95a1fa7bb25b04a3e731f6487e8745461b3d0a0 Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 11 Nov 2026 23:35:36 +0000 Subject: [PATCH] Rust leaf: ownership map and changelog say what the cache judge and the admission check refuse, and that store-level writers trust the claimed id Co-Authored-By: Claude Opus 5.5 --- docs/write-ownership-map.md | 21 +++++++++++++-------- packages/cultcache-rs/CHANGELOG.md | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/write-ownership-map.md b/docs/write-ownership-map.md index ddf9aa67..6c6dd94b 100644 --- a/docs/write-ownership-map.md +++ b/docs/write-ownership-map.md @@ -55,14 +55,19 @@ What this establishes: - Retype as remove plus add already works in the cache (P4, P5); the cut adds no new primitive. - Directory store: its own judge (`DirectoryMessagePackBackingStore.cs:287-306`) asks `Registry.Owns` for every key, upserts included. It must take the same per-key rule. -- TypeScript, Python (and Rust before its leaf cut): records are keyed by `(type, key)` (TS `#entryId(definition.type, key)`, - `cult-cache.ts:323`; Python sorts and stores by `(type, key)`). - A write of A at k never meets B's record: both live at k (P1). Neither carries a foreign record; a store holding an - unregistered type does not load (P2). Rust, after the leaf cut, keeps its `(type, key)` map but its one judge - (`refuse_retype`) refuses a second registered type at a held key on every write path, pull and SoA load, whatever schema - id an envelope claims, and carries an unregistered type's records as foreign; it walks the vector's 12 rows. So no leaf has a "write meets another type's record" path to fix. Bringing the - ruling to the leaves first needs C#'s key identity (one record per key) and C#'s foreign carry, which are parity changes - of the leaves' store model, not a write-ownership rule. Owned by the new follow-up `variants:follow_up:leaf-key-identity-parity`. +- TypeScript and Python: records are keyed by `(type, key)` (TS `#entryId(definition.type, key)`, `cult-cache.ts:323`; + Python sorts and stores by `(type, key)`). A write of A at k never meets B's record: both live at k (P1). Neither carries + a foreign record; a store holding an unregistered type does not load (P2). So neither has a "write meets another type's + record" path to fix: bringing the ruling to them first needs C#'s key identity (one record per key) and C#'s foreign + carry, parity changes of their store model, not a write-ownership rule (follow-up `variants:follow_up:leaf-key-identity-parity`). +- Rust is that follow-up's first leaf cut (`cut-leaf-key-identity-rs`). The cache keeps its `(type, key)` map; one judge + (`refuse_retype`) refuses another registered type at a key the cache holds, or any write at a key a store carries + foreign, on every write path, pull and SoA load, comparing types, not the ids envelopes claim. At a key the cache does not + hold (a fresh key, or one another writer filled after this cache's pull) the single-file store's commit guard judges by + the stored and claimed schema ids, so an envelope claiming another registered type's id is refused earlier, at every + envelope admission (`refuse_claimed_id`, the pull's own `resolve_registered_type` lookup); an id naming no registered type + passes. Store-level writers called directly (`push`, `insert_entry_if_absent`, compare-and-swap) trust the claimed id. + The cache carries an unregistered type's records as foreign and walks the vector's 12 rows. - Interop (unproven): a Python store holding A and B at k fails C# decode first on payload encoding (Python's lambda-defined document wrote JSON payloads), so what C# does with two records at one key was not reached. diff --git a/packages/cultcache-rs/CHANGELOG.md b/packages/cultcache-rs/CHANGELOG.md index a6c52977..05a4c057 100644 --- a/packages/cultcache-rs/CHANGELOG.md +++ b/packages/cultcache-rs/CHANGELOG.md @@ -17,7 +17,7 @@ only in git history. - A single-file store holds one record per key. The v1 and legacy readers refuse a store with two records under one key (`CultCacheStoreUnreadable`, the record `Undecodable`): the store does not open, and a write over it refuses and leaves the file. A consumer that holds such a file must repair it with the runtime that wrote it before opening it here. - Every single-file writer (`push`, `push_all`, `insert_entry_if_absent`, the compare-exchange and snapshot-append family, and the cache's puts and batches) refuses to put a second type under a key the store holds, with `SchemaConflictError` naming the key and both types, writing nothing. Retyping a key is a delete, then an add (one batch may hold both). A write owns the held record when its schema id equals the id the held record is stored under, as C# `WrittenOwns` asks; a type renamed under a stable id therefore replaces its own record, and a write of another type name under another id is refused (`push` of the same type name under another id still replaces the record, as before; `compare_and_swap_entry`, and a `push_all` batch holding one type under two ids, refuse it). Registered types here declare no compatible ids, so equality is the whole test. Writers used to leave two records under the key. - `compare_and_swap_entry` over a single file refuses an in-place retype (expected `A/k`, replacement `B/k` under another schema id) with the same `SchemaConflictError`, as `compare_exchange` does, where it used to retype the record and return `true`. A lost race still returns `false` first. -- A cache holds one record per key. `put`, `put_envelope`, `put_raw_envelope` and a batch of a second type under a key the cache holds, or a key a store carries foreign, are refused with `SchemaConflictError` even with no store (a memory-only cache used to hold both), whatever schema id the envelope claims: another registered type's record is never owned, an envelope stamped with the held type's id included; a refused write or pull changes nothing. Two stores holding one key refuse the pull, naming the key and both types, and so does one store holding two records at a key that resolve to one type. A store holding a record no registered type claims now loads (it used to refuse with "No schema is registered for persisted entry type") and the record is carried, through `put_prepared_batch` too, which lays foreign records back byte for byte; `delete` at a foreign key is refused. `load_soa` refuses a second type at a key or a record at a foreign key, and still refuses an unregistered type. The redb stores still key by `(type, key)`, and their own writers (`push`, `insert_entry_if_absent`, compare-and-swap) still put a second type at a held key: a cache now refuses to pull such a redb store with `SchemaConflictError`, where it used to load both records. +- A cache holds one record per key. `put`, `put_envelope`, `put_raw_envelope` and a batch of a second type under a key the cache holds, or a key a store carries foreign, are refused with `SchemaConflictError` even with no store (a memory-only cache used to hold both), whatever schema id the envelope claims: another registered type's record is never owned. `put_envelope`, `put_raw_envelope`, `put_prepared_batch` and `load_soa` also refuse, at any key, an envelope whose schema id names another registered type (the id a pull would resolve the record by), so a stale cache or a fresh key cannot store one type under another's id; an id naming no registered type still passes. Store-level writers called directly (`push`, `insert_entry_if_absent`, compare-and-swap) still trust the claimed id. A refused write or pull changes nothing. Two stores holding one key refuse the pull, naming the key and both types, and so does one store holding two records at a key that resolve to one type. A store holding a record no registered type claims now loads (it used to refuse with "No schema is registered for persisted entry type") and the record is carried, through `put_prepared_batch` too, which lays foreign records back byte for byte; `delete` at a foreign key is refused. `load_soa` refuses a second type at a key or a record at a foreign key, and still refuses an unregistered type. The redb stores still key by `(type, key)`, and their own writers (`push`, `insert_entry_if_absent`, compare-and-swap) still put a second type at a held key: a cache now refuses to pull such a redb store with `SchemaConflictError`, where it used to load both records. - A write of the same type under a held key replaces the record by key, and in one `push_all` the last given of several entries under a key and a schema id is the record, whatever their type names. The redb stores are unchanged and still key by type and key. ### Added