A staged write mints a later storedAt, so the 'rewrite kept storedAt' premise is gone; the pin is that a record rewritten under another schema fails Expect. The variant refusal names the renamed base through ChangedKeys, as C2a's typed exception carries it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Directory store now sets StoredSchemaId with StoredVersion after a page write (found by main's OwnPushOfALoadedDriftedEntryKeepsExpectHolding). RecordRewrittenUnderAnotherSchema stages the record, since a flush writes only the write set. Variant conflict test expects CultWriteConflictException.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Identity: C2a's StoredSchemaId/StoredVersion owns; main's PersistedSchemaId, PersistedIdentity and both MarkWritten are deleted. Stamp rule: ReadStore judges the stamp before the body. Flush over damage refuses (merge-r1-precedence). Mesh single-file writes route through SingleFileMessagePackBackingStore. TS: store-file.ts commits, C2a's header handling kept. Rust: C2a's readable_store_format in front of the grown decoder. Build not yet verified at this commit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Red until stores mark through a protected base member: the directory store's
Push+PushAll leaves the entry's persisted schema id stale, and a store outside
GameCult.Caching has no way to mark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red at b549d731: an unchanged drifted record fails Expect on both stores, and another writer's normalising flush is not seen. Pins the cases of cultlib-gaps:cut_spec:cut-expect-persisted-schema.r1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relabel tests become refusals: ARecordReadByNameIsReadOnly (name branch)
and AnOlderVersionCannotWriteANewerRecordItReadsThroughTheNewerEntry (a
persisted entry's list). New: every write path refuses before staging,
declaring the id claims the record, a type registered after load claims it,
MintElementIds is refused by a read-only record, the undeclared dropped-slot
case, and a variant of a read-only base lands. The copy test also checks the
write beside the read-only record landed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete RawBytes from CultPersistedRecord and CultSchemaCatalogEntry, with every
drop-on-assign hook and the catalog's content guard. A decoded record or snapshot
carries no stored bytes, so an in-place edit of its payload or overrides can never
be shadowed by cached bytes, and the public snapshot and record serializers encode
every record from its fields.
SingleFileBackingStore now reads through ReadStore, which returns the snapshot and,
parallel to it, the exact slice the file holds for each catalog entry and record. Its
durable index keeps, by record key, that slice and its SHA-256 (hashed when a
comparison asks), which is the record's version. A write re-encodes every record
the writer staged and copies every other record, and every catalog entry it keeps,
as the slice the file holds. The directory store keeps its manifest's catalog slices
the same way. SerializeSnapshot/DeserializeSnapshot hooks become
SerializeStore/ReadStore plus SerializeCatalogEntry.
A staged but unchanged non-canonical record is now re-encoded canonically and gets
a new version; an untouched one keeps its bytes and version.
Tests: in-place edits through the snapshot API (payload, override) and through a
held document (collection, nested object) are written as edited; an untouched
non-canonical record survives a neighbour's write byte for byte; a staged one is
re-encoded. The cost test counts record hashes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ported from Soul's s2 probes: P5 skewed clock (equal storedAt re-mint, mint from the durable one),
D (directory store re-mint, as a store-kind parameter on the later-storedAt test), E (a disappeared
base refuses a variant write), P3 (foreign refusal whose reload fails carries the cause). New: a
foreign record a refusal reloaded is not seen as moved, and a writer that finds the file gone has
read nothing. ExpectOnAVariant is restored to pin that Expect tests only the stored delta: a stale
base reaches the write conflict, not a mismatch. Per-commit build not verified.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ebd66e6f and cc8604ae do not build: the re-mint named MintStoredAt without its class.
Two tests pin the staleness rule for a write that would add a variant to a plain store.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The merge tests become the refuse, reload, retry rule; new tests pin copied records that
cannot be decoded, the later storedAt over a replaced record, a batch that removes a staged
key, and a Mesh write into a variant store. The persistence and composition contracts state
the rule for the parity cuts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CULTLIB_REAL_STORE names a copy of a real store: one staged write leaves every other record and
catalog entry byte for byte. New: a copied record changed after the load keeps a marked header; what
this cache wrote counts as known id-free; a batch wins over a staged write of its key; a write is
judged on the staged edits plus the variants another writer left in the file.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The unconditional-commit last-writer-wins test now keeps another writer's record; the load-minted ids
stay for MintElementIds after an unconditional write; a detached store marks the variant it writes, not
the one it copies; a variant write supplies the indexed override; the conditional/unconditional
comparison uses one batch (a conditional commit refuses staged writes by design).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
New WriteSetTests (single-file and directory stores, flush and commit): another writer's write and
removal survive, a store that appeared after open is kept, an unstaged in-place change is not
written, a copied record keeps the catalog entry the store holds now, an entry's unknown trailing
fields survive byte for byte, conditional and unconditional commits write the same bytes, the
header drops only when every copied record is known id-free, v2 exactly with a variant. Mesh: a
write copies neighbours forward and replaces only its key. The lay-back and whole-view tests and
the flush-restamp tests are rewritten or removed with the rules they pinned.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
- RefuseVariantIndexSharing built its holder list per landing record by scanning
every landing record, so opening a store with one variant was quadratic. It now
groups what lands once and looks each (index, value) up once. Pinned by a count
of index-accessor reads at two sizes, not a clock.
- JudgeMerge decoded every unseen disk record before Resolve could exit early,
so a plain-only store failed a conditional commit on another writer's schema it
does not register. It now judges only when the batch, the disk (by the record's
variant header, never its payload) or this cache holds a variant.
- Plain duplicates find the holder with the smallest key (ordinal), so the choice
is the same live, after a flush and after a reopen. The comment claiming "most
recently indexed" was wrong across reload.
- A dropped slot is reported once per record, not once by the schema diff and
again by the variant.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The held index kept one key per value, last writer winning, and RemoveKey dropped
a value outright. Two consequences: removing or changing one of two plain
holders left the other unfindable, and the variant index rule judged a variant
against that lossy view, so a variant could land on a value a plain record still
held and the store then refused to reopen.
- _indexes maps a value to the keys holding it, oldest first. Index, Unindex and
the removal maintain the set. GetByIndex finds the most recently indexed
holder of a value, so removing the winner finds the other (behaviour change for
plain duplicates: it used to find nothing).
- One rule, RefuseVariantIndexSharing, serves both write orders. It reads the
truthful index minus what the commit replaces or removes, plus what it lands.
ResolveOne no longer judges indexes; ClaimIncomingIndexValues, HeldByVariants
and RefusePlainSharingWithVariant are gone.
- A conditional commit merged onto the file is judged by the cache's own
resolution (CacheBackingStore.Judging) on the merged set: records other writers
added, records they removed, and the batch. Another writer's variant sharing a
value with the batch, or a base removal that strands another writer's variant,
is refused with the named refusal instead of writing an unopenable store.
- Shedding is one decision at load. ToStoredDocument resolves the schema once and
returns the report with it; a variant's override of a slot the type lacks is
dropped from the delta there, so flush needs no predicate and every store
reports through the same surface (ReportVariantSheds and OverridesTheTypeHas
are gone). The directory store refuses variants and loads through the same call.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The index rule is about the commit's result, so it now runs for every commit that changes an indexed value: a plain record landing on a value a variant of the post-commit set holds is refused naming both keys and the index (the held index is never trusted; a plain write had already overwritten it). Plain-vs-plain is untouched. A variant sheds overrides of slots its type lacks when it is flushed, as a plain record sheds the member, and the load's report names the slot when the schema itself never had it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hand-written and directory-store variants now override their indexed value or
name as the rules require; the subscription test writes its variant through a
committed batch so the file holds it, subscribes by key, and reports what it
saw when it times out.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F3: an override drifts exactly as the same member would on a plain record. A
slot the type dropped is ignored and reported by the store's schema resolution
(LastSchemaMigrationReports), the one surface a plain record uses; a value that
no longer decodes refuses the load, naming the variant and member. The second
report surface (VariantMigrationReports, the per-variant report map, the
catalog slot-name plumbing) is deleted.
F4: an override for a slot the type does not have, or a value that does not
decode as the member, is refused at write. Drift is for type changes after the
fact, not for bad writes. F5: two overrides of one slot are refused.
F2: a variant that shares an indexed value with another record is refused,
naming both keys and the index (the R6 rule extended to [CultIndex]); plain
record duplicate handling is unchanged.
F7: a flatten now resolves against the same batch, so a base edited in that
batch is in the flattened record; and the directory store refuses to load a
variant page as it refuses to write one.
The C1 vector's big variant now overrides its indexed code, as the unique-index
rule requires.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reader refusals (v1 header over a variant, payload on a variant, unknown op), a
load that finds a variant flattened by another writer, a re-resolution reaching a
read-only cache, a base removed in the same batch as its new variant, and
plain-record-to-variant refused without the name rule masking it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
VariantTests drive the cache batch API and the real single-file store. The C1
vector was written by the code under test through that path; C# reads it and
rewrites it byte for byte, and the other runtimes' tests read the same bytes next.
The C0 tests that expected C# to refuse a v2 store now expect what C1 does with
it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>