Commit graph CultLib/tests/GameCult.Caching.Tests
Author SHA1 Message Date
Meta Vens
bad511f5ac Merge CultCache R1 (f789c8d1) into C2a: one reader per runtime, R1's failure taxonomy
R1's rules win where both sides decided the same question: a zero-byte file is
not a store (refused as unreadable in every runtime), only nothing at the path is
empty, a dangling link is an I/O error, refusals never echo stored values and echo
a header only as cultcache.store.v<digits>, writes report Rejected, NotReplaced
and ReplacedNotDurable. C2a's rules win on what it added: the v3 element-id
header, variants, whole-file proof, typed unreadable errors in C#, TypeScript and
Python. Rust keeps R1's CultCacheStoreUnreadable{path, kind}; C2a's
StoreUnreadableError is deleted. The readability manifest's zero-byte row is
refuses x4.

Not yet built or tested: verification follows in the next commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:41:35 +02:00
Meta Vens
15ccdbb953 Test: a staged write a commit landed is no longer staged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:52:42 +02:00
Meta Vens
a9741dbf57 Tests: pin the staged-key rule, the multi-key refusal and the carried record's stamp; a pull no longer clears staged keys
A pull runs only on a clean store, and a store is clean exactly when nothing is staged, so the pull's clear was redundant and is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:47:47 +02:00
Meta Vens
86eea40bce One owner for the foreign-record refusal: it reaches the cache, names every key, and refuses only what the cache wrote
CacheBackingStore.RefuseForeign now decides, for both store kinds, which written keys a foreign record refuses, drops the
store's staged change and the cache's copy of each through the Loaded path a pull uses, and throws one conflict naming every
refused key. The single-file store tracks the keys staged since its last load or write, as the directory store already did, so
a clean copy of a record another writer made foreign is carried, not refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:34:09 +02:00
Meta Vens
bc15dba81b Test: the owner's-id case of the moved-record lay-back publishes the owner's catalog entry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:00:52 +02:00
Meta Vens
0a98004bc4 Tests: pin the Stryker survivors in the lay-back and the directory refusal
The file's record moves when only its storedAt or only its schema id differs from the loaded one; a
staged removal is refused and dropped with the rest; a refusal leaves staged what it did not refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:58:50 +02:00
Meta Vens
b0f7d59c96 Directory store: a refused foreign overwrite leaves the pending set with its failed write
WriteGeneration left the refused key in the dirty and deleted sets, so every later generation
refused it. The refusal is now one check before any staging, shared by flush and commit, that
removes the refused keys from the pending sets. Tests cover both store kinds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:51:54 +02:00
Meta Vens
c246e0c5ae Single-file store: lay back an unowned record only while the file still holds it as loaded
A whole-view write laid back the loaded bytes of an unclaimed record over whatever the file held,
reverting its owner's newer write and moving storedAt backwards. It now lays back only while the
file holds the loaded (schemaId, storedAt); a newer record is carried as the file has it and a
removed one is not written back. The v3 header is decided by records that are written as the cache
holds them, so a laid-back id-less record no longer marks the store. A document changed in place is
written as a write of the record, not dropped. A refusal drops the store's copy of the refused
records, so it no longer refuses every later write.

Does not build with the directory store untouched? It builds; the directory twin follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:51:54 +02:00
Meta Vens
904c00ac81 Test: pin the Stryker survivors in the foreign-record and lay-back cut
- The lay-back fixture gains an older entry listing the id ahead of the
  one that owns it, and a test where another writer re-describes the id
  after the load: a whole view (flush and commit) lays back the entry it
  loaded. Kills the survivors in Publisher's own-id preference, the
  laid-back entry's precedence in the written catalog, and the commit's
  choice of that catalog.
- A store whose file is gone lists no foreign records.
- The registry's public resolution still refuses an unclaimed id, naming
  the schema and the id (was NoCoverage).
- A directory load retried after a writer's commit lists each foreign
  record once.
- Deleted LaidBack.ExceptWith(replaced): equivalent, since an upserted key's
  new entry already carries what the cache would be told.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:59:52 +02:00
