Caching: a lent registry never widens what the passed entry declares; docs state every refusal

The cacheless written test now holds on the entry passed: the claimant's compatible ids must all be listed by the written entry of its id.
CHANGELOG and WriteRecord's registry doc describe the refusal through another registered type's listed id, the condition on own drift, and the Shared caveat. Not yet built or tested here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-27 09:18:30 +01:00
commit f744817a15
3 changed files with 49 additions and 3 deletions

View file

@ -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 same types does (`WriteDocument` with no registry lends `CultDocumentRegistry.Shared`; `WriteRecord` without one takes the entry it writes), and owns the record only when that type is the entry it writes and reads it whole. Refused: a renamed entry named like the written schema but listing another registered type's id is refused as a cache refuses it; and a record another registered type claims, under that type's current id or an old id it lists, is refused and left in place. Unchanged from 2.0.1 and the earlier builds of this release: a record the written entry's own type reads whole under drift is owned and restamped. Because a registry-less `WriteDocument` reads `Shared`, loading a second type with the written schema's name makes the claim ambiguous and the write is refused (it can only refuse more, never own more); pass a registry to pin the answer.
- A cacheless `WriteDocument` or `WriteRecord` picks the type that reads a stored record the way a cache registering the same types does (`WriteDocument` with no registry lends `CultDocumentRegistry.Shared`; `WriteRecord` without one takes the entry it writes), and owns the record only when that type is the entry it writes and reads it whole. Refused: a renamed entry named like the written schema but listing another registered type's id is refused as a cache refuses it; and a record another registered type claims, under that type's current id or an old id it lists, is refused and left in place. Also refused: a record under an id nobody registered whose stored entry lists another registered type's id ahead of the written one's, and a `WriteRecord` record under an id the passed entry does not list when a lent registry's entry for the same schema id does (the lent registry never widens what the passed entry declares). As in 2.0.1, a record the written entry's own type reads whole under drift is owned and restamped, but only when no other registered type is picked first: a lent registry type or a `Shared` type that lists the record's id, or whose id the stored entry lists first, takes it and the write is refused. Because a registry-less `WriteDocument` reads `Shared`, any type loaded there can do this, not only a second type with the written schema's name (that case is ambiguous and refused); a wider registry can only refuse more, never own more. 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<string, string> durable, IEnumerable<CultStoredDocument> observed, IEnumerable<CultForeignRecord> foreign)` in place of `(IReadOnlyCollection<CultPersistedRecord>, IEnumerable<CultStoredDocument>)`. A store kind outside the library lists the record versions it holds and passes its foreign records.