ExprGraph (immutable class, one validator in Create), ExprBuilder (hash-consing, dead-node removal, Substitute), ExprOps (the one op table), ExprEvaluator, ExprBindings, ExprGraphException; CultMathExprResolver appended to the default chain after CultMathResolver.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultPayloadDepth.Check walks MessagePack headers iteratively and refuses the first
container past CultMessagePackSecurity.MaximumPayloadDepth, read or skipped, called by
SerializeUntyped (on the bytes it produced) and DeserializeUntyped (before decoding).
UntrustedData's 500 is replaced by the same value as a backstop. CultUnionGuard refuses
[k, nil] arms. New vectors in tests/vectors/union-parity, with bounds-outcomes.msgpack
as their data; the gate now runs every Check-calling test body and fingerprints bytes
and write times. All commits build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A guard ahead of MessagePack-CSharp's union formatter decides both; the store
decode site names the record and schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
GameCult.Geometry no longer carries vector or rectangle values. CultCircle and
CultSphere hold float2/float3, the isosurface and surface-nets extractors take
float3 origin, and the per-call conversion to float3 is gone. Their MessagePack
bytes are unchanged (pinned against the pre-collapse encodings), now produced
by CultMathResolver through the default CultCache options.
This is a breaking change to the GameCult.Geometry public API.
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>
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>
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>
A variant record stores [baseKey, overrides[]] in record slot 4 and an empty
payload; the cache resolves it against its base inside the hold that admits it or
any record on its base chain, and re-resolves dependents in the same Apply, so a
base change reaches its variants with contiguous sequences and no stamp. Plain
Upsert at a variant key is refused (Flatten is the explicit way); cycle, missing
base, different concrete type, evicted base with survivors and a name that
shadows the base are refused naming the keys. Overrides of a dropped slot or an
undecodable value are ignored with a warning and reported as compatible drift.
The store writes cultcache.store.v2 only when it holds a variant and reads v1 and
v2. The codec pair CultInspectorModel took becomes CultCodec, shared with the
cache, which needs it to clone documents and overlay override values. The
directory store, CultMesh single-file reads and CultNet row building refuse a
variant by name until their own cuts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete the trailing Skip loop in the record reader, the ?? v1 header default,
and the mesh reader's copy of the record decoder. The single-file store and
CultMesh single-file reads accept cultcache.store.v1 only. Persistence-format
and parity docs state the rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
452f928 made nil the unset wire form, including dictionary keys: a
Dictionary<CultRecordRef<H>, float> holding the empty key encoded it as C0.
Python msgpack refuses nil map keys under strict_map_key, its default, so
cultcache-py cannot decode such a record. Nil cannot be the one canonical
form; "" can, and every runtime reads it.
The writer always writes "". The reader still takes nil as unset, because
452f928/b85a828 stores and Aetheria's Cut 9 import wrote nil; such a record
converts to "" on its next save and is byte-stable after that. The
store-composition contract names "" for value and key position, requires
readers to accept nil, and tells other runtimes to write "".
The tests pin payload bytes, so each claim is consequential: an unset ref
writes "", reloads through a fresh cache and resaves byte-identical; a
nil-written legacy payload reads as unset and resaves as ""; a ref-keyed
dictionary with an empty key writes "", reads back unset and resaves
byte-identical. The nil writer fails every byte assertion, a reader taking
nil as a set key fails the legacy pair, and a reader taking "" as a set key
fails the read-back assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The formatter wrote default(CultRecordRef<T>) as nil but read nil back as
"", so re-upserting a loaded record changed its bytes (0xC0 became 0xA0).
One in-memory unset value: CultRecordKey.Value is never null, so
default(CultRecordRef<T>) equals a ref built from "". One wire form: an
empty key writes nil, and nil or a legacy "" reads back as default.
Load then save is byte-stable; a store holding "" converges to nil on its
next save.
Rust, TypeScript and Python have no reference type: a reference member is a
string in the consumer's own payload, so there is no sibling codec to align.
The store-composition contract now names nil as the one unset wire form.
The new test loads a record with an unset ref through a fresh cache,
re-upserts and flushes, and requires identical payload bytes; it fails on
the old formatter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An exception thrown inside a `when` filter is swallowed as false, so a manifest
re-read failing mid-File.Replace turned a race that should retry into a thrown
torn-page error. The re-read now runs in the handler, and a failed re-read
counts as moved. Parallel.For's AggregateException is unwrapped where the pages
are read, so the page's own failure propagates and the torn check no longer
walks wrappers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Key identity serialized a key with the options of the key type's own
assembly, but the store writes it inside the document with the document
assembly's options, [CultCacheFormatterResolver] resolvers included. A key
that failed to serialize fell back to comparison by ToString.
The inspector codec now takes the owning document type, ReplaceKey and
FreshKey take it, and a key serializes as its declared key type under that
document's options, the store's own path. A key the document cannot
serialize is refused with a notice; the text fallback is deleted.
The comment and README said "the dictionary's own comparer"; it is the
rebuilt dictionary type's default comparer, and now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A missing or hash-failing page under an unchanged manifest is corruption,
not a writer mid-commit, but every InvalidDataException was retried five
times and reported as "did not settle" with the real cause buried. The
retry now also requires the manifest bytes to have changed; otherwise the
corruption error is thrown on the first attempt. A vanished page directory
is a race under the same condition.
The reload test's writer now commits under every unleased read, so it only
settles if the retry takes the lease that commit created.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A pull takes the commit lease only when .commit.lock exists. Without it
(no writer has created one yet, or the store was copied without it) the
reader loaded with no lease, and a writer committing between its manifest
read and its page reads left it with a vanished page or a stale manifest.
An unleased pull now keeps its load only if the manifest bytes it read are
still current afterwards; a changed manifest, or a named page that is
missing or fails its content hash, reloads it, at most five times, and a
pull that has not settled by then throws. A retry takes the lease once a
writer has created it. Opening still creates nothing. The lease comment no
longer claims a missing lock means no writer ever committed, and the store
composition contract states the rule.
The test deletes the lock from a seeded store and commits from a second
cache through an internal seam between the manifest read and the page
reads, once replacing the page the reader named and once only adding a key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A refused key and an unavailable fresh key are now the model's answer:
ReplaceKey returns the kept key with the notice text, FreshKey returns
null with its notice, and IMGUI only shows them.
A key is taken when another serializes the same or when the dictionary's
own comparer calls them equal, so 0.0 against -0.0 is refused instead of
throwing in BuildDictionary.
Keys never actually compared by bytes: the Studio handed the model the
document codec, whose registry guard throws for any non-document value,
so every key fell back to ToString. CultCacheMessagePack now builds the
model with a value serializer, and the Studio and tests both use it.
Tests pin the comparer rule, the unchanged-key rule (an old empty
reference key survives an edit of its value) and serialized identity
(keys whose ToString collides); each fails with its rule removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening a directory store, read-only or writable, created the .records
folder and its .commit.lock before reading a manifest that might not
exist. Only a write creates them now: PullAll takes the commit lease with
create: false, which opens an existing lock and returns no lease when
there is none (no writer has ever committed there, so there is nothing to
exclude). CommitBatch and PushAll create the folder and lock as before.
Test DirectoryMessagePackBackingStore_Open_CreatesNothing_UntilAWrite:
opening a missing store and a manifest-less .records folder, read-only
and writable, leaves the filesystem unchanged; the first flush creates the
manifest, the lock and one page. With the previous lease code restored
the test fails on the first read-only open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reflective CultDocumentRegistry is now the only descriptor authority,
and payloads always go through MessagePackSerializer with
OptionsFor(document assembly). No consumer builds AOT, IL2CPP or trimmed,
Unity always ran the reflective path, and no benchmark showed a gain,
while the generator duplicated the registry's shape rules.
Removed: GameCult.Caching.MessagePack.Generator and .Analyzers (solution,
project and analyzer references, NuGet pack list), CultGeneratedDocumentMetadata
(provider attribute, interface, definitions, loader), the generated descriptor
and codec branches in CultCache and CultDocumentMessagePackSerialization,
DocumentShapeSweepTests and the generator half of the unkeyed test, the
Microsoft.CodeAnalysis.CSharp test reference and InternalsVisibleTo.
The registry rejects, with a message naming the fix, what MessagePackSerializer
cannot serialize and what the generator used to paper over: a document without
[MessagePackObject], a non-public document without AllowPrivate = true, and a
non-public member of an AllowPrivate document that is not [IgnoreMember].
DocumentShapeTests pins every rejection message and the accepted shapes'
round trips against MessagePackSerializer bytes (keeps the 93A16E0305 pin).
Rejected shapes are emitted into a dynamic assembly, which registry
discovery skips.
Nested member type names use reflection's Outer+Inner form, not the
generator's dot form, so ReferenceHolderEntry's fixture id is re-pinned
(bd8506... -> 4ec0c5...). A scan of the schema catalogs in all 2786 .cc
stores under F:\Projects (AetheriaEve and CultLib-* worktrees, bin and obj
excluded) found no member type name naming a nested type in either form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generator and CultDocumentRegistry now discover members by the same
steps: most-derived declaration first, override chain collapsed to one
member, attributes read up the override chain, rejections in one order
with one text (GCC001 / InvalidOperationException).
- Accessors are named per document, so documents sharing a [CultName] or
[CultIndex] base member no longer emit duplicate methods (CS0111).
- Duplicate [Key] slots are refused, including new-hidden fields and
properties; a hidden persisted member with a different slot is refused.
- String [Key("x")] gets its own message instead of "has no [Key]".
- A persisted property whose set accessor is private, protected or init,
or a readonly field, is refused instead of CS0272 in generated code.
- Overrides: MessagePack's DynamicObjectResolver reads the base
declaration's [Key]/[IgnoreMember] (it throws "key is duplicated" for a
re-keyed override and serializes an override marked [IgnoreMember]).
An override that changes them is refused in both paths; an override
that repeats or omits them is accepted and byte-pinned.
- Typed Serialize<T>/Deserialize<T> used static Options and skipped
generated codecs and declared resolvers; deleted. CultNetDocumentBinding
and CultMeshSnapshots route through the untyped path with their registry.
DocumentShapeSweepTests compiles each shape through the generator and
compares generated vs reflective schema id, catalog, name/index values
and payload against MessagePack's own bytes, or equal rejection text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The static UntrustedData options are gone. An assembly declares its formatter
resolvers with [assembly: CultCacheFormatterResolver(typeof(R))];
CultDocumentMessagePackSerialization.OptionsFor(assembly) composes them ahead
of CultDocumentResolver and StandardResolver, once per assembly, and hands
assemblies that declare nothing the base Options. Generated codecs and the
reflective SerializeUntyped/DeserializeUntyped path both read
OptionsFor(typeof(X).Assembly). No registration call, no mutable options, no
load order.
CultMessagePackSecurity copies UntrustedData and supplies a collision-resistant
comparer over CultRecordRef<T>.Key.Value, so ref-keyed dictionaries
deserialize.
Tests: RefKeyedDictionaryRoundTrips failed on 825b4f7 ("No hash-resistant
equality comparer available"); DeclaredResolverEncodesValueTypeAsPositionalArray
and UnknownAssemblyGetsBaseOptions did not compile there (new API);
InteropNoteBytesUnchanged is a pin whose payload constant was captured on
825b4f7. BenchItem, the one unkeyed [CultDocument] in any consumer (in-memory
benchmark, no store), gains [Key]s in its existing slot order.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A direct MessagePackAnalyzer reference with PrivateAssets="all" in
Caching.MessagePack and Networking keeps MessagePack 3's generator out of
consumer builds, where it emitted a reference to a nonexistent
GeneratedMessagePackResolver formatter for Dictionary<CultRecordRef<T>, float>
(CS0426). CultLib composes StandardResolver itself and never registers a
generated resolver.
tests/GameCult.Caching.ConsumerBuild is RefKeyedDictionaryCompilesInAConsumer:
its build is the test. On 825b4f7 it failed with CS0426; now it builds and
obj has no MessagePack.SourceGenerator output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cache-wide unpublished list let any thread drain and deliver another
thread's changes: a throwing observer dropped the rest of the queue and threw
into an unrelated writer, writes returned before their change was published,
and a third-party load could wait indefinitely.
Every admission now runs in a hold (Held). The outermost hold on a thread
collects exactly the changes it admits and publishes them after it leaves the
gate, before it returns, on its caller's thread; nested holds append to it.
Every change is delivered even when an observer throws; the first exception
(AggregateException for several) is then rethrown, and a failing body's own
exception wins over observer exceptions.
Stores take the hold instead of locking a shared Gate object, so a direct
PullAll on an attached store publishes its own load and the empty follow-up
Loaded hand-over is gone. Gate is replaced by protected Held.
Tests: OwnChangeIsPublishedBeforeWriteReturns and
ThrowingObserverDoesNotLoseOtherChanges fail on 4eacd87 and pass here. The
repull tests now record a write that reaches the gate while the pull is paused
instead of inferring readiness from thread state; both fail when the pull does
not hold the gate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Observers and OnUpdate never run under the cache gate. Admit queues changes
while the gate is held; the outermost entry point (attach, pull, commit,
upsert, remove) publishes them after releasing it. A store's direct PullAll
hands over an empty load outside its lock so the cache publishes then.
A throwing observer can no longer split store from cache: publication starts
only after the store has adopted its loaded view. The directory store adopts
its durable catalog only after the cache admits the load; the write-only
_durableIndex is deleted.
Single-file Push, Delete, PushAll and CommitBatch take the gate like the
directory store; Dispose flushes under it. Write mints storedAt and admits
under one gate hold, and Land re-stamps staged upserts under the gate that
admits them, so a concurrent pull cannot land a later storedAt in between.
Tests: ObserverDoesNotRunUnderTheGate,
ThrowingOnUpdateHandlerDoesNotResurrectDeletedRecord,
RepullNeverErasesAStagedWriteOnDirectoryStore,
DirectCallsOnAnAttachedStoreShareTheCacheGate; the directory last-writer
result is pinned to {a,c,x}; the single-file re-pull test waits for the write
to reach the store or block instead of sleeping. Pausing tests always release
the pause so a failure cannot deadlock Dispose.
Docs: observer rule, direct store calls share the gate, directory TryCommit
contention and gate wait, per-key last-writer-wins, directory flush merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An attached store takes its cache's gate as its own lock (CacheBackingStore.Gate),
so a pull's load callback into the cache can no longer take the gate after the
store lock. The directory store's private mutation gate is deleted; the
single-file pull now runs under the gate; PullAllBackingStoresAsync takes the
gate around each store's pull. Fixes the pull/upsert deadlock on directory
stores and the re-pull that erased a staged single-file write.
An unconditional single-file Commit writes the store's whole view plus the batch,
exactly as a flush would, instead of merging onto the file; it persists earlier
staged writes and leaves the store clean. Conditional commits still land onto
the file under the lock. Directory-store commit and flush already share one
generation writer (staged keys onto the current manifest) and are unchanged.
Contracts state both rules, the lock order, that OnUpdate fires for loads only,
that a missing single file keeps staged writes, and that a store failing to
hydrate on open throws and is left byte-identical.
Tests: PullAndUpsertOnDirectoryStoreDoNotDeadlock, RepullNeverErasesAStagedWrite,
UnconditionalCommitIsLastWriterWinsLikeFlush, UnconditionalCommitPersistsStagedWrites,
OpeningACorruptStoreThrowsAndLeavesItUntouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 3 of the CultCache migration (Aetheria docs/cultcache-migration-cut.md, 2.1
and 2.2). One commit, because routing, the batch that replaces the ambient
transaction, and conditional commit share one admission path.
Deleted first:
- globals: InitializeGlobals, MaterializeMissingGlobals, the two-argument
constructor, the initializeGlobals plumbing in CultCacheMessagePack,
ContainsDurableRecord (cache and directory store)
- replication: the push-to-every-store loops and the attach-time push of
existing records
- typeof(T) as the schema owner in AddAsync/UpsertAsync and in
CultNetDatabase.PutAsync/PutPredictedAsync
- the ambient transaction: ExecuteTransactionAsync x2, CultCacheTransaction,
CommitTransaction, VisibleStoredDocuments, the AsyncLocal, the SemaphoreSlim
gate, and every ambient check
- CultCacheOpenOptions.PullOnOpen; OpenAsync is Task.FromResult(Create(...))
- DirectoryMessagePackBackingStore.ResolveLegacyUncataloguedRecordCatalog: the
catalog precommit (fe55bff) is an ancestor of the v4 page format (eea8248),
so no v4 manifest can reach it
- CultCache.DeleteAsync<T>; its one caller, CultNetDatabaseSubscriptionClient,
called it by reflection and now calls Remove(key)
Added:
- AddBackingStore(store, params Type[] homes): attach validates (clean, not
already attached, one untyped store, no claimed home type, no admitted
record changes home) and then pulls under the gate
- Home(type): most derived routed home, else the untyped store
- Admit: every add and remove, from a write, a batch or a load, validates
(home, read-only, loaded-from-home, global singleton), lands durably, then
applies and publishes; a pull hands its whole delta over in one call, so a
refused record leaves the store and the cache unchanged
- read-only stores (CacheBackingStore(readOnly), both store constructors,
CultCacheOpenOptions.ReadOnly); flush skips them and writes only dirty stores
- assignable GetGlobal<T>/GetByName<T>/GetByIndex<T>/Watch<T>; more than one
candidate throws naming them
- Commit(Action<CultCacheBatch>) returning bool, TryCommit returning
CultCommitOutcome, CultCacheBatch (Upsert, Remove, Expect, ExpectUnchanged,
sealed after commit), CultCommitRequest; CacheBackingStore.CommitBatch
evaluates conditions under the store's exclusive lock
- single-file store: <path>.lock sidecar for CommitBatch and PushAll; a clean
store commits onto the file as it is, so records it never loaded survive
- storedAt minted in one place and bumped a tick when not later than the record
it replaces
Consumer adjustments in this repo: Mesh streaming tests drop PullOnOpen; the
Studio bridge stops reflecting on it; BackingStoreTests' global test creates
its global explicitly and its transaction tests are rewritten against Commit
(the two that proved ambient hydration rules are deleted); the pre-Cut 2
refusal test expects the attach to throw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The parked/cultcache-soa tag message says the managed document was deleted
separately. parked-features.md now supersedes that; the tag is unchanged.
The directory store gains its design statement and a comment that only
manifest-named pages are loaded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every /// line in GameCult.Caching and GameCult.Caching.MessagePack goes; they
restated member names. Three blocks that state a rule stay as // comments:
MaterializeMissingGlobals runs after hydration so persisted globals win,
ExecuteTransactionAsync commits durably before the cache and observers
advance, and CommitBatch implementations restore their staged view on
failure. Both projects add NoWarn CS1591, since GenerateDocumentationFile is
on in src/Directory.Build.props.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SerializeSchemaCatalog and DeserializeSchemaCatalog had no consumer outside
the snapshot round-trip test. Catalog entries still serialize inside
SerializeSnapshot/DeserializeSnapshot through the same per-entry
reader and writer, and the round-trip test now checks the catalog through the
snapshot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DirectoryMessagePackBackingStore loses HydrationFilter, FlushStageProbe,
ReadStageProbe and their stage constants, ReadPersistedGeneration,
PullSelected, and every legacy path: the v2 directory-indexed and v3
immutable-pages formats, v1 inline records and split pages, the index
upgrade on flush, and the legacy page paths. CacheBackingStore.PullSelected
and CultPersistedRecordMetadata go with them.
A missing manifest is an empty store. An existing manifest whose
FormatVersion is not cultcache.store.v4.directory-content-addressed-pages
throws "Directory store {path} is {format}; only {v4} is readable." A clean
flush writes nothing, since there is no legacy index left to upgrade.
Tests deleted with the surface they injected through: the v1 missing-catalog
load, pages-before-manifest, fault-before-manifest (4 cases), same-timestamp
page, mutation-during-flush and reader-lease tests. The orphan-page test now
checks through a fresh store pull instead of ReadPersistedGeneration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ConfigureCache, ConfigureStore, DirectoryStorePath, DirectoryStoreHydrationFilter
and ConfigureDirectoryStore had no consumer outside tests. CreateCore attaches
the directory store at its default record path, or the single-file store,
with no customization hooks. OpenAsync keeps its behavior (pull, then
materialize missing globals); PullOnOpen stays until Cut 3.
The indexed-filter test loses its selective-open half and keeps the v4
manifest assertions: the hot index carries only the SHA-256 page identity and
both records reload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes FlushBackingStore, both PrepareForReloadOrShutdown overloads,
PullBackingStoreRecordsAsync, TryGet<T>, TryGetByName, TryGetByIndex,
Resolve<T>, the cache and store Logger, and LastSuccessfulFlushAtUtc on the
cache and stores. GetStoredDocuments<T> folds into GetAll<T>.
The soft flag goes from FlushAllBackingStores, ExecuteTransactionAsync,
CommitBatch and PushAll. It stays on FlushAsync(bool) because
CultCacheStudioWindow reflects on that signature until Cut 6.
CultMesh.FlushAsync and CultNetLocal.FlushAsync keep their parameter and
stop forwarding it.
The store's EntryAdded/EntryUpdated/EntryDeleted subjects become
protected internal Loaded/Unloaded callbacks set by the cache at attach.
AddStoredDocumentInternal is synchronous, so the GetAwaiter().GetResult()
bridges on the store subscriptions and the global materializer are gone.
Tests: drop the LastSuccessfulFlushAtUtc asserts; OpenAsync and GetAll tests
read through Get/GetAll; the persisted-global test proves the durable global
loads without staging a default instead of going through selective hydration;
the lease-before-observers test observes through CultCache.Watch; the
PullSelected selective-hydration test is deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>