From a056918be6dbeb8a0a09327133b7a6356b46811a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 23 Oct 2026 01:44:57 +0200 Subject: [PATCH] CHANGELOGs list the measured public surface against 2.0.1 (apicompat strict): members on their owning types, Id members in Mesh, Networking and Geometry, Rust SchemaConflictError, cultnet-rs register The Unity list no longer calls MarkFlushSucceeded removed and places HeaderFor and the CacheBackingStore members where apicompat finds them. cultnet-rs gains a CHANGELOG. The store-composition contract moves DecodeDocument to the codec and states that a call's registry serves that call only. Docs only. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultcache-rs/CHANGELOG.md | 4 ++++ packages/cultcache-ts/CHANGELOG.md | 2 +- packages/cultnet-rs/CHANGELOG.md | 12 ++++++++++++ .../Contracts/cultcache-store-composition.md | 17 +++++++++++------ unity/org.gamecult.cultlib/CHANGELOG.md | 16 +++++++++++----- 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 packages/cultnet-rs/CHANGELOG.md diff --git a/packages/cultcache-rs/CHANGELOG.md b/packages/cultcache-rs/CHANGELOG.md index f73df370..f15fcee2 100644 --- a/packages/cultcache-rs/CHANGELOG.md +++ b/packages/cultcache-rs/CHANGELOG.md @@ -11,6 +11,10 @@ only in git history. - A single-file store written as `cultcache.store.v3` is read and written: a record that holds an element id (an owned list element with an `[CultElementId]` member, carried through every write) marks the store v3, and a v3 store is carried through a rewrite with its ids. A runtime from before this change refuses a v3 store by name. - A write over a file the reader refuses (a zero-byte file, a truncated store, a stamp this runtime does not read) refuses with `CultCacheStoreUnreadable` and leaves the file as it is. A whole-store write used to replace such a file. This reverses 0.3.1, where `push_all` replaced a file with no readable stamp (healing it); the stamp rule now has one owner, the reader every write asks under the lock, and it accepts v1 and v3 (0.3.1 refused v3). +### Added + +- `SchemaConflictError`, the typed refusal of a write the catalog cannot describe: records of different types under one schema id, or an arrived schema and a registered one that share an id and disagree on the schema name. A whole-store write (`push_all`) refuses with it, names the schema id, the names involved and a record key, writes nothing and leaves the file as it is. A caller tells it apart with `error.downcast_ref::()`. + ## [0.3.1] ### Fixed diff --git a/packages/cultcache-ts/CHANGELOG.md b/packages/cultcache-ts/CHANGELOG.md index ca6b87cc..87e1d1d0 100644 --- a/packages/cultcache-ts/CHANGELOG.md +++ b/packages/cultcache-ts/CHANGELOG.md @@ -10,7 +10,7 @@ All notable changes to `@gamecult/cultcache-ts` are recorded here, following - A single-file store written as `cultcache.store.v3` is read and written: a record that holds an element id (an owned list element with an `[CultElementId]` member, carried through every write) marks the store v3, and a v3 store is carried through a rewrite with its ids. A runtime from before this change refuses a v3 store by name. - A single-file store refuses a zero-byte file as `StoreUnreadableError`; only a path with nothing at it is an empty store. A header this runtime does not read is refused, and the refusal echoes the header only as `cultcache.store.v`. - A write over a file the reader refuses (`StoreUnreadableError`, from `push`, `pushAll` and `delete`) refuses and leaves the file as it is. A flush used to replace such a file. A write set cannot merge with a disk it cannot read, so the file is repaired or removed by its owner before the store writes again. -- This reverses the 0.14.2 `pushAll` fix in two ways. A full `pushAll` over a file with no readable stamp no longer replaces it (healing it): the reader refuses it, and the file is left as it is. And a `cultcache.store.v3` stamp is read, not refused. The stamp rule has one owner, `refuseUnreadFormat`, asked only by the one reader that every write consults under the lock. +- This reverses the 0.14.2 `pushAll` fix in two ways. A full `pushAll` over a file with no readable stamp no longer replaces it (healing it): the reader refuses it, and the file is left as it is. And a `cultcache.store.v3` stamp is read, not refused. The stamp rule has one owner, `refuseUnreadFormat`, asked only by the one reader that every write consults under the lock. The reader judges the stamp from the header before it decodes the body, so a newer store is refused by name whether or not its body is whole. - A dangling symbolic link at the store path is an I/O error with code `ENOENT`, not an empty store. ### Added diff --git a/packages/cultnet-rs/CHANGELOG.md b/packages/cultnet-rs/CHANGELOG.md new file mode 100644 index 00000000..d12dc3ed --- /dev/null +++ b/packages/cultnet-rs/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to `cultnet-rs` are recorded here, following +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Earlier releases are recorded +only in git history. + +## Unreleased + +### Breaking + +- `CultNetDocumentRegistry::register` returns `Result<&mut Self>` where it returned `&mut Self`. A binding that claims a document type or a schema id another binding already holds is refused with `cultcache_rs::SchemaConflictError` (naming the new binding's schema id and both document types) and nothing is bound; binding a type to the schema id it already holds still replaces that binding. Callers chain `register(...)?`. +- `CultMeshCdnChunkRef` gains a public `id` field, the decimal text of `offset` (the reference's element id, slot 4). A manifest written before ids reads `""`; encoding writes the slot. diff --git a/src/GameCult.Caching/Contracts/cultcache-store-composition.md b/src/GameCult.Caching/Contracts/cultcache-store-composition.md index ea67142d..7bb33a58 100644 --- a/src/GameCult.Caching/Contracts/cultcache-store-composition.md +++ b/src/GameCult.Caching/Contracts/cultcache-store-composition.md @@ -351,17 +351,22 @@ no serialization; a CultMath encoding, if added, uses this shape. A single-file store file can be read and written without a cache, through public members of `SingleFileBackingStore` (for example `SingleFileMessagePackBackingStore`): `ReadStoreSnapshot()`, `WriteRecord(record, catalogEntry, holdsIds, registry)`, -`WriteDocument(key, document, storedAt, registry)` and `DecodeDocument(key, payload, registry)`. +and `WriteDocument(key, document, storedAt, registry)`. Decoding a stored payload needs no file, so it is on the codec, not the store: +`CultCodec.DecodeDocument(key, payload)` (`CultCacheMessagePack.CreateCodec(registry)` makes the codec). - `ReadStoreSnapshot()` reads the file once, under the store's gate, and returns the durable snapshot (records, schema catalog, format stamp), or null when nothing is at the path. It throws `CultStoreUnreadableException` for a file the reader refuses. - `WriteRecord` writes one record under the store's lock: the record replaces the one at its key, every other record and catalog entry is copied forward as stored, and the file is replaced atomically. It takes a payload the caller already encoded, so a payload it cannot read leaves the header as the file has it (`holdsIds` null). -- `WriteDocument` writes one typed document as a cache write does: it plans element ids, encodes the document with the - registry, and stores it through `WriteRecord`. A failed write gives the minted ids back. -- `DecodeDocument` decodes a payload read from the store as a cache load does: it mints the ids a load mints and refuses what - a cache load refuses. +- `WriteDocument` writes one typed document as a cache write does: it plans element ids, encodes the document as `T` (the type + its schema id and ids are taken from; a derived instance is stored as `T`) with the registry, and stores it as `WriteRecord` + does. A failed write gives the minted ids back. +- `CultCodec.DecodeDocument` decodes a payload read from the store as a cache load does: it mints the ids a load mints and + refuses what a cache load refuses. It reads no file and holds no state. + +The `registry` argument of `WriteRecord` and `WriteDocument` serves that one call: the store attaches it under its gate for the call +and keeps none, so the same call decides the same header on a fresh instance and on one that has served other calls. They share everything with a cache's store and nothing is re-implemented beside it: one reader (the stamp is judged before the body, and a file the reader refuses is refused by a read and by a write alike), the store's lock, the atomic replace, and the @@ -370,7 +375,7 @@ keeps whatever the file already says). Ownership by declaration binds a cacheles the written catalog entry does not own or list as compatible is refused with `CultSchemaConflictException`, and the file is left as it was. Element-id planning (`CultElementIds`) is internal to the library: a caller never plans or marks ids, the store does. A store attached to a cache refuses `WriteRecord` and `WriteDocument` (write through the cache, whose staged writes land through the -same private write) and `DecodeDocument` (read through the cache) with `InvalidOperationException`. CultMesh's single-file document helpers (`CultMesh.WriteSingleFileDocument`, +same private write) with `InvalidOperationException`. CultMesh's single-file document helpers (`CultMesh.WriteSingleFileDocument`, `ReadSingleFileDocument` and their payload forms) are built on these members and nothing else of the store. ## Runtime status diff --git a/unity/org.gamecult.cultlib/CHANGELOG.md b/unity/org.gamecult.cultlib/CHANGELOG.md index ff8d0767..f976c3a7 100644 --- a/unity/org.gamecult.cultlib/CHANGELOG.md +++ b/unity/org.gamecult.cultlib/CHANGELOG.md @@ -14,17 +14,23 @@ Releases as 3.0.0 (major). - 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 is under a schema id the writer does not declare is refused with `CultSchemaConflictException`, and the file is left as it was. A cache declares through its registry; a cacheless CultMesh write declares through the catalog entry it writes. - 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. -- `SingleFileBackingStore`'s abstract and protected surface changed for store kinds outside this library: `SerializeSnapshot` and `DeserializeSnapshot` are replaced by `SerializeStore`, `SerializeRecord`, `SerializeCatalogEntry` and `ReadStore`, and `HeaderFor`, `HeaderForWrite`, `Foreign`, `SetForeignRecords`, `RefuseUndeclared`, `RefuseUndeclaredOnDisk`, `StagedChecks`, `StageChecks`, `JudgeChecks` and `RefuseRemovalOfUndeclared` are new protected members a store kind implements or calls. `MarkFlushSucceeded` and the `Judging` hook are removed. +- 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`, `ForeignRecords`, `SetForeignRecords`, `Undeclared`, `RefuseUndeclared`, `RefuseUndeclaredOnDisk`, `RefuseRemovalOfUndeclared`, `Refuse`, `SameStoredIdentity`, `VersionOf`, `StoredAtOver`, `Overwrites`, `StagedChecks`, `StageChecks` and `JudgeChecks`. `MarkFlushSucceeded` is unchanged. - `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. - `GameCult.Mesh`: `AsSchemaAlias()` is removed from `CultMeshDocumentHandle` and `CultMeshCollectionHandle`. Read the handle by the schema you mean. -- The element types of owned lists in `GameCult.Mesh` (`CultMeshBodies`, `CultMeshCdn`, `CultMeshDiscoveryService` and `CultMeshEntityPrefabs`) and `GameCult.Networking` (`CultWitnessArtifactBundle`) gain a public string `Id` member with `[CultElementId]`, so their lists merge by identity. Code that matched those elements positionally by shape must set or read `Id`. +- The element types of owned lists gain a public string `Id` member with `[CultElementId]`, so their lists merge by identity: in `GameCult.Mesh`, `CultMeshBodyDescriptor`, `CultMeshCdnChunkRef`, `CultMeshDiscoveryCandidateDocument`, `CultMeshEntityPrefabComponent`, `CultMeshEntityPrefabAssetRef` and `CultMeshEntityPrefabNode`; in `GameCult.Networking`, `CultWitnessContractPin`, `CultWitnessArtifactEntry` and `CultWitnessTimingEntry`; in `GameCult.Geometry`, `CultGeometryDomainNode`, `CultGeometryFeatureClaim` and `CultGeometryContributionRow`. Code that matched those elements positionally by shape must set or read `Id`. - `CultNetDocumentRegistry.Register` refuses a binding that claims a document type or a schema id another binding already holds, with `CultSchemaConflictException`; binding a type to the schema id it already holds still replaces that binding. -- `InternalsVisibleTo GameCult.Mesh` is removed from `GameCult.Caching`: CultMesh's single-file document helpers use the public `ReadStoreSnapshot`, `WriteDocument`, `DecodeDocument` and `WriteRecord` below. +- `InternalsVisibleTo GameCult.Mesh` is removed from `GameCult.Caching`: CultMesh's single-file document helpers use the public `ReadStoreSnapshot`, `WriteDocument`, `WriteRecord` and `CultCodec.DecodeDocument` below. ### Added -- `SingleFileBackingStore.ReadStoreSnapshot()` reads a store file once, under the store's lock, with the reader a cache's store opens with, and returns the durable snapshot or null when nothing is at the path. `SingleFileBackingStore.WriteRecord(record, catalogEntry, holdsIds, registry)` writes one record without a cache: every other record and catalog entry is copied as stored, the file is replaced atomically, and the header is decided as a flush decides it. It refuses on a store attached to a cache. -- `SingleFileBackingStore.WriteDocument(key, document, storedAt, registry)` writes one typed document without a cache, as a cache write does: it plans element ids, encodes the document and marks the store when it holds an id; a failed write gives the minted ids back. `SingleFileBackingStore.DecodeDocument(key, payload, registry)` decodes a stored payload as a cache load does. Both refuse on a store attached to a cache. `CultElementIds` stays internal. +- `SingleFileBackingStore.ReadStoreSnapshot()` reads a store file once, under the store's in-process gate, with the reader a cache's store opens with, and returns the durable snapshot or null when nothing is at the path. `SingleFileBackingStore.WriteRecord(record, catalogEntry, holdsIds, registry)` writes one record without a cache: under the store's lock every other record and catalog entry is copied as stored, the file is replaced atomically, and the header is decided as a flush decides it. It refuses on a store attached to a cache. +- `SingleFileBackingStore.WriteDocument(key, document, storedAt, registry)` writes one typed document without a cache, as a cache write does: it plans element ids, encodes the document as `T` (the type its schema id and ids are taken from, also for a derived instance) and marks the store when it holds an id; a failed write gives the minted ids back. It refuses on a store attached to a cache. The `registry` of `WriteRecord` and `WriteDocument` serves that call only; a store keeps none. +- `CultCodec.DecodeDocument(key, payload)` decodes a stored payload as a cache load does (`CultCacheMessagePack.CreateCodec(registry)` makes the codec). It reads no file and holds no state. `CultElementIds` stays internal. +- `CultPersistedStoreSnapshot.FormatV3` names the element-id store header. +- `CultDocumentAttribute.CompatibleSchemaIds` and `CultDocumentDescriptor.CompatibleSchemaIds`: the schema ids besides its own a document type claims, so a record stored under one of them loads as that type. +- `CultStoredDocument.StoredSchemaId` (the schema id the record carries in its store, which may be an older one) and `CultStoredDocument.HoldsIds`. +- `CultCache.MintElementIds()` rewrites, once, the records of a store written before element ids whose ids exist only in memory, and returns how many it rewrote. +- `CultElementIdAttribute` declares an element id (derived from a named member when given), `CultForeignRecord` is a record a store carries untouched because no registered type reads it, and `CultElementIdException`, `CultSchemaConflictException`, `CultStoreUnreadableException` and `CultWriteConflictException` are the refusals named above. - A store written by 1.0.x with a record-reference list in a slot still opens (from 2.0.1). ## [2.0.1] - 2026-10-22