Meta Vens
a11d30fce2 C#: a whole-view write lays back a record under an id its type does not own
C2a F2, widened by Self: records and catalog entries a runtime does not own
survive its writes byte for byte. The schema-identity probe showed a C#
rewrite restamping every other runtime's record to its sha256 id and
dropping their catalog entries.

- The single-file store remembers, per key, a record it loaded under an id
  its type neither owns nor declares compatible (resolved through the
  catalog's schema name or an entry pointing at a local id), with the entry
  that published it. While the store still holds the entry it loaded, a
  whole-view write (flush or unconditional commit) lays that record back
  with its id, storedAt, bytes and catalog entry. A write of the record
  itself still stores it under the type's own id.
- A record published by an entry under the type's own id is this schema's
  and is re-encoded as before (the registered entry wins its own id).
- A laid-back record's load-minted element ids stay in memory only, so
  MintElementIds still rewrites it.
- The rolling-deploy tests lose their premise: an older version's whole
  view no longer sheds a newer record's member. They now pin that it lays
  the record back and the newer cache's condition holds; the A-B-A test
  keeps F6's defence through an explicit older write.
- Contract: the stamp and the lay-back rule in the persistence format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:20:11 +02:00
Meta Vens
7dd19baac3 Test: a store holding a schema the cache does not register opens and carries it
The corrupt-store test pinned the old refusal of an unknown schema at open.
Under C2a F2 that record is foreign: the store opens, lists it, and opening
still rewrites nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:52:08 +02:00
Meta Vens
05221dae74 C#: a record no registered type claims is carried untouched, and a write over it is refused
C2a F2, operator: "CultCache never destroys or relabels a record it doesn't
understand; keeps it byte-for-byte, refuses writes that would overwrite it."

- The registry's claim decision (Claimant) is split out of resolution. It
  never decodes or compares shapes, so a store classifies a record from its
  header. The last branch, "No local CultCache schema matches", now means
  foreign instead of a refused load.
- Both store kinds keep a foreign record out of the cache and list it in
  CacheBackingStore.ForeignRecords (key, schema id, catalog name, storedAt).
- Single-file store: a whole-view write lays back every foreign record the
  file holds now, with its catalog entry, and keeps a marked header while it
  carries one (it cannot see their ids). A staged write, a commit (onto the
  file or whole view) and a whole view over a key the file holds as foreign
  refuse with CultSchemaConflictException naming the key and id. A merge
  judgement never decodes a foreign record, and one that replaced a held
  record takes it away.
- Directory store: a foreign record's page is never read; a generation that
  would replace or remove one refuses before any page is written.
- Contracts: hard rejection no longer lists "no compatible local schema
  candidate"; the persistence format says what a foreign record is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:50:15 +02:00
Meta Vens
c996ae7892 Caching: a single-file refusal echoes a header only in its known shape
RequireSingleFileFormat echoed any header string. It now names one only in
the shape cultcache.store.v<digits> and otherwise gives its byte length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:35:20 +02:00
Meta Vens
2d7a3c78eb Caching tests: an empty store file is refused; a missing file or [0x90] is empty
Pins the C# side of the cross-runtime rule the Rust, Python and TS runtimes
now share. C# already refused a zero-byte file (the snapshot reader runs out
of bytes at the array header); this test keeps it that way. Its exception is
still EndOfStreamException, an IOException: that overlap belongs to R3's C#
work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:05:15 +02:00
Meta Vens
98999997ce CultCache: a second type for one schema is refused, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." C# now holds one type per schema, as TypeScript,
Python and Rust already do.

- Delete IsExactWireAlias and the branch that registered an identical second
  type by CLR type only. A second type with the same schema name and version
  is refused with CultSchemaConflictException naming both types and the
  claimed id, whether it is identical, declares other compatible ids, or has
  other members. The untyped DuplicateSchemaRegistration refusal and the
  separate SchemaIdClaimedTwice path collapse into that one refusal.
- Owner-beats-lister (batch 10) is unchanged: one type owns an id, others may
  list it as compatible.
- The per-name version index stays; it serves v1/v2 of one schema name, not
  aliasing.
- Tests: the alias-registration tests are replaced by one refusal test over
  identical, re-declared and re-shaped second types, in both registration
  orders.
