14 KiB
C2a land map
What C2a (hands/variants-c2a, a5c7954e) must still prove to merge to main, measured
on 2026-10-22 against main 7a8933b0 and the 2.0.0 release candidate
hands/cultlib-unity-release-2.0.0 (5b5d8573, which carries f91e30ed). The land cut
is the merge plus the gate. Everything else is classified below. Governing ruling:
cultlib-gaps:ruling:release-now-c2a-bumps-format.
Facts
1. The merge. A trial git merge origin/main on a5c7954e gives 12 conflicted files and
33 hunks of about 950 marker lines (CultCache.cs 17 hunks/196; cult-cache.test.ts 337;
single-file-messagepack-backing-store.ts 4/106; rs lib.rs 2/51; VariantTests 51; three
CHANGELOG add/adds; store-composition contract 32; Directory store 14; InteropPeer 21).
Main moved 4.8k lines in the cache runtimes since base 1a12bc2d. Three semantic collisions
are not textual:
- Durable record identity has two owners. Main added
CultStoredDocument.PersistedSchemaIdandCacheBackingStore.MarkWritten(73d438db,afd799ba,3f6dda8b). Conditional commit compares the persisted schema id plus storedAt. C2a already owns this asStoredSchemaId+StoredVersion(SHA-256 of the stored bytes, which names the schema id and storedAt) set byCultCache.Wrote. After the merge, 9 references in ConditionalCommitTests and the contract name main's owner. The version is the owner. PersistedSchemaId and MarkWritten are deleted, and main's +309 test lines are ported as pins against StoredVersion. 2.0.0 ships MarkWritten as protected API, and C2a also removesSerializeSnapshot/DeserializeSnapshotoverrides and changesConditionsHold. C2a is therefore a major release (3.0.0), which semver forces. - TS store commit moved. Main's
store-file.ts(0e6452c9,0010ee92; 391 lines) now owns commit, replace and the<path>.lock. C2a's v3 read and keep-v3-on-rewrite (STORE_FORMAT_ELEMENT_IDS) lives in the old store file and must move into store-file.ts. - Rust decode door. Main's cultcache-rs 0.3.0 grown decoder (
decode_stack,fc630d3b) must sit under C2a'sreadable_store_formatand v3decode_store_file.
2. The gate already holds; it needs a test, not a format change. C2a stamps
cultcache.store.v3 (single file) or ...v5.directory-content-addressed-pages exactly when
a record holds an id (HeaderFor, CultCache.cs:3543). It also marks when a copied record
decodes holding one (CopiedHoldIds, :4285). The 2.0 C# reader accepts only v1/v2
(CultDocumentMessagePackSerialization.cs:229) and directory v4 (Directory store :452).
Main's TS, Rust and Python readers accept only v1 (store-format.ts isV1Snapshot, lib.rs:1274,
stores.py:192). Probe on Yggdrasil at 5b5d8573 plus C2a's v3-base.msgpack (job
gate1.log): 4/4 pass. The single file is refused naming v3, and a later flush leaves it
byte-identical. A v5-stamped directory store is refused naming v5, and its 4 files are
byte-identical after a later add and flush. The pin: C2a already ships
tests/vectors/document-variants-c2a/old-reader-refusal.sh, which stages an old revision
plus probes on a scratch ref and runs them per runtime. The land cut sets its
OLD_READER_REV default to the tag cultlib-unity-v2.0.0. It also adds the never-rewritten
assertion and the v5 directory case to the C# probe, and runs all four runtimes at the
tag. If the tag is absent, the land waits for it. The tag, not a package, is the pin: the tag's
tree holds every runtime's released reader.
Outside C2a's reach (release session): in 2.0, CultMesh.WriteSingleFileDocument/
WriteSingleFileDocumentPayload (CultMeshSingleFileDocuments.cs:211-245 at 7a8933b0) build
a fresh one-record v1 snapshot and replace the file without reading it. A 2.0 Mesh write
to a C2a v3 path therefore discards it whole, ids and other records included, and no header
gate can stop a writer that never reads. C2a's version reads under the lock and refuses.
Fix it in 2.0.0 before tagging, or record it.
5. Consumers. Aetheria pins a CultLib revision (VerifyCultLibRevision) and Unity tags.
Ghostlight, Eve's Rust crates, Huginn and the Brokkr daemon pin cultcache-rs by git rev.
None of them moves when main moves. Two C# consumers build against an unpinned sibling
..\CultLib checkout: Eve GameCult.Eve.Surface.csproj:16 and Brokkr's three csprojs.
Eve.Surface has [MessagePackObject] element arrays with no [CultElementId]
(EveAssetCatalogEntry[], EveEntitySoaBuffer[], EveInputActionDocument[],
IReadOnlyList<EveCommandTemplate>, 0 ids in the repo), so C2a registration refuses its
documents as soon as the checkout pulls the land. Grep finds no object lists in Brokkr's
[CultDocument] files (not build-verified). Removed C2a API (Options, AsSchemaAlias) is
unused by Eve and Brokkr. Fork: variants:question:unpinned-consumers-at-land.
Classification
| Item | Class | Why |
|---|---|---|
| c2a-format-version-gate | a | The gate test above. No new header: v3/v5 already refuse. |
| slot-reuse-contract-doc | a | The ruled consequence of the write set C2a lands. One paragraph in cultcache-schema-compatibility.md, absent at a5c7954e (grep). |
| foreign-ids-under-v1-header | b | Obsoleted by fb065f0d (write-time CopiedHoldIds, after the finding) for every C# writer with the type. The residual (non-C# or opaque writers) is what non-csharp-writers-mark-ids-2 ruled to carry, and the contract states it (persistence-format.md:434-439). The gate changes neither side, so no fork remains. Resolve as Superseded by that ruling. |
| custom-resolver-element-ids | b | Fixed by b091d21f (19:52Z, after the 18:27Z follow-up): ListElementsClaimedByAResolverAreRefused, AResolverClaimingTheListTypeOfAnObjectListIsRefused. s6 holds on the claim loop. |
| codec-door-proven-mismatch | b | Done in h6 57fdf70c (s6 only-proven-mismatch Holds; Geometry 76/0). |
| consumer-element-id-sweep-r2 | c | Each repo's sweep is its own work (q3). Only the unpinned trackers matter at land, and that is the question below. Aetheria bumps a pin on purpose (C4). |
| schema-discovery-holder-recogniser | c | TryGetDictionaryRefTypes is identical on main (CultCache.cs:1288). It describes record-ref dictionaries and is not on the id path. Changing it moves schema hashes, which is its own epoch event. |
| copy-check-memo | c | Performance only (about 3x flush on unmarked stores of 20k records). Correct as is. |
| string-key-field-and-shadow-pins | c | No wrong behaviour at 64ec14f9. These are test pins and may ride the land cut if Hands touches the file. |
| declared-range-gate-own-version, range-check-in-ci | c | The script exists only on C2a and runs in no CI. It guards a TS publish, not a merge. Fix both before the first TS release from main after the land. |
| s6 Unproven: Unity/IL2CPP identity, other MessagePack versions | c | The merge reaches no Unity build. Unity sees C2a only at the 3.0.0 tag, so measure it in the Unity release cut (operator Unity check, C3). |
| multi-store-commit-atomicity, store-replace-cut, reader-refuses-duplicate-keys, soft-flush-all-runtimes, writer-dangling-link-tolerance, unique-index-cut, py-shared-write-lock, rust-lock-open-waits, ts-lock-test-hardening, c0/legacy/directory vectors, culture-test, wire-id-cuts, remaining-variant-cuts, golden-path, paged store | c | None says "before land". Each is pre-existing on main or C2a-independent. None lets a released reader strip ids or corrupt a C2a store. |
The land cut's shape (transcribe once the question is ruled)
- Base. Merge main at or after the
cultlib-unity-v2.0.0tag intohands/variants-c2a. Versions stay main's. CHANGELOGs get Unreleased entries, and the Unity entry says major. - Deletes and collapses. Delete
PersistedSchemaId,PersistedIdentity,CultStoredDocument.MarkWrittenandCacheBackingStore.MarkWritten(main, about 60 lines), and the store-composition contract's MarkWritten paragraph. C2a'sStoredVersion/Wroteis the one owner. In TS, delete C2a's header handling in single-file-messagepack-backing-store.ts and move it into store-file.ts. In Rust, C2a's v3 decode goes through the grown decoder. - Adds. Retarget old-reader-refusal.sh to the tag. Add the C# probe's never-rewritten assertion and the v5 directory case (as in gate1.log). Add the slot-reuse paragraph. Port main's ConditionalCommitTests to the version identity.
- Verification. Full suites: Caching, Mesh, Networking, Geometry, WebSockets,
ConsumerBuild, InteropPeer, cultcache-ts/-rs/-py, cultnet-ts and cultmesh-ts, all on
Yggdrasil. Run old-reader-refusal.sh for all four runtimes at the tag, and it must refuse.
Negative greps over the merged tree:
PersistedSchemaId|MarkWritten, conflict markers. Mutation: drop the v3/v5 stamp inHeaderForand the gate probe must fail. - Estimate. About 950 conflict lines resolved. Net -60 to -150 from the identity collapse and TS de-duplication, +80 to +120 tests and docs. One Hands pass of 100-150 calls, then one Soul pass.
- Precondition (question). Eve.Surface, and Brokkr if confirmed, are pinned or swept before the merge reaches main.
Store gate: attachment is the attach's hold (store-gate-one-owner r2)
Spec variants:cut_spec:cut-store-gate-one-owner.r2 (base ec94ab98), question
variants:question:pulled-variant-codec. It closes Soul s3's
attach-window-cacheless-write (Medium) and codec-loan-pinned-by-reflection (Low).
Facts (source reads at ec94ab98 and a probe, 2026-10-25).
- The window. AddBackingStore holds the cache's gate, then the store's gate through
InStoreHold, lends the registry and codec, setsLoaded, adds the store to_stores, pulls, and only then setsstore.Cache(CultCache.cs 2268-2312).InStoreHoldlets any call on the same store's gate re-enter it (2825-2831). So a journal the pull's Admit runs on the attaching thread can callstore.WriteDocument:Gate(cacheless)guesses null, re-enters the store's gate, findsCache == nulland runs the write, and itsfinallycallsUseRegistry(null, null)(Soul s3 P1, full1.log lines 49-53). Other threads are not in the window: they queue on the store's gate and see either the published cache or a store that the failed attach put back. - The codec loan has no reader on the attach path. A pulled variant's codec is read only
by
CultStoredDocument.HoldsIds(543-557). Header decisions readHoldsIdsonly on staged entries (single-file 4318, 4447 and 4472; directory store 362), and a staged entry is always the document the cache pushed (PushsetsEntries[key] = entry, 4211-4215). A single-filePullAllreturns without loading while the store is dirty (4157-4159), so a pulled copy is never staged. A refused write unstages its keys before it adopts the reload. The directory store refuses variants (rulingq6-directory-and-cultnet-refuse; DirectoryMessagePackBackingStore.cs 139-144 and 642-643). Admit re-resolves every pulled variant with the cache's codec (CultCache.cs 3259). Soul's N4 mutant (the attach lends no codec) was caught only by the reflection test, and h4's M2 only by a bare-store test that setsLoadedand callsUseRegistryby hand. - Probe (scratch clone
imagination-store-gate-r2/clone, commits 4663934e and c099b9e8 onec94ab98; Yggdrasil t1.log, t2.log and t3.log; 2026-10-25 around 20:50 CEST). The probe did three things.InStoreHoldrefuses a cacheless call that re-enters the same store's gate. The_codecfield and the two-argumentUseRegistryare deleted. The pull builds variants with a null codec. Results:- Mesh is 342/0/4.
- In the Caching suite, the only failures were two probe tests, both from authoring
errors: a key
AddAsyncminted, and an element id that was not 12 hex characters. No existing test failed,UserCodeInACachelessCallMayCallItsOwnStoreincluded. - Soul's P1 as a test: the journal's write is refused with the new message. The attach
keeps its registry and the file holds only
a. After the attach, a cache write, a flush andstore.PullAll()all succeed. - A cacheless write nested in another on the same store is refused, the outer write fails, and no file is written.
- Through a real attach, a store holding a name-override variant flushes an unrelated
commit with a
cultcache.store.v2header. With no codec loan, the pulled copy does not mark the store. - Mutant
return cachelesschanged toreturn false, which is ec94ab98's behaviour, fails both new gate tests. In that mutant the nested write succeeds and the file exists.
Rationale.
- Attachment has one fact again. While the attach runs, the store's gate is the attach's
hold, and
Cacheis published when that hold ends. A cacheless write is a write the store owns outright, so it must be the outermost hold of its store's gate. One more argument to the existing owner of per-thread gate facts (InStoreHold) refuses it. No field mirrorsCache, andLoadedand_registryare not read. Precedent:ConcurrentHashMap.computeIfAbsentrefuses a mapping function that updates the same map withIllegalStateException("Recursive update")since Java 9. That is from memory, not fetched this pass; the 2026-10-23 prior-art note covers the documented restriction in its counter-positions. Non-cacheless store calls from user code still re-enter:UserCodeInACachelessCallMayCallItsOwnStorestays green. - The late publish stays. It is what bounds
Gateat two passes (s2's finding). Publishing early would bring back the failure-rate-bounded retry. - The codec loan goes: it is a value nothing on the attach path reads. A pulled variant
carries no codec and counts as holding ids (the documented conservative answer), and
the cache's resolved copy is what any header decision sees. This reverses the codec
half of Defaulted ruling
late-publish-needs-attach-codec, so it goes to Self asvariants:question:pulled-variant-codec. The spec is written for the recommended option. - Scar check:
git log -Sover CultCache.cs forreentrant,outermostand_attachingfinds nothing.Cache?.Codecappears in1468a83a(h4) and619e089fonly. No earlier re-entry refusal was built and deleted. - Behaviour change, stated in the CHANGELOG: a cacheless write made from user code that
another cacheless write of the same store runs used to succeed, and its
finallytook the outer call's registry. It is now refused.