From 723b799668a078fe4f245e929be1c920187ea02e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 27 Oct 2026 18:24:16 +0100 Subject: [PATCH] Caching docs: state exactly what a cacheless write owns and refuses The CHANGELOG bullet, the persistence-contract paragraph and the registry param docs of WriteRecord and WriteDocument now list the two ways a record is owned and every refusal the code makes, including a stored entry that lists another registered type's id ahead of the written one's and a call that writes no entry. The registry-less Shared read is described as it is today. Co-Authored-By: Claude Opus 5.5 --- .../Contracts/cultcache-persistence-format.md | 17 ++++++++++------- src/GameCult.Caching/CultCache.cs | 4 ++-- unity/org.gamecult.cultlib/CHANGELOG.md | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md index 87653470..10fb9fc8 100644 --- a/src/GameCult.Caching/Contracts/cultcache-persistence-format.md +++ b/src/GameCult.Caching/Contracts/cultcache-persistence-format.md @@ -206,13 +206,16 @@ The rule is checked again where the write lands, under the store's lock: a write whose durable record is not owned, because another writer rewrote it after this cache loaded, is refused typed, and the record is reloaded as the store holds it, read-only when a type can read it. This holds for every write through the store, the store's own `Push`, `Delete` and -`CommitBatch` included. A CultMesh single-file write asks the same rule: it owns the ids its written entries declare (their own ids and the ids they -list compatible) and an id whose publishing entry its claimant reads whole (no persisted slot unread). The claimant is picked by -the one precedence over the written entries and the call's registry, a written entry being the only description of its schema id -(the registry's type for that id is never asked), and the record is owned only when the claimant is a written entry. So a cacheless -write is never more permissive than its declaration, nor than a cache registering the registry's other types beside the written -ones: a record another registered type claims is refused, under that type's current id or an old one it lists, and a registry only -adds refusals. With no registry the written entries are the only claimants. It is refused the same way otherwise. +`CommitBatch` included. A CultMesh single-file write (`WriteDocument`, `WriteRecord`, or `PushAll` on an unattached store) asks the same rule: it owns the ids its +written entries declare (their own ids and the ids they list compatible) and an id whose publishing entry its claimant reads whole (no persisted +slot unread). The claimant is picked by the one precedence over the written entries and the call's registry, a written entry being the only +description of its schema id (the registry's type for that id is never asked), and the record is owned only when the claimant is a written +entry. A registry adds claimants and declares nothing, so it only adds refusals; with none the written entries are the only claimants. Refused: +a record another registered type claims, under that type's current id or an old one it lists; a record whose stored entry lists another +registered type's id ahead of the written one's, whatever name the stored entry carries; a record two registered types list compatible or two +registered versions of the written schema could read; a record the picked written entry reads only by dropping persisted slots; and the +removal of any stored record by a call that writes no entry. A registry-less `WriteDocument` reads `CultDocumentRegistry.Shared`. It is +refused the same way otherwise. A record that resolves to no local type is a foreign record, for example one of a type this build does not have, or one renamed under a new schema name without declaring its old id. A store never destroys or relabels it: the store carries it diff --git a/src/GameCult.Caching/CultCache.cs b/src/GameCult.Caching/CultCache.cs index b0771655..ce276a0c 100644 --- a/src/GameCult.Caching/CultCache.cs +++ b/src/GameCult.Caching/CultCache.cs @@ -4484,7 +4484,7 @@ namespace GameCult.Caching /// The record to store. /// The schema catalog entry for the record's schema id. /// Whether the record's payload holds element ids, or null when the caller cannot tell. - /// The registry the payload was encoded with. Given, the write decides the header for copied records that registry can read, and its types also claim stored records as a cache registering them would, so it can only refuse more; stays the only description of its own schema id and the registry's type for that id is never asked. Omitted, it decides nothing for copied records and the one entry is the only claimant. It serves this call only: the store keeps no registry. + /// The registry the payload was encoded with. Given, the write decides the header for copied records that registry can read, and its other types also claim stored records as a cache registering them would; it declares nothing, so it can only refuse more. stays the only description of its own schema id and the registry's type for that id is never asked. Owned are a record whose id declares (its own or one it lists compatible) and a record the entry reads whole when it is the picked claimant; refused are a record another registered type claims, one whose stored entry lists another registered type's id ahead of the entry's, and one that two registered types list or two versions of the schema could read. Omitted, it decides nothing for copied records and the one entry is the only claimant. It serves this call only: the store keeps no registry. /// The call holds the store's gate while it runs. User code it runs must not enter a cache or another store; that throws . /// The store is attached to a cache; write through the cache. /// The file at the path is not a store this runtime can read. @@ -4507,7 +4507,7 @@ namespace GameCult.Caching /// The record key the document is stored under. /// The document to store. /// The stored-at stamp, or null for now. - /// The registry that describes and encodes the document, or null for . Its types claim stored records as a cache registering it would: the document's type owns a record its entry does not declare only when it is the claimant and reads it whole; another registered type's record, or one whose stored entry lists another registered type's id that type claims first, is refused; a wider registry (Shared when none is passed) can only refuse more. It serves this call only: the store keeps no registry. + /// The registry that describes and encodes the document, or null for . The document type's entry is taken from it and is the only description of that schema id. Its other types claim stored records as a cache registering them would, and it declares nothing, so it can only refuse more: the document's entry owns a record whose id it declares (its own or one it lists compatible), and a record it reads whole when it is the picked claimant; refused are a record another registered type claims, one whose stored entry lists another registered type's id ahead of the entry's, and one that two registered types list or two versions of the schema could read. A wider registry (Shared when none is passed) refuses at least as much. It serves this call only: the store keeps no registry. /// The call holds the store's gate while it runs. User code it runs (property getters while the document is encoded, constructors while a copied record is read) must not enter a cache or another store; that throws , carried by the encoder's wrapper when the code ran inside it. /// The store is attached to a cache; write through the cache. /// The file at the path is not a store this runtime can read. diff --git a/unity/org.gamecult.cultlib/CHANGELOG.md b/unity/org.gamecult.cultlib/CHANGELOG.md index f7da30e2..f031e599 100644 --- a/unity/org.gamecult.cultlib/CHANGELOG.md +++ b/unity/org.gamecult.cultlib/CHANGELOG.md @@ -13,7 +13,7 @@ Releases as 3.0.0 (major). - A store is written in the format its content needs, and CultLib 2.0 refuses the newer ones by name. A single-file store holding a document variant is written as `cultcache.store.v2`, one holding an element id as `cultcache.store.v3`; a directory store holding an element id is written as `cultcache.store.v5` (`v4` otherwise). A store is marked only when a record actually holds an id or a variant. Upgrade readers before writers: a 2.0 reader that opens such a store is refused, not misread. - Writes are write sets. A flush or commit replaces the records it staged and copies every other record forward as stored, under the store's lock, instead of rewriting the whole store from the cache's memory. A record the cache does not know is kept, and an untouched record keeps a dropped member's slot through every write (never reuse a dropped slot; see the schema-compatibility contract). - A write or removal at a key whose stored record the writer does not own is refused with `CultSchemaConflictException`, and the file is left as it was. A writer owns a record whose schema id it declares (owns or lists compatible) and a record its type reads whole under compatible drift: that record is writable and is restamped under the type's own id on write, as in 2.0.1. A record a type reads only by dropping persisted slots stays read-only until its id is declared compatible, which accepts the loss (this differs from 2.0.1, which shed the slots silently). A cache owns through its registry; a cacheless CultMesh write owns through the catalog entry it writes. -- A cacheless `WriteDocument` or `WriteRecord` picks the type that reads a stored record the way a cache registering the call's registry would, except that the entry it writes is the only description of its schema id: `WriteDocument` takes it from the registry (`CultDocumentRegistry.Shared` when none is passed), `WriteRecord` writes the entry it is given, and without a registry that entry is the only claimant. A record is owned when the written entry declares its id (its own or one it lists compatible), or when the picked type is the written entry and reads the record whole. Refused, with the file left as it was: a record another registered type claims, under that type's current id or an old id it lists; a stored entry named like the written schema that lists another registered type's id; and a record two registered versions of the written schema could read. As in 2.0.1, a record the written entry's own type reads whole under drift is owned and restamped. A registry only adds refusals: it never changes what the written entry declares, so a registry whose type shares the written schema id neither widens nor narrows the passed entry. A registry-less `WriteDocument` reads `Shared`, so any type loaded there can refuse the write; pass a registry to pin the answer. +- A cacheless `WriteDocument`, `WriteRecord` or unattached-store `PushAll` owns a stored record in two ways only: the entries it writes declare the record's schema id (an entry's own id, or one it lists compatible), or the type picked to read the record is one of the written entries and reads it whole (no persisted slot unread). The type is picked as a cache registering the call's registry would pick it, except that a written entry is the only description of its schema id: `WriteDocument` takes its entry from the registry (`CultDocumentRegistry.Shared` when none is passed), `WriteRecord` writes the entry it is given, and a registry's own type for that id is never asked. A registry adds claimants and declares nothing, so it can only refuse more; without one the written entries are the only claimants. Refused, with the file left as it was: a record another registered type claims, under that type's current id or an old id it lists; a record whose stored entry lists another registered type's id ahead of the written one's, whatever name the stored entry carries; a record that two registered types list compatible, or that two registered versions of the written schema could read; a record the picked written entry would read only by dropping persisted slots; and the removal of any stored record by a call that writes no entry (a `Delete` then `PushAll`). As in 2.0.1, a record the written entry's own type reads whole under drift is owned and restamped. A registry-less `WriteDocument` reads `Shared` today, so any type loaded there can refuse the write; pass a registry to pin the answer. - Registration refuses an owned object-list element type that has no `[CultElementId]` member (a public string with `[Key]`), naming the type and the list. Element ids let a write set merge a list by identity; a list of record references or plain values is a whole value and is not affected. - The abstract and protected surface for store kinds outside this library changed. On `SingleFileBackingStore`, `SerializeSnapshot` and `DeserializeSnapshot` are replaced by the abstract `SerializeStore`, `SerializeRecord`, `SerializeCatalogEntry` and `ReadStore`, and the abstract `SerializePayload(object)` becomes `SerializePayload(object document, Type documentType)`: a store encodes a document as the type it is given. On `CacheBackingStore`, `Judging` is removed, `ToStoredDocument` takes the record's version (`record, version, catalog, deserialize, out report`), and these protected members are new for a store kind to implement or call: `HeaderFor`, `HeaderForWrite`, `Foreign`, `SetForeignRecords`, `Undeclared`, `RefuseUndeclared`, `RefuseUndeclaredOnDisk`, `RefuseRemovalOfUndeclared`, `Refuse`, `SameStoredIdentity`, `VersionOf`, `StoredAtOver`, `Overwrites`, `StagedChecks`, `StageChecks` and `JudgeChecks`. `MarkFlushSucceeded` is unchanged. `CacheBackingStore.ForeignRecords` is new and public: the records a store holds under a schema id its cache does not declare. - `CultCommitRequest.Expected` is now `IReadOnlyList<(CultRecordKey Key, string? Version)>` (it was a key with a schema id and a stored-at), `CultCommitRequest.Checks` is new, and `ConditionsHold` takes `(IReadOnlyDictionary durable, IEnumerable observed, IEnumerable foreign)` in place of `(IReadOnlyCollection, IEnumerable)`. A store kind outside the library lists the record versions it holds and passes its foreign records.