- cultcache-store-composition.md drops the AsSchemaAlias stale-protection note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:15:57 +02:00
Meta Vens
5b86bc9faf Test: a flush storing a record under a new id gives it a later storedAt when the file no longer holds it
Defends the id half of the whole-view restamp rule on its own: the record C# loaded
under v1's id is gone from the file when a v2 cache flushes it back, so there are
no stored bytes to compare, and only the changed id says it is a new store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:32:57 +02:00
Meta Vens
9629aa119b C#: a type listing an id registers beside the type that owns it; the owner wins
Batch 9 refused any overlap between two types' ids, which forbade the migration
CompatibleSchemaIds exists for: v2 declares v1's id while v1's class stays
registered (Soul F8). The rule is now owner beats lister. A type owns its own
schema id; any number of types may list an id as compatible beside its owner,
registering in either order. A record under an id resolves to its owner, else to
the one type listing it. An id no type owns and several list names no single
type, so a record under it is refused, typed (CultSchemaConflictException naming
the id and the listers), where it resolves: refusing at registration would
depend on whether the owner had registered yet.

A second owner of one id is still refused at registration, typed. A schema id
hashes the schema's name and version, so the only second owner is a type of the
same name and version, which the name-and-version check already meets; the
separate own-id lookup could never fire and is gone.

Tests: an owner and a lister register in both orders, the owner's id resolves to
the owner, the lister's own id to the lister, and without the owner to the
lister; two listers register in both orders, a record under their id is refused
typed with no owner and resolves to the owner with one, registered before or
after them; a cache with v1 beside v2 reads each version's records as its own
type, in both registration orders. The emitted-fixture note says why a fixture
is emitted now that v1/v2 pairs no longer need hiding from the shared registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:26:06 +02:00
Meta Vens
188d4ad88d C#: a whole-view write that stores a record differently mints it a later storedAt
A condition names a record by (stored schema id, storedAt), and that name must
say which bytes are stored. A single-file whole-view write (plain flush, or an
unconditional commit) re-encoded every record under its own type's id with the
storedAt it loaded, so the pair could name two byte strings: v1 shed a member
keeping storedAt, a second v2 cache restored v2's id keeping it again, and a v2
cache holding the original bytes committed its condition (Soul F6, A-B-A).

Now such a write stores a record under a later storedAt whenever it stores it
differently: under another id than it was loaded under, or with other bytes than
the file holds at that storedAt (a record written with the ids it minted at
load, for one). A record stored exactly as the file holds it keeps its storedAt.
The store's entries and the cache's take the id and storedAt the write gave them.

Tests: the A-B-A through commits and through plain flushes, with Expect and
ExpectUnchanged; a flush that re-encodes a record restamps the cache that
flushed (a condition holds straight after, SM12); a record stored unchanged keeps
its storedAt through commits and flushes; persisting load-minted ids gives a
later storedAt. The rolling-deploy test asserted v1's rewrite kept storedAt; it
now asserts a later one, since that rewrite is a new store of the record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:25:22 +02:00
Meta Vens
fdb8a60af9 Test: conditions on records loaded under an old id, before a rewrite and after a directory pull
A plain record and a variant loaded under a declared old id pass Expect and
ExpectUnchanged while the file is unchanged, and again after a whole-view commit
rewrites them (the variant case pins the id surviving resolution). A directory
store pull rereads a record whose manifest id alone changed, so a condition on
it holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:48:40 +02:00
Meta Vens
5607d9703d C#: the directory store's loaded id is its manifest's; the variant fixture names itself
A directory-store record's identity on disk is its manifest entry, which is what
a commit condition compares; the page holds its own copy of the id. A load now
takes StoredSchemaId from the manifest, so a manifest rewritten under an older
id (AConditionOnARecordUnderADeclaredOldIdHoldsInTheDirectoryStore) is the id
the condition expects. Canonicalisation had hidden the page and manifest
disagreeing in that fixture.

The whole-view test's variant overrides its name: a variant may not share its
base's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:52:15 +02:00
Meta Vens
4182c2502b C#: one registered type carries each schema id; a second claimant is refused typed
Registration refuses a type any of whose ids (its own, or one it declares
compatible) another registered type already owns or declares, with
CultSchemaConflictException naming the id, both schema names and both CLR types.
TypeScript already refuses this (cult-cache.ts); before, C# kept whichever type
registered first (Soul F4). An exact wire alias must now also declare the same
compatible ids: a type differing only in its declaration had it dropped when it
registered second (H4), and is refused the same way.

