A CultStoredDocument built by a caller and handed straight to a store never passed an
admission, so HoldsIds stayed false and its ids landed under a v1/v4 header. An admission
still decides it (it counts the ids it fills); a record that never had one reads it off the
document it stores. CacheBackingStore.HeaderFor documents the rule for third-party stores.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CacheBackingStore.HeaderFor(holdsIds, existingHeader, wholeStore, directoryStore) replaces FormatFor,
DirectoryFormatFor and the writers' private marker logic. A writer that rewrites the whole store
decides by content; a writer that touches part of it keeps a header already marked. Flush,
unconditional and conditional commit, the directory store and CultMesh's single-file write all call
it. A typed CultMesh write replaces the file with a document it can see, so it decides by content;
a raw payload is opaque and keeps a marked file marked.
A record's HoldsIds is what its stored form persists: a load keeps a variant's delta as read, so
the ids a load mints in an override exist only in memory and no longer mark the store. A
conditional commit onto the file no longer reads this cache's view of what the file held, so a
stale view cannot over-mark it. IdPlan.HoldsIds is derived from HeldIds and the fills.
Reverts InternalsVisibleTo GameCult.Caching.MessagePack and the public DirectoryFormatV4/V5
constants: the directory store reaches the decision through the protected member it inherits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A flattened variant's HoldsIds is read off the document re-resolved against the post-batch base, not carried from the
variant or from the document the flatten was planned against. The conditional commit onto a file decides by the same
rule as the unconditional commit (ids in the records the file will hold, ids only this cache minted excepted, a marked
file staying marked). The directory manifest's header choice moves beside FormatFor. Pins the three-batch flatten
probe on the commit and flush paths, the conditional commit, the last id leaving, an id-bearing override that is not
the last, and a failed CultMesh write giving its ids back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AddJournal's Disposable.Create counted a refused dispose (from inside a journal
or another cache's hold) as done, leaving the journal registered forever. A
private registration now flips only after the removal succeeded. Held's catch
path dropped the hold's journal failures; it now throws an AggregateException
with the body's exception first. The store-composition contract states exactly
when the second getter read runs on a conditional commit. Adds the direct test
that a load is journaled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Attaching a store with no file never calls Loaded, so it did not exercise the
rule; a commit removing a key the cache does not hold admits nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A disposed R3 subscription ignores OnNext, so a no-op removal from the cache's
observer array cannot be seen by what a receiver gets; the test now checks the
cache stops holding the subscriber. An admission that changed nothing (a store
attached with no records) is not journaled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A store is written v3 (v5 for a directory) only when a record in it holds a non-empty
element id. CultElementIds.Plan reports that as IdPlan.HoldsIds; the admission stamps it
on the stored record and every store reads it. The descriptor's CarriesElementIds is
deleted. A file already marked on disk stays marked. CultPersistedStoreSnapshot.FormatFor
is the one header decision.
Plan refuses one element object sitting twice in a list at write, naming the record and
list, instead of writing both ids and refusing the store at load.
CultMesh single-file write mints, refuses and marks as a cache write does; the read applies
the load-time plan, so a write and a load refuse the same things.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultCache.AddJournal registers an ordered admission journal. It is called under
the gate right after Apply, once per admission that changed something, with the
admission's changes (dependents included) in Sequence order, before any
observer. A journal that throws joins the hold's failures: the admission stands,
the other journals run, the change is published and the writer hears of it
afterwards. Any hold entered while a journal runs throws. A hold now carries its
own changes and failures. Order stays data; no delivery thread or scheduler is
added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hold whose body throws after it admitted changes still publishes them, and
Held now assigns its result up front so it compiles under Stryker Safe Mode
(CS0165 had dropped every mutant in it). Disposing a Watch subscription removes
the observer from the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul verdict: merge. Before Cut 4 relies on sequence density: pin the
publish-on-throw guard in Held and observer removal on unsubscribe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every public CultMath value type serializes: MessagePack in the default resolver
chain (byte-identical to Aetheria's and StreamPixels' copies across 653 values
and all 115 float fields of the real Aetheria.cc) and System.Text.Json converters
(STJ-parity number handling, math types as dictionary keys). CultVec2/3 and
CultRect are gone with bytes unchanged. The cultlib Unity package depends on
cultmath; the release gate refuses until CultMath 0.3.0 carries every type.
Three Soul passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Named literals now follow AllowReadingFromString and WriteAsString as
System.Text.Json's float does; quoted numbers refuse whitespace;
UnmappedMemberHandling.Disallow and AllowDuplicateProperties=false are honoured
and a repeated property replaces the whole value; dictionary keys read only the
canonical spelling. Components live in stack spans, property names are
pre-encoded, and there is no per-property closure, so a float3 read allocates
nothing (424 bytes before). Tests pin unknown nested values, de-DE and sv-SE
style cultures, and a distinct negative-zero key.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Engine tests for the plan (duplicates, format, empty derived source, atomicity, two
cultures, interface unions, key sharing); cache tests for the marker on single-file and
directory stores, override values on write and load, minting that is given back, and the
in-memory-only mark.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete the dead removed-ancestor branch and the duplicated name-read in the
variant name rule (resolution already refuses a missing or removed base and a
variant shares its base type). Add tests for the index rule counting a record
the same commit removes, blank names not colliding, and Dispose rethrowing
escaped completions after every observer completed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One Scalar plus Node description per type now drives reading, writing and the
dictionary-key spelling, replacing the per-value JsonDocument parse. NaN and the
infinities follow JsonNumberHandling (named literals or JsonException), floats
beyond float.MaxValue are refused instead of becoming infinity, out-of-range
components throw JsonException instead of FormatException, property matching
honours PropertyNameCaseInsensitive, and every type works as a dictionary key
spelled as comma-joined components.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CultMathJson is the JSON twin of CultMathResolver: vectors, quaternion and
Color32 are objects keyed by field name, matrices are arrays of row objects,
rect is {min,max}. Consumers add them with AddCultMathConverters(); CultLib owns
no JSON options. CultCircle and CultSphere serialize and deserialize through
them, with [JsonConstructor] so the struct constructor is used. The Caching
README documents each MessagePack and JSON shape in one table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Golden bytes now cover double2, double3, int3, int4, bool3, bool4, quaternion,
Color32, Random, float3x3 and CultCellular. A missing Color32 alpha decodes as
255, the constructor default. A test proves a consumer resolver registered for
the assembly wins over CultMathResolver. The redundant nil check is deleted;
ReadArrayHeader already refuses nil.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The cache no longer fans changes out through an R3 Subject. It keeps a
copy-on-write observer array and Publish catches per observer: every observer
receives every change, an exception that escapes (a fail-fast R3 handler) joins
the OnUpdate failures rethrown to the writer after delivery, and the observer
stays subscribed. Dispose completes observers, and a disposed cache completes
new ones at once. Deletes the false "OnNext does not throw" comment.
The composition contract now states the observer policy and that a throwing
name or index getter refuses a change set, loads included.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolving a variant serializes its base, and the serializer reads the same
getters; the test measured them together and expected too few.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolve now ends in Project, the only place an admission runs a [CultName] or
[CultIndex] getter on a landing record. It runs before land and before any
Sequence is minted, so a throwing getter refuses the admission (loads
included) with nothing written, minted or left in memory. The variant name and
index rules and the indexer read the projection; Apply runs no user code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The first commit's resolver needs the CultMath project reference and scoped
span parameters to compile; the golden test file also had two literal slips.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CultMathResolver formats float2/3/4, double2/3, int2/3/4, bool2/3/4,
quaternion, Color32, Random, float2x2, float3x3, rect and CultCellular as
component arrays, and sits in the default CultCache resolver chain so no
consumer registers anything. It lives in GameCult.Caching.MessagePack, which
already owns that chain; CultMath core stays dependency-free.
Vector, rect and Aetheria-formatter byte shapes are pinned by golden tests, a
reflection test fails when a public CultMath value type has no formatter, and
decode skips extra elements and refuses nil.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker on the escaping showed the pair-skip branch uncovered and the run
offset unpinned; the kinds now include a valid pair, a replacement character
before a lone low surrogate, a reversed pair, and two unpaired surrogates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[CultElementId(nameof(Member))] declares an id derived from that member's
invariant text, never random, so content-addressed elements keep encoding to the
same bytes. CultMeshCdnChunkRef derives its id from Offset; PackArtifact sets
it, the Rust pack_content writes it, and the committed content vectors are
re-cut once so both runtimes carry it. Engine tests drive Problems and Assign
directly (InternalsVisibleTo the Caching tests).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ids that differ only by %2F versus /, keys that differ only by case, and
unpaired surrogates each get a test; the ungrouped list stays flat.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker survivors on the grouping diff: the no-declaration path, the
null-string identity, the inherited [CultGlobal] check and the id separator.
GroupLevel takes text and tie as optional arguments so the scalar kinds
stop passing constant strings that could be swapped without any effect.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[CultElementId] marks a string member; the registry refuses a document type
whose list element type (union bases included) lacks one, naming the type and
member. A write mints a random 12-hex id for each unset id and refuses a
duplicate within one list. A store written before ids loads with ids minted in
memory from SHA-256(record key, element path), so every reader and every reload
agrees; they persist on the store's first write. CultCache.MintElementIds()
rewrites the records whose ids exist only in memory.
CultLib's own list-element types gain an id (Geometry, Mesh body, discovery,
entity prefab). CultMeshCdnChunkRef and the CultWitness* elements are pinned by
cross-runtime vectors and are left alone; see the C2a report.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[CultInspectorGroupBy(nameof(A), nameof(B))] on a document class names the
members its record list groups by, outermost first. CultInspectorModel owns
the rest: GroupingOf validates the declaration (a refused one gives a flat
list and a notice), GroupRecords partitions RecordCandidates by value
identity with occupied values only, CreateInGroup builds the record a node
holds, RecordRefLabel owns None and Missing <key>. A variant groups by its
resolved Document.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RecordCandidates and the new IsRecordCandidate share one rule, so a
picker's candidates and a drop's acceptance cannot disagree. Member
attributes are read with Attribute.GetCustomAttributes(member, true):
PropertyInfo.GetCustomAttributes ignores inherit, so a base abstract
property's inspector attributes never reached the override.
Co-Authored-By: Claude Sonnet 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>
They hold a FileShare.Read handle and expect a delete or replace to fail;
Linux unlink and rename ignore open handles, so they can never pass there and
showed as three standing failures. [Platform("Win")] reports them skipped with
a reason on Linux and keeps them running on Windows. No Linux failure injection
exists: the store names its temp file with a fresh GUID and the suite is
likely root in the verify container, so read-only directories do not fail.
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>