Exact aliases that agree in every respect still register by type, and a record
under their shared id still resolves to the first registered (H3). Removing that
would delete CultMesh's same-schema aliases; it is left for an operator ruling.

Tests: both registration orders for a shared compatible id, an owned id declared
by another type, and a same-schema type with a different declaration; an alias
with the same declaration still registers. The registry tests use the shared
EmittedDocumentTypes helper in place of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:52 +02:00
Meta Vens
5c8af219f9 C#: a commit condition compares the id a record was loaded under with the file's, exactly
Each stored document carries StoredSchemaId: the id its record sits under in its
store, the one it was loaded under until a write puts it under its type's own id.
Expect, ExpectUnchanged, the reload and merge identity checks compare
(StoredSchemaId, storedAt) with the store's bytes exactly. CanonicalSchemaId is
deleted: canonicalising the disk id let an older version's whole-view rewrite
(same storedAt, restamped to its own id, a member shed) pass a newer cache's
condition (Soul F1), and failed every condition on a record resolved by name or
through a file entry listing the local id (F3). A whole-view write marks every
record it wrote as stored under its type's id, in the store and the cache.

Tests: Soul's rolling-deploy probe as AWholeViewRewriteByAnOlderVersionFails...
(Mismatch for Expect and ExpectUnchanged, nothing derived lands, a pull shows the
rewrite and the retry commits); F3's H1, H1b, H2 pass Expect and ExpectUnchanged
when nothing changed; conditions after a whole-view commit hold for a plain
record and a variant. BareDeck is emitted at run time (EmittedDocumentTypes) so
the shared registry never sees it beside DeclaringDeck; the CanonicalSchemaId
tests and their Claimant fixtures go with the function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:40 +02:00
Meta Vens
b295bf1688 C#: pin the directory store's conditions, drop the merge-judging canonicalisation
A condition on a record under a declared old id is tested against the directory manifest too, and
a duplicate declared id is listed once. The merge-judging comparison keeps its plain schema-id
equality: a record it re-reads is only judged again, and no test reaches a difference.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:39:11 +02:00
Meta Vens
0cef517d83 C#: a schema name that drifted no longer refuses a record resolved by its schema id
The contract says the name a catalog entry carries is metadata and a schema renamed under a
stable id opens in every runtime that holds the id. The C# shape comparison refused any name
drift, so a record under a declared old id (the entry carrying another name and a different
content hash) or under a renamed stable id could not open. The comparison keeps the slot and
type checks; a record resolved by name has the same name by construction.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:36:43 +02:00
Meta Vens
d589e61f67 C#: a blank declared compatible schema id is dropped, not refused
A type that failed to build a descriptor poisoned the shared registry's discovery of every
loaded document. A blank id is now filtered like the type's own id, and the test declares one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:31:28 +02:00
Meta Vens
186e5b29ae C#: a document type declares the older schema ids its records may sit under
CultDocumentAttribute.CompatibleSchemaIds, as TypeScript and Python already name it. The
registered catalog entry lists the ids after its own, the registry resolves a record under one
to the type (its own id first, then the first registered type that declares it), and a
condition on a record compares schema ids through that resolution, so a record still under a
declared old id counts as the record the cache holds. The cache already stamps every record it
writes with the descriptor's own id; the store writers still change no identity, so a record a
commit does not rewrite stays under the id the file holds it under, published by the registered
entry. Without the declaration such a commit is refused, typed, as before.

Contract and XML docs describe the declaration.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:30:05 +02:00
Meta Vens
9cee5e3f42 C#: no store writer restamps a record; the directory store has one source of arrived entries
Deleted the heuristic that rewrote a record the cache held "unchanged since read" (same key and
storedAt) under the registered id, in the single-file commit and in the directory generation. It
could overwrite a record another writer changed while keeping its storedAt. A commit onto a file
now writes its batch and leaves every other record as the file holds it; a record under an id only
an arrived entry lists is refused, typed, with the file left as it was. Deleted the directory
store's _durableCatalog, a second stale-able source of arrived entries beside the manifest read
under the lease.

Tests: the lost-update probe, the two refusals that replace the restamp tests, the
arrived-overwrites-registered guard in both id orders, the content-hash tie-break against the
compatible-list order, and refusal determinism across record arrival order.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:12 +02:00
Meta Vens
ca0b62023d Writers: drop the same-tier dedupe refusal (unreachable), and pin tie order and the registered lister
Two entries of one tier that share an own id are refused when the id is carried, so a refusal at dedupe could never fire.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:14:33 +02:00
Meta Vens
38ebc2015b Tests: the file-level cases keep the schema name, which the cache requires of a persisted schema under a registered id
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:07:52 +02:00
Meta Vens
01d6055cd0 Tests: C# writer derivation, rename, stale hash, refusals
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:05:03 +02:00
Meta Vens
1a4289cac6 Test: the directory merge test starts from a stale content hash
The directory store's preference for the entry being written was not pinned: nothing distinguished the entry's hash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:11 +02:00
Meta Vens
59f9e9aedb One local schema id in cultmesh-py; catalog writers merge only the same schema; a compatible entry before its owner reads as the owner
cultnet-py's resolve_document_and_schema_id_for_raw_record is replaced by resolve_document_for_raw_record, which resolves the
document only; cultmesh-py keys previous values by the local schema id, so a snapshot under a compatible id or the schema name
reports an update with its previous value. Writers (C#, TypeScript, Python) merge same-id entries only when their schema names match;
another schema's entry is dropped or the record gets a default entry under its own id, and a record's own id is claimed before an
entry that lists it as compatible. The C# single-file writer prefers the entry being written now, as the directory writer does, so
name, content hash and members come from the registered schema and the compatible ids are the union. Vector:
compatible-before-owner-v3. A cultnet-py test's schema path no longer depends on the working directory.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:01:00 +02:00
Meta Vens
2fbdf514ce Test: the registry resolves a persisted id to the entry that owns it
The vector test alone did not distinguish the order on C#: the compatible entry's own list reached the same local schema.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:26:27 +02:00
Meta Vens
bcf6ce5cf6 Test: the directory store's wrong-format refusal is the typed exception naming the manifest
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:22:58 +02:00
Meta Vens
f96a0e7f7a Catalog writers merge entries that share an id; every reader resolves an id by its owner first
Entries that share a schema id are one schema seen by different writers, so the writer lists every id any of them lists, in any
order: C# single-file and directory (CultSchemaCatalogEntry.MergeById), TypeScript and Python. Rust synthesises one entry per id and
has no such merge. An id that one entry owns and another lists as compatible names the owner in Rust, TypeScript, Python and the TS
inspector, as C# already did (own-id-over-compatible-v3). Also: a directory manifest in an unreadable format is the typed refusal
naming it, cultnet-py applies and reports a record under the local schema id, and Rust's error prints its cause once.
Vectors for the Mesh layout discriminator: a nil and an integer at catalog slot 5.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:17:59 +02:00
Meta Vens
2d2fad1f45 Tests: the directory store names its refused manifest and keeps the entry that publishes a compatible id
The two paths the previous batch left unpinned. Replication routing comments in cultnet-py and cultnet-ts say what the payload-based
and binding-based resolution is and is not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:45:31 +02:00
Meta Vens
969d78de39 C#: compatible schema ids publish a schema on open, Mesh takes only the older layout, the refusal names its file
A record whose schema id the catalog lists only as a compatible id now opens (the cache resolved a persisted schema by entry id
alone). CultMesh's fallback to the older catalog-entry reader fires only for a store in that layout, decided by the entry's slot 5,
not for any decode failure, so a current-layout store with a malformed slot is refused as CultCache refuses it.
CultStoreUnreadableException.Path names the refused file. New vectors: compatible-id-only-v3 (accepted by all four) and three
current-layout catalogs with one malformed slot. The contract states the writer's invariant.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:17 +02:00
Meta Vens
67575c4ec4 Store reader: a record whose schema the catalog does not publish is unreadable
C# read such a store on flush and commit and refused it on open, with an untyped InvalidOperationException from the cache. The
store reader now refuses it with CultStoreUnreadableException, so open, flush and commit give one verdict, as Rust, TypeScript
and Python already do. The readability open test builds a cache with a codec, so a variant store opens as it would in use.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:19:35 +02:00
Meta Vens
436eed73e4 Store readability: one typed refusal, one full reader, one store shape
CultStoreUnreadableException (a NotSupportedException) is the one refusal for a store file this runtime cannot read, thrown
by the single-file reader, the rewriting paths and CultMesh's reads, with the cause as the inner exception. The reader is exactly
one array of three slots (or an empty array): a header alone and an extra top-level slot are refused instead of read as empty or
skipped. CultMesh's older-catalog fallback is the same reader with the older entry reader, so it also requires one array.

Vectors: bad-body, header-only, missing-schema, variant-slot, extra-top-slot in the readability set, and a legacy-catalog store
with and without bytes after it for CultMesh. The persistence contract says what is true.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:06:44 +02:00
Meta Vens
e18624f72b Merge origin/main into hands/variants-c2a
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 03:58:11 +02:00
Meta Vens
486244b60f Caching: JSON converter and MessagePack formatter for CultPhasor
The Phacelle merge added the public CultMath value type CultPhasor without
its Caching adapters, failing the every-public-type tests. Register it the
way CultCellular is: {"cos","sin"} objects in JSON (which also gives the
dictionary-key form) and a [cos, sin] array in MessagePack. The adapters are
Caching code and stay MIT; only CultPhasor.cs itself is MPL-2.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:46:29 +02:00
Meta Vens
5d1fc496d7 Caching tests: register the shared valid store's record type
The open verdict for v3-base.msgpack needs a cache that knows vectors.item.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:30:30 +02:00
Meta Vens
4b3c76e418 Caching tests: the open verdict is asserted where the store is attached
AddBackingStore pulls, so a file the reader refuses throws on attach, outside
the assertion. Wrap attach and pull together.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:11:40 +02:00
Meta Vens
4028205ff1 Store readability: one reader decides open, flush, commit and CultMesh writes
A rewrite replaces a file only when the runtime's own reader opens it. C#
drops ReadStoreHeader (a second, header-only verdict that let a flush replace
files the reader refuses) and asks DeserializeSnapshot from open, flush and
commit; the reader now requires exactly one array, so C# refuses bytes after
the store on read as TS and Python do. A zero-byte file is an empty store for
all three paths. CultMesh.WriteSingleFileDocument reads the file it replaces
through the Mesh reader before writing. Rust reads and rewrites through one
classification (trailing bytes refused on read; a non-store first slot must be a
legacy envelope array the reader accepts). TypeScript checks a legacy array with
pullAll's own decoder before replacing it. Python already conformed.

Shared byte vectors and per-runtime verdicts live in
tests/vectors/document-variants-c2a/readability; every runtime's test reads
them. Holds gains second-dictionary-value and second-list-item cases. The
persistence format contract now marks v3 by content and states the readability
rule.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 01:59:28 +02:00
Meta Vens
fd1cf22ffe Pin Holds on every list shape, and drop C#'s redundant first-slot check
CultElementIds.Holds is the one fact a store marks its header by; a test now
drives it directly over a dictionary value, a list of lists, a list under an
unset element and an element, and over unset ids that hold nothing, so a
mutant that skips any of those branches fails.

C# ReadStoreHeader refused a first-slot string without the cultcache.store.
prefix and then ran RequireSingleFileFormat, which refuses every string that
is not v1, v2 or v3 anyway. The prefix check is gone; a non-header string is
refused as a format this runtime does not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:11:13 +02:00
Meta Vens
ea1bd6a797 Every runtime rewrites only a file that is exactly one store
A rewrite read the header of the file it replaces but not whether the file was
a store: C# ReadStoreHeader skipped one value and ignored bytes after it, and
took a non-array value or an array whose first slot is a string that is not a
store header for a headerless legacy file (so did the C# Mesh raw write, which
uses it); TypeScript's pushAll took any non-array value or non-header first slot
for v1; Rust ignored bytes after the array.

The rule, one per runtime: a file is a store on rewrite only when it is exactly
one complete MessagePack array. Bytes after it, a value that is not an array,
and a first slot that is a string but not a header this runtime reads are
refused, and the file is left untouched. An empty file and a legacy envelope
array (first slot not a string, empty included) carry no header and are still
rewritten.

- C#: ReadStoreHeader refuses non-array values, trailing bytes and a non-header
  first string, with a NotSupportedException naming which.
- TypeScript: storeHeader (store-format.ts) decides the rewrite header;
  @msgpack/msgpack's decode already refuses trailing bytes.
- Rust: store_header(bytes, whole) walks with a cursor; the rewrite path asks
  for the whole file, the read paths keep reading as before.
- Python: push_all already reads the whole file (unpackb refuses trailing
  bytes, the legacy decoder refuses anything but a list of maps); pinned by a
  test.

Tests in each runtime run the same file matrix (truncated, trailing, scalar,
map, string first; empty, empty array, legacy), plus a C# unconditional commit
over a store with trailing bytes and the C# Mesh raw write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:01:58 +02:00
Meta Vens
4f742d05c3 C#: a flush refuses to overwrite a store file whose header it cannot read
SingleFileBackingStore.PushAll replaced the file without reading it, so a
truncated store or one in a format this runtime does not read was clobbered,
where Rust, TypeScript and Python refuse. PushAll now reads the header of a
non-empty file under the lock before it writes; a file it cannot read throws
and is left as it was. An empty file and a legacy envelope array carry no
header and are still replaced.

The header is read by the store's codec: SingleFileBackingStore gains a
protected virtual ReadStoreHeader (default: read the whole snapshot), and the
MessagePack store overrides it with
CultDocumentMessagePackSerialization.ReadStoreHeader. An unconditional commit
reads the file it rewrites through the same check.

Doc text: last-writer-wins now says it holds only over a file the writer can
read (CultCache.cs PushAll comment, cultcache-persistence-format.md
"Conditional commit is the only safe multi-process write" and writer rule 4,
cultcache-store-composition.md "A plain flush and an unconditional commit are
last-writer-wins").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:51:47 +02:00
Meta Vens
619e089fa6 C#: HoldsIds is read off the stored form each time a store writes
A record's HoldsIds was decided by its admission and trusted afterwards, so a
document changed after it was committed, or loaded and changed in place,
flushed under the header its admission-time content needed (Soul P7, P10).

HoldsIds now has one owner, the record's stored form, read when asked:
- a plain record: CultElementIds.Holds over its document, a scan that reads
  and refuses nothing;
- a variant: the same scan over each override value, decoded with the codec
  the record was built with (the cache's, or the attached cache's at load);
- a variant built with no codec (a store that loaded it with no cache) cannot
  see its values and counts as holding an id, so a store never writes an
  unmarked header over ids it could not see.

Cut: the admission path that set HoldsIds on plain records, on variants from
their resolved copies, and on flattened records; the internal setter and its
backing flag; IdPlan.HeldIds and IdPlan.HoldsIds. The Mesh typed write reads
Holds off the document it writes. Admission keeps its own id counting
(IdPlan.Count) for IdsInMemoryOnly.

Tests: an id added to a committed or loaded deck after admission marks a
PushAll v3, and taking it away unmarks it; a variant read without a codec
counts as holding ids while the same record read with one does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:46:50 +02:00
Meta Vens
cf16cb7c1f Merge origin/main: CultCache publication Cuts 1-3 and the RUDP hardening
One conflict, at the end of Admit in CultCache.cs. The branch had added the
IdsInMemoryOnly marking of a load's minted ids before Apply; main had turned
the thread's hold into a Hold carrying Changes and Failures and run the
journals right after Apply. Both are kept, in that order: the marking touches
only the records and runs no user code, then Apply mints the Sequences, the
hold records the changes, and the journals run under the gate.

Id planning and Project do not collide. Ids are planned and applied first,
Resolve (variants, then Project, then the name and index rules) reads the
filled documents, land runs, and only then Apply mints. A Project getter that
throws is caught by the same handler that undoes the id plans, so a refused
admission changes no document and mints nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:33:11 +02:00
Meta Vens
9c62187562 Test: the directory store keeps a marked manifest on a partial flush, so only the single file drops back
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:56:26 +02:00