Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Meta Vens
94f20eee5e cultnet-ts server: close() waits for the goodbyes to leave the socket; peer reaping is opt-in and TypeScript-only
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:51:34 +01:00
Meta Vens
ae0b9e234f cultnet-ts: changelog and README for the many-peer RUDP server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:32:01 +01:00
Meta Vens
37a2b002db cultnet-ts: CultNetRudpSocketTransportServer serves many peers on one socket, one reliable session per remote endpoint (matches Rust server hub and C# socket server; wire unchanged)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:30:29 +01:00
Meta Vens
1f495156b5 cultnet-rs-presence-provider-a: one Rust provider of gamecult.runtime_presence_health.v2 for Idunn-managed services, every platform; custody follows delivery; signer owns health; write-lease guard; route answer; no network (squash of hands/cultlib-gaps-presence-provider-a f1941449..ed48b5e6; Soul s1-s3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:08:26 +01:00
f194144913 Research: presence provider library/launcher split prior art (cultlib-gaps B4b2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:07:21 +01:00
Meta Vens
64b86e0a27 write-ownership-cs: a write owns a record by the written type; one-batch retype is eviction then add (squash of hands/variants-write-ownership a4eae905..8c7252ea)
A write owns the record at its key when the written type declares the
record's schema id, or reads an older undeclared id of its own schema name
whole (ruling own-entry-reads-whole); every other registered type's record
is refused, in every cache, with or without a backing store
(cache-refuses-retype).

Removing a record and adding one of another type in a single batch is
staged as the held record's eviction then the add, so validators, Claim,
the store judges and the change journal see what they see staged apart
(one-batch-retype-is-eviction-then-add); Watch<old> gets Removed and
Watch<new> Added. A one-batch retype across home stores is refused by the
batch-spans rule and must be staged apart. A variant rebase changes type
exactly where a plain write may (variant-rebase-follows-plain-write).
Store kinds outside the library get one protected per-key judge
(store-kind-per-key-judge), exercised by tests/GameCult.Caching.OutsideStoreTests.

Spec variants:cut_spec:cut-write-ownership-cs.r1; reports h1-h5;
Soul s5 clears the merge (verdict variants:verdict:cut-write-ownership-cs.s5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:22:20 +01:00
Meta Vens
a4eae90561 Unity CHANGELOG: remote writes to a CultNetDatabaseServer need a granting authority
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 02:07:59 +01:00
Meta Vens
a924512e01 Garden path: remote writes need a granting authority (cut remote-write-authority)
Merges hands/garden-path-remote-write-authority (51ca81a1..1e73e206).
cultmesh-garden-path:cut_report:cut-remote-write-authority.h1/h2; Soul s1 and s2 hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 02:07:44 +01:00
Meta Vens
1e73e20645 RemoteWriteAuthorityTests: pin the refusal on the wire, the aliased delete's canonical ask and the null decision
A real LiteNetLib pair on ephemeral loopback ports carries the handlers' answers to a client that decodes them with the
production schema decoder; no fixed port and no global frame provider. The delete under a schema alias and a null
decision are pinned where each is decided.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:43:18 +01:00
Meta Vens
49594e4901 RemoteWriteAuthorityTests: pin that the refusal is built and sent, and a reasonless refusal's text
Mutation triage: removing the refusal exception's constructor body survived
because the handler threw on the uninitialized peer either way; the helper now
requires the failure to come from the peer's send, not from building the message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:10:03 +01:00
Meta Vens
85dbd47dcc RemoteWriteAuthorityTests: subscribe through R3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:48:53 +01:00
Meta Vens
5a80f2adca cultnet.error.v0: add code write_not_authorized across C#, both schema copies and Rust
Captured-bytes case in cultnet-rs error_message.rs, asserted byte-for-byte by
the C# schema contract test; the cultnet-distributed-database contract and the
Networking README state the remote write authority invariant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:47:24 +01:00
Meta Vens
b4f494b7a0 CultNetDatabaseServer: a peer's put or delete commits only when its write authority grants it
CultNetDatabaseServerOptions.RemoteWriteAuthority is asked once per received
write, after the schema resolves and before the shard does; none configured
refuses every remote write; an authority that throws refuses and is logged.
A refusal applies, publishes, logs and forwards nothing. The host's own
CultNetDatabase.ApplyPutAsync/ApplyDeleteAsync ask no remote authority.
Existing server tests whose subject is not the grant pass a test-local
granting authority.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:47:24 +01:00
Meta Vens
cf1e4f7a9d CultNetDatabaseServer: delete the ApplyPutAsync/ApplyDeleteAsync pass-throughs
A second public door named like the peer path with no caller besides the
handlers. Hosts keep CultNetDatabase.ApplyPutAsync/ApplyDeleteAsync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:44:09 +01:00
Meta Vens
4b3bb26e76 Garden path map: stage 1 rulings fill the model page; GP1 and F13-F17 for the remote write door
Every fork the model page waited on is ruled, so its cells now carry the
rulings. GP1 confirms at runtime that the database server applies a put
from a peer that carries no identity. F13-F17 record what the remote
write authority cut stands on: peers have no identity, refusal codes are
a coordinated cross-runtime enum, forwarding re-sends as a new client,
the door has no consumer outside CultLib, and the Rust document server
has the same gap. The cut sequence splits authority collapse into its
remaining lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:36:36 +01:00
Meta Vens
51ca81a1d4 AOT cut 1: consumers with the MessagePack generator on can declare CultRecordRef<T> members and keys
Merges hands/cultlib-aot-record-ref (b7a68884..3d4efc89).
cultlib-gaps:cut_report:cut-aot-record-ref-generator.h1, Soul verdict s1 holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:19:05 +01:00
Meta Vens
16d68660de Write-ownership map: body facts across runtimes, authority table, cut sequence
Probed at b7a68884: only the C# cache arm retypes a key (cache {A,B}
replaces B's record with A); cacheless writes already refuse. TS, Rust
and Python key records by (type, key), so two types coexist at one key
and no leaf has a retype path; leaf parity needs key identity first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:08:03 +01:00
Meta Vens
3d4efc893a GeneratorConsumer: skip every ref-keyed dictionary serialization under NativeAOT
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:03:21 +01:00
Meta Vens
e3fe3a32f6 GeneratorConsumer: upsert through UpsertAsync
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:01:41 +01:00
Meta Vens
5c55f9cb15 Assume CultRecordRef<T> formattable for MessagePack's source generator consumers
The package ships MessagePackAssumedFormattable(typeof(CultRecordRef<>)) as an
MSBuild AssemblyAttribute in buildTransitive, so generated resolvers defer the
ref to CultDocumentResolver. Geometry imports the targets and drops its three
per-member [MessagePackFormatter] workarounds. GeneratorConsumer is the
generator-on twin of ConsumerBuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:59:13 +01:00
Meta Vens
b7a68884c6 Land C2a: CultCache document variants, element ids and write ownership (3.0.0 Unreleased)
Merges hands/variants-c2a at 7605bd17. Verified at 463714c4 (code tree
identical): every C#, TS, Rust and Python suite green on Yggdrasil and the
old-reader refusal gate refusing in all four runtimes
(variants:cut_report:cut-land-c2a-main.h1, Soul variants:verdict:cut-land-c2a-main.s1).
No tag: tags, Unity plugin rebuild and the declared-range gate are the 3.0.0 release cut.
Non-building branch commits exist; bisect with --first-parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:56:22 +01:00
Meta Vens
7605bd1777 C2a land map: steps 3 and 4 closed, step 5 held by Soul
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:56:04 +01:00
Meta Vens
463714c48f Land map: step 6 records the land is --no-ff and why; steps 1, 2 and 5 marked closed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:43:09 +01:00
Meta Vens
3157d4b325 Merge origin/main into hands/variants-c2a (docs and research only) 2026-10-27 23:42:49 +01:00
Meta Vens
7a7b76efdd Research: prior art for structural versus name-inclusive schema ids (serves variants store-identity question)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:34:41 +01:00
Meta Vens
723b799668 Caching docs: state exactly what a cacheless write owns and refuses
The CHANGELOG bullet, the persistence-contract paragraph and the registry param docs of WriteRecord and WriteDocument now list the two ways a record is owned and every refusal the code makes, including a stored entry that lists another registered type's id ahead of the written one's and a call that writes no entry. The registry-less Shared read is described as it is today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:24:16 +01:00
Meta Vens
f80634df97 Caching tests: an unattached store's Delete then PushAll writes no entry and so owns no stored record
A cacheless call that writes nothing declares nothing and no written entry can claim the record, so the removal is refused and the file left as it was. Red under the Any-to-All mutant of the declared predicate in OwnsOnDisk, which owns every id when no entry is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:24:16 +01:00
Meta Vens
a731eaf3dc Research: prior art for a process-global default registry deciding write ownership (serves variants❓registryless-write-reads-shared)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:12:25 +01:00
Meta Vens
648045a201 Tests: a lookup over several written entries shadows by the entry that has the id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:48:44 +01:00
Meta Vens
928a5ff2ca Tests: the shape probe passes the registry's compatible ids so the lent entry would own it
The lent-shape test omitted the old id from the passed entry, so the base code refused it for the id and not for the shape. It now lists them, making the registry's wider entry the only thing that could own the record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:26:17 +01:00
Meta Vens
959f7e4965 Caching: a cacheless write's written entries are the only description of their schema ids
CatalogEntryLookup now takes the lent registry and answers from the written entries plus the registry's other types (descriptors whose schema id no written entry has). The registry Owns overload, the subset predicate and OwnsOnDisk's lent branch are deleted; the passed entry alone describes the written type, so a lent registry only adds claimants and can only refuse more.

Not yet built or run (verified by the job that follows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:19:34 +01:00
Meta Vens
be12d75a01 Map: claimant-one-owner r2 body facts (one description of the written type decides ownership)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:15:34 +01:00
Meta Vens
fc74cf6f41 Tests: C# refusal lines carry their source and the gate counts them distinct
The console logger repeated each line, so a raw count was 28 for 14 refusals. Names now prints the source (committed or live) and the gate counts distinct 'OLD-READER <runtime> refused <label>' lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:35:23 +01:00
Meta Vens
44faddbc3f Tests: count the C# refusal lines in the TRX, where each test's output appears once
The console logger repeats every test's output in its summary, so the log held 28 lines for 14 refusals.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:33:23 +01:00
Meta Vens
0b4606cbd4 Tests: every old-reader gate stage counts the refusal lines its probes printed
A case that returned or passed early is Passed but prints no 'OLD-READER <runtime> refused' line. Each stage now requires the pinned number of those lines (14 C#, 3 Rust, 4 Python, 3 TypeScript) and fails '<stage>-refused-count' otherwise.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:31:33 +01:00
Meta Vens
ce46a40153 Tests: rename the seeding loop variable that shadowed the record (414cf6e1 does not build the Caching tests)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:22:18 +01:00
Meta Vens
414cf6e1ee Tests: rename a lambda parameter that shadowed the record (f744817a does not build the Caching tests)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:19:57 +01:00
Meta Vens
f744817a15 Caching: a lent registry never widens what the passed entry declares; docs state every refusal
The cacheless written test now holds on the entry passed: the claimant's compatible ids must all be listed by the written entry of its id.
CHANGELOG and WriteRecord's registry doc describe the refusal through another registered type's listed id, the condition on own drift, and the Shared caveat. Not yet built or tested here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:18:30 +01:00
Meta Vens
848fbcd4c5 Gate: a skipped or expected-failure Python case is not a passed one
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:13:48 +01:00
Meta Vens
c8d02949e0 Gate: fix the Python failure-count awk (unescaped paren)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:07:57 +01:00
Meta Vens
e82bf9a3c0 Gate: a failing Python probe is not a passed case
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:07:52 +01:00
Meta Vens
86bd050149 Gate: count passed cases, fail on any not-Passed result, pin the 1.0.60 writer vector by hash
C# stages count TRX results with outcome Passed per class and fail '<stage>-notpassed' on any other
outcome (an [Ignore]d case is NotExecuted); Python also fails on skipped=, expected failures= and
unexpected successes=. Every stage prints its passed count. The committed 1.0.60 writer vector is
compared to a recorded sha256 (the 1.0.60 writer stamps wall-clock times, so no writer stage can
reproduce its bytes).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:02:18 +01:00
Meta Vens
01a750afb2 Tests: a cacheless write never takes a record a same-named version of its schema claims
Kills the written-by-name mutant (the claimant condition compared schema names instead of ids).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 08:37:10 +01:00
Meta Vens
cad173ef11 Tests: AnUnpublishedIdIsOwnedByNoOne passes the claimant condition (d3a4d0a3 does not build the Caching tests)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 08:22:27 +01:00
Meta Vens
d3a4d0a3bc Caching: a cacheless write owns a record only through a claimant it writes
Owns takes a condition on the claimant's answer (mayClaim): the cache arm accepts any
claimant, the cacheless arm only one of the entries the call writes. Another registered
type's record, under its current id or an old one it lists, is refused as at 0e57bf11.
Tests pin P1a/P1e/P1f shapes, the writer's own drift under a larger registry, and that a
second same-name type in the lent registry only refuses. CHANGELOG entry for the cacheless
claimant; stale comments on the store's owns predicate and WriteDocument's registry doc fixed.

Builds not yet run when committed; verified on Yggdrasil before push.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 08:19:21 +01:00
Meta Vens
50a4d73f29 Tests: a gate stage that prints no REFLIST-SUMMARY line fails by name instead of aborting
summary_of ended in a grep that exits 1 on no match; under set -e that killed the script before it named the failed stage. With the
head ref-list test red (owns-declares-only) the gate exited 1 naming nothing; it now names head-ref-list and summary-diff.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 08:11:08 +01:00
Meta Vens
dcae9aa763 Tests: the 2.0.1 reader stage pins ten OldReaderGateProbe cases, not twelve
The five TestCaseSource tests each run over two sources (committed, live): ten cases, plus the one OldReaderRefListProbe case. The
first run of the count check reported 10 and 1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 08:03:17 +01:00
Meta Vens
703786f40d Tests: close the 2.0.1 ref-list probe's method (it did not compile at the previous commit)
OldReaderRefListProbe.cs is staged into the 2.0.1 tree by old-reader-refusal.sh and is not part of this tree's build; its method
lacked a closing brace at fd9bbaf4, which the gate's 2.0.1-reader stage reported.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 08:01:15 +01:00
Meta Vens
f41dc3d63b Tests: every gate stage asserts how many cases it ran
old-reader-refusal.sh pins the expected case count of each stage as a constant (C# stages from the TRX the run writes, by class; Rust,
Python and TypeScript from their own output) and fails naming '<stage>-count' on any other number, so a probe renamed out of its filter
or a skipped leaf test no longer passes with nothing run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:59:19 +01:00
Meta Vens
7c7bafe30e Tests: regenerate the C2a gate vectors with the fixed clock
Only StoredAt moves (Soul s1 vec2.log); the writer is deterministic (two runs, identical trees). The directory page is renamed by its new
content hash. A README says how they are made and compared.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:59:07 +01:00
Meta Vens
fd9bbaf4a4 Tests: the ref-list gate compares schema ids after one write at both trees; the C2a writer takes a fixed clock
The 1.0.60 ref-list summary is taken after attach, an upsert of alpha, beta and the list reversed, and a flush, at 2.0.1 and at
head, and names records by schema id (the restamp-on-write ruling makes them equal). C2aGateWriter stamps every record with one
instant and compares the committed vectors byte for byte, the directory store as relative path and bytes with lock files left out.
The committed vectors are regenerated in the next commit, so TheCommittedVectorsAreWhatC2aWrites is red at this commit; it builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:57:29 +01:00
Meta Vens
338035eacb Tests: a record under an id its entry only lists is published by that entry
Kills the Stryker survivor FirstOrDefault to First in PublishedBy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:46:17 +01:00
Meta Vens
b23ed3f1d6 Tests: a registry-less record write takes its one entry as claimant
Kills the two survivors of the claimant lookup over written entries (Named inverted, Owner dropped).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:25:50 +01:00
Meta Vens
6fb0e22c41 Tests: pin the unpublished-id rule at Owns (a store file cannot hold such a record)
ReadStore refuses a record whose schema the catalog does not publish, so the
file-level scenario the spec named cannot be built; the rule is pinned where it is decided.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:17:04 +01:00
Meta Vens
dc924da605 Caching: ClaimantFacts is a plain struct (the netstandard target has no IsExternalInit)
The previous commit does not build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:13:55 +01:00
Meta Vens
bf29ddb33d Caching: one claimant precedence; a cacheless writer picks the claimant its registry picks
CultDocumentRegistry.Claimant is now the only precedence that decides which entry
reads a persisted catalog entry, static over a lookup (a registry's indexes, or the
entries a registry-less cacheless write writes). ClaimantEntry is deleted. A cacheless
WriteDocument or WriteRecord lent a registry is never more permissive than a cache
registering the same types; an unpublished id is carried, not owned, through Owns' catch.
Fixes the four Low findings of variants:verdict:cut-restamp-on-write.s2.
Not yet verified on Yggdrasil when committed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:13:21 +01:00
Meta Vens
0e57bf113e c2a-land map: refresh onto 7c7cc799 (gate r7, claimant-one-owner) and the remaining landing sequence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 07:09:43 +01:00
Meta Vens
73e684b1b2 Research: whole-view flush cost on Aetheria's catalog, measured (serves variants:ruling:page-tree-store-research)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:57:52 +01:00
Meta Vens
dbc94e023f Research: prior art for a paged single-file CultCache store (serves variants:ruling:page-tree-store-research)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:47:16 +01:00
Meta Vens
7c7cc79904 Contract: a cacheless write asks the same ownership rule with its one entry
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 06:40:56 +01:00
Meta Vens
6a20309ded CultMesh garden path target r2: built-for-load invariant; scope note narrowed to the postponed storage engine
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:33:03 +01:00
Meta Vens
fe44c59f2a Research: sizing a .NET WebTransport server (serves cultmesh-garden-path:ruling:browser-transport)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:27:29 +01:00
Meta Vens
0c19d080b3 Cut the read-whole flag's copies in Resolved and Flatten: the staging half asks only the store's loaded view
Hand mutants dropping each copy survived the full Caching suite; the flag is read only through the store's Entries (RefuseUndeclared), which
ToStoredDocument sets, so the cache's resolved variants and flattens were never asked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 06:26:15 +01:00
Meta Vens
94fe281c02 Research: C# wasm measuring probe (Mono interpreter, Mono AOT, NetWasm) on a fixed-point orbs sim
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:24:16 +01:00
Meta Vens
10d012186a Test: owned drift variants run on the single-file store with a codec
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 06:15:10 +01:00
Meta Vens
2137b10efb One ownership rule for cache and cacheless writers; pin owned drift variants and the cross-schema guard
Owns is now a static rule over (declared, claimantOf): a registry supplies both from its indexes, a cacheless writer supplies its one entry, so
the hand-written cacheless lambda is deleted and a renamed entry listing the writer's id is owned by both. The schema-name check is the
claimant selection and is pinned. Variant read-whole flag pinned for rewrite, flatten and remove.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 06:14:11 +01:00
Meta Vens
baffd774f0 Research: prior art for the six garden-path stage-1 forks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:08:40 +01:00
Meta Vens
dee12a70cf cultmesh-garden-path map: model page (identity, lifecycle, authority), body facts F1-F12, stage 1 cut lines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:02:49 +01:00
Meta Vens
e6ffe28043 Test: a lossy drift record is refused before the store's lock is asked
Kills the staging-flag hand mutant that the apply half's refusal hid.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 05:43:02 +01:00
Meta Vens
f87d12b9ae Cut the dead StoredSchemaReadWhole defaults and Wrote sets; test the refusal text, the cacheless declaration and the unnameable claimant
Stryker found the true default and the two Wrote sets equivalent: a constructed or written record is stored under its type's declared id, so the flag is never asked. Not run at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 05:34:43 +01:00
Meta Vens
d1bf21b3ff cultlib-gaps map: AOT cut 1 body facts (MessagePackAssumedFormattable via buildTransitive) and AOT cut sequence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 04:51:28 +01:00
Meta Vens
eb6a8f2c3c Research: CultCache C# under NativeAOT, probed (serves cultlib-gaps:follow_up:cs-aot-safe-registration)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 04:19:19 +01:00
Meta Vens
e3fac9c2b0 Research addendum: correct the CultCache reflection claim (registration-time descriptor, not proven per-record IL emit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 04:03:48 +01:00
Meta Vens
8c78610cbc Research addendum: NetWasm, and CultCache's runtime reflection as the C#-in-browser prerequisite
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 04:01:53 +01:00
Meta Vens
a768b821fc Tests: three gates are reached by a stale guess followed by a settled cache, ordered with the store's hooks
The spec's account (guess null, then a successful attach) takes two gates, not three: the guess gate is taken only for a non-null guess.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:54:12 +01:00
Meta Vens
438562ef4c Research: running C# simulation code in the browser (serves cultmesh-garden-path open fork 1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 03:52:03 +01:00
Meta Vens
2181a7461f Tests: the racing cache holds its records in a routed store
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:51:01 +01:00
Meta Vens
5d66bb0ad4 Tests: fill the racing cache through AddAsync
Does not build-verify at the previous commit (Upsert is not on CultCache).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:49:58 +01:00
Meta Vens
bcefcf9382 Tests: declared-lossy id constant; successful-churn gate test races an attach with a long pre-publish check
Not run at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:49:16 +01:00
Meta Vens
3d2687b976 CultMesh garden path: draft target (what a developer writes, invariants, sequence)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 03:46:33 +01:00
Meta Vens
da951adce0 Tests: fixtures that were read-only because their type read the record whole now lose a slot; restamp tests fixed
The ForeignRecordTests read-only fixtures (another runtime's deck, the moving deck) had the same shape as the deck, which the
type now reads whole and owns; each gets a slot the deck lacks, so it is read only by dropping it. RestampOnWriteTests and the
successful-churn gate test are corrected from the first run. Not run at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:46:27 +01:00
Meta Vens
9a4dc1c8e3 Research: prior art on multiplayer framework ergonomics and scale-out (serves the CultMesh garden-path direction, operator 2026-10-27)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 03:45:20 +01:00
Meta Vens
1607434535 Record the reference-game choice for the CultMesh garden path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 03:40:12 +01:00
Meta Vens
5036244778 Tighten the gate-bound tests: two gates under failing churn, exactly one when detached or settled, three reached under successful churn
Not built or run at this commit: verified on Yggdrasil at the cut's head.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:39:02 +01:00
Meta Vens
0fbc12275a Restamp on write: one ownership rule, Owns, under which a type that reads a stored record whole may write it
CultDocumentRegistry.Owns replaces Declares as the one answer to whether a writer may write, relabel or remove a record under a
schema id: the id is declared, or the type that reads it reads it whole (equal content hash, or compatible shapes with no persisted
slot ignored). Staging asks the declared half or CultStoredDocument.StoredSchemaReadWhole; the apply halves (single file,
directory, cacheless CultMesh) ask Owns over the durable catalog, once per distinct id. A record read only by dropping slots
stays read-only until its id is declared compatible.

Not built or run at this commit: verified on Yggdrasil at the cut's head.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:39:02 +01:00
Meta Vens
d0c7981b3f Record the operator's CultMesh product direction beside the witness research
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 03:37:56 +01:00
Meta Vens
8ee6671410 c2a-land map: refresh onto 193cf1c3 (restamp r2, gate r6) and the landing sequence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 03:31:13 +01:00
Meta Vens
53f84e4e2f Research: prior art on direct store writes behind an attached cache (serves variants:follow_up:public-store-writes-behind-cache)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 03:26:28 +01:00
Meta Vens
0ad9811f88 Note why CultMesh's witness path is trusted-co-op first
CultMesh was built for trusted co-op (Aetheria: Starbridge) with witness
authority deliberately deferred, so the current shape is the intended
first stage rather than a failed attempt at Byzantine tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 06:18:01 +01:00
Meta Vens
70ee27fe94 Add witness-authoritative networking prior-art research
Read-only survey of CultMesh's current witness path against crash-fault
consensus, CRDTs, BFT, game netcode and P2P anti-cheat, plus design notes
from the 2026-10-26 discussion: optimistic peer quorum with a server audit
slow path, network-proximity witness selection, dispute-scoped verification,
and cross-runtime determinism as a security property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 06:02:53 +01:00
Meta Vens
193cf1c39e Store gate: Cache owns the attach window; pin the three-gate bound; fix the unseen-cache wording
A cacheless write that re-enters its own store's hold now consults Cache first, so a journal's WriteDocument or WriteRecord gets the same refusal during an attach as after it (the attached text); the re-entry text no longer names an attach's pull. InStoreHold takes the store instead of its gate.
CacheBackingStore.GateTakes (internal, thread-static) counts the gates Gate takes; OneStoreCallTakesAtMostThreeGatesUnderFailingAttachChurn asserts at most three per call under failing-attach churn, which pins the stale-guess loop (M4b).
The contract and AddBackingStore say what holds: other threads may guess the tentative cache, but none runs a body against it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:35:58 +01:00
Meta Vens
74021e2326 Caching tests: after the refused journal call the attached store still pulls
The folded-in writedoc case used to assert the refusal left the attach's registry in place and a later PullAll succeeded; the per-entry-point test now asserts that for every entry point.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:08:06 +01:00
Meta Vens
2a77a312db Caching tests: the per-entry-point attach test upserts its later record under a fixed key
The previous code commit's AJournalOnTheAttachingThreadCannotReachItsStore failed all 21 cases:
AddAsync mints its own key, so "later" never appeared. That commit builds; its new test is red.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:49:23 +01:00
Meta Vens
c8a17ccd13 Caching docs: Cache is published for the pull (contract, CHANGELOG)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:39:17 +01:00
Meta Vens
c66b4c9e04 Caching: the attach publishes Cache before its pull and takes it back in the same hold; Gate is straight-line with a settled cache
Cache is the one attachment fact. AddBackingStore, the only writer, publishes it right after the
registry loan and sets it back to null in the catch, all inside its hold of both gates, so a
journal's store call during the attach is refused as it is after it (it used to stage or write
records the cache never admitted). Gate loses MaxPasses and its loop: the guessed cache's gate,
the store's gate (where Cache cannot change), then the settled cache's gate. Tests: one per store
entry point through a real attach (single-file and directory), each asserting the attaching cache
and a fresh cache agree after a flush; the second-store refusal pinned before the same-store one;
the three attach tests renamed to the published-cache shape. The old
AJournalOnTheAttachingThreadCannotWriteItsStoreCacheless is the writedoc case of the new test.

This commit builds; docs follow in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:39:17 +01:00
Meta Vens
2894b36c84 c2a-land map: store gate, Cache is the one attachment fact (store-gate-one-owner r3: entry-point table, shapes weighed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:35:22 +01:00
Meta Vens
0fa836cb46 Caching tests: attaching no store is refused (the argument check had no test)
Found by the scoped mutation run over AddBackingStore. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:27:59 +01:00
Meta Vens
6353055b72 Caching tests: the nested cacheless write propagates its refusal; the variant header pin overrides the name beside the reels
Fixes two tests of the previous commit that failed on their own fixtures (the inner call's
exception was swallowed in the hook; a variant that shares its base's name is refused). Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:14:38 +01:00
Meta Vens
22c87fd530 Caching: a cacheless write never re-enters its store's gate (Soul s3 attach-window-cacheless-write)
InStoreHold gains a cacheless flag: on re-entry of the same gate a cacheless call throws
instead of running, so the attach's hold (until it publishes Cache) and an enclosing store
call's user code cannot write the store behind the cache. Gate passes the flag; no field
mirrors Cache. Tests through a real attach and a nested cacheless write, a header pin for
attached variant stores, contract and CHANGELOG.

Builds on the previous commit; neither is built or run until the verify job.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:09:12 +01:00
Meta Vens
295738fdd8 Caching: the attach lends the store no codec; a pulled variant carries none (Soul s3 codec-loan-pinned-by-reflection)
Deletes CacheBackingStore._codec and UseRegistry's codec parameter, the reflection LentCodec
test helper and the bare-store codec test. The cache re-resolves every pulled variant with its
own codec when it admits the load.

Not yet built or tested; the verify job at the head covers every commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 20:07:34 +01:00
Meta Vens
97a624d3ee c2a-land map: store gate, attachment is the attach's hold (store-gate-one-owner r2 facts and rationale)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:06:05 +01:00
Meta Vens
ec94ab9878 Caching tests: a variant a pull loads reads its values with the codec the attach lent
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:10:10 +02:00
Meta Vens
2db8d72e35 Caching docs: Cache is published once, after the pull, so a call makes at most two passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:05:02 +02:00
Meta Vens
1ccfe615bc Caching tests: churn readers run under a role that does not wait in the reader
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:03:09 +02:00
Meta Vens
f574a5a25e Caching tests: the attach lends the codec, Cache is published after the pull, Gate's passes are bounded
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:01:30 +02:00
Meta Vens
1468a83a15 Caching: the attach lends the codec with the registry and publishes Cache only after the pull
UseRegistry(registry, codec) is the single writer of both loans; the pull builds
variants with the store's own codec, a failed attach takes both back through the
same call, and AddBackingStore sets store.Cache last. Cache now goes from null to
a cache at most once, so Gate's retry makes at most two passes: the 1,000 stale
cap becomes MaxPasses = 2 and its comments say why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:01:29 +02:00
Meta Vens
a616431ec5 Caching: the gate's stale-guess cap is 1,000, so a validation that runs the body again and again throws before the suite times out
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:05:12 +02:00
Meta Vens
ece44a84be Caching docs: the store-composition contract says what the cache gate order enforces today and names the cross-cache read follow-up; CHANGELOG: a failed attach takes its registry back
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:34:06 +02:00
Meta Vens
ba6589650f Caching: a failed attach takes its registry back (one UseRegistry writer), and a gate retry that never settles throws instead of looping
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:33:01 +02:00
Meta Vens
a2b8d7d28d Caching tests: a failed attach leaves no registry, the Soul P4 load pins the refusal and body in one hold, every cache gate entry and an own-store call keep the order rule (red: failed attach)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:33:01 +02:00
Meta Vens
751d9f34e0 Caching tests: a reader queued before an attach runs under the cache's gate after it; an attach pull may read its own cache; user code may call its own store
Kills the survivors of the first mutation table (no recheck in the store branch, a Gate() that refuses the held cache, a stale
retry result, a same-store reentry refusal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:15:42 +02:00
Meta Vens
d676e4a334 Caching tests: the gate tests dispose their caches after the joins, so a regression fails a bounded join instead of hanging in Dispose
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:02:40 +02:00
Meta Vens
3c5f1fe0c2 Caching docs: the lock order is cache gate, store gate, file lock; the gate owner re-checks after locking (contracts, CHANGELOG)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:01:43 +02:00
Meta Vens
217a94807a Research: prior art on writing back a record read under an old schema (serves the C2a 1.x-store write fork)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:59:33 +02:00
Meta Vens
1eb517296b Caching tests: the PullAll-after-failed-attach test queues on the second attach; a cacheless PullAll without a registry is not a refusal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:57:13 +02:00
Meta Vens
577526bb28 Caching: a thread holding a store's gate enters no cache gate it does not hold and no second store
CultCache owns the per-thread gate facts: _storeHeld joins _held. InStoreHold marks the store gate; Gate() is where every
cache gate entry (holds and reads) refuses while that mark is set and the gate is not already the thread's. A getter run by a cacheless
call or an attach that reaches a cache or a second store throws instead of deadlocking against an attach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:54:53 +02:00
Meta Vens
fba7ac1e8a Caching tests: the write-through-a-getter test uses AddAsync (CultCache has no Upsert)
The previous commit's tests do not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:45:32 +02:00
Meta Vens
45529db85d Caching tests: user code under a store's gate cannot enter a cache or a second store, and a queued reader runs under the right gate (red)
Re-entrant getter during a cacheless write (read and write), a getter against the attaching cache, an attach pull that enters
a second store, and readers queued on a failed attach (snapshot, PullAll). Every thread join is bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:41:31 +02:00
Meta Vens
9c8973a73e Caching: Cacheless is private protected, so SingleFileBackingStore's WriteRecord and WriteDocument reach it
The previous commit does not build: Cacheless was private to CacheBackingStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:38:51 +02:00
Meta Vens
b348978d31 Caching: one owner of store gate selection; delete ForTheCall, RefuseWhenAttached and Held's unchecked choice
CacheBackingStore.Gate<T>(body, cacheless) reads Cache as a guess, takes that gate, checks the guess under it
and retries when stale. A cacheless call's refusal is the store-gate branch's validation outcome, in the same hold
as its body. WhileDetached becomes UnderStoreGate over CultCache.InStoreHold; AddBackingStore tests Cache, not Loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:38:26 +02:00
Meta Vens
7b1cd00c2d Research: prior art on running user code under a store lock (serves variants❓store-hold-user-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:30:47 +02:00
Meta Vens
36b22931a3 Gate: every csharp stage runs and the gate names the stages that failed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:29:53 +02:00
Meta Vens
89d170344a ref-list-drift: the store the 1.0.60 writer makes for a list of record references, written on Yggdrasil at cultlib-unity-v1.0.60
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:28:33 +02:00
Meta Vens
c9da27b28c Gate: the ref-list summary names records by schema name; a flush carries an untouched record's schema id forward here and restamps it at 2.0.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:27:18 +02:00
Meta Vens
8efffc1038 Gate: the 1.0.60 writer keys its records alpha, beta and list, so the reference list resolves
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:23:57 +02:00
Meta Vens
7b5f8ca7c3 Gate: the 1.0.60 writer names its types as RefListDriftTests does, and the directory vector keeps the lock its writer leaves
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:22:51 +02:00
Meta Vens
79ed75bedc Gate probe: the 2.0.1 ref-list probe reuses 2.0.1's own RefListDriftTests document types
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:21:18 +02:00
Meta Vens
6dd3ebd0d4 C2a gate vectors: the v3 single-file and v5 directory stores C2aGateWriter writes, written on Yggdrasil at de78734f
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:20:00 +02:00
Meta Vens
de78734f4d C2a format gate: the gate harness, a C# gate probe, leaf probes on the C2a-written vector, and the 1.0.60 ref-list probes
old-reader-refusal.sh gains gate <runtime> and per-runtime old revisions (b8fc5eb0 = cultlib-unity-v2.0.1,
02cf04b5 = cultcache-rs-v0.3.1, 45c2f400 = cultlib-unity-v1.0.60). C2aGateWriter writes the live v3 and v5 stores.
OldReaderGateProbe replaces OldReaderV3Probe. The vectors themselves follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:18:57 +02:00
Meta Vens
e06d5a2e8c CachelessSingleFileTests: the second cache's record is keyed by its name's hash, so assert the file's three records, not the key "c"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:21:38 +02:00
Meta Vens
e38925e249 Caching: the cacheless call reaches the store's gate through WhileDetached (the base class owns the gate; 694e6fe6 did not build)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:18:12 +02:00
Meta Vens
8b780a27bc cultcache-py CHANGELOG: registration refusals are SchemaConflictError, one owner per schema id, records resolve by schema id first
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:17:32 +02:00
Meta Vens
694e6fe632 Caching: a cacheless call takes the store's own gate and refuses under it; the attach holds that gate across its whole body
The call no longer picks its gate from a read of Cache (Held), so a call queued on a cache's gate during an attach that then fails
cannot run beside a second cache's attach. Lock order: cache's gate then the store's, in the attach only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:16:44 +02:00
Meta Vens
748b2d8082 Caching tests: a call arriving during an attach waits for it, success or failure; a refused attach leaves the store cacheless (red)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:12:27 +02:00
Meta Vens
5ea97b44a7 CachelessSingleFileTests: WriteDocument plans ids by the declared type (S9)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 04:14:02 +02:00
Meta Vens
792f14bbef CHANGELOGs: cultmesh-ts C2a breaks, cultcache-ts exported types, Unity ForeignRecords and AsSchemaAlias interface removal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 04:11:54 +02:00
Meta Vens
38dfa60804 cultcache-ts: the header peek is the one judge of a stamp; isStoreSnapshot is a shape test; byte-offset views, array16/32 and str16/32 leads and a spaced stamp pinned
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 04:11:53 +02:00
Meta Vens
13706c795f Cacheless calls and attach share the store's gate: an attach waits for a call, a call that finds the cache is refused
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 04:07:10 +02:00
Meta Vens
8619883b96 cultcache-ts tests: an empty array before a newer stamp is not a stamp
Hand mutant T4 (lead >= 0x90) survived; the empty-array case now carries a newer stamp after it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 02:01:05 +02:00
Meta Vens
a4e70ac4fb Cacheless calls detach their registry when they end; the declared type decides the header; both pinned
Mutation of the restore line and of the header's Holds(runtime type) survived the previous tests (Stryker/hand mutants C1, C7). Not yet built.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 02:00:16 +02:00
Meta Vens
3704ce097c CachelessSingleFileTests: the gated store reads the attached registry without requiring one (7d25d92b did not pass: the gate test threw)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 01:49:31 +02:00
Meta Vens
7d25d92bd7 CachelessSingleFileTests: a call keeps its registry while another call waits at the gate
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 01:46:09 +02:00
Meta Vens
a056918be6 CHANGELOGs list the measured public surface against 2.0.1 (apicompat strict): members on their owning types, Id members in Mesh, Networking and Geometry, Rust SchemaConflictError, cultnet-rs register
The Unity list no longer calls MarkFlushSucceeded removed and places HeaderFor and the CacheBackingStore members where apicompat finds them. cultnet-rs gains a CHANGELOG. The store-composition contract moves DecodeDocument to the codec and states that a call's registry serves that call only. Docs only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 01:44:57 +02:00
Meta Vens
d1dfd8f027 cultcache-ts: the reader judges the stamp from the header before it decodes the body; the header-form pins return
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 01:38:16 +02:00
Meta Vens
19f3a5edbb Cacheless single-file API: WriteDocument encodes the declared type, the registry is the call's argument, DecodeDocument moves to CultCodec, ReadStoreSnapshot's gate is pinned
SerializePayload takes the declared type. A call attaches its registry under the store's gate and restores the previous one, so no call leaves a registry on the store. Not yet run; the next commit set is verified on Yggdrasil.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 01:38:16 +02:00
Meta Vens
f54472e5fe Merge origin/main (cultcache-rs 0.3.1, the whole-store writer fix) into the C2a landing branch
The stamp functions (Rust refuse_unread_format, TS refuseUnreadFormat) are the single owner of the stamp rule, accept v1 and v3, and are called only from the one reader every whole-store write runs under the lock. The fix's header peek (TS refuseUnreadStoreBytes, peekHeader) is deleted. Its tests that v3 is refused and that a stampless file is healed are dropped; the reversal is a Breaking line in the Rust and TS CHANGELOGs. The 0xff-after-stamp test now runs over v1 and v3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 00:44:59 +02:00
Meta Vens
baee4b6ba1 CachelessSingleFileTests: the attached-store refusal test uses a registered document type; storedAt round-trip format
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 00:32:22 +02:00
Meta Vens
2f17721eba CachelessSingleFileTests: WriteDocument and DecodeDocument pin storedAt, random versus deterministic ids, null arguments and the attached refusal before the registry
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 00:29:03 +02:00
Meta Vens
02cf04b5c9 Merge hands/whole-store-writers-read-header: Rust push_all and TS pushAll read the header under the lock and refuse a newer store format (cultcache-rs 0.3.1; Soul s2 holds)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 00:17:07 +02:00
Meta Vens
bf01212f9a Docs: cacheless single-file access names WriteDocument and DecodeDocument; CultElementIds is not public
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 00:15:28 +02:00
Meta Vens
729754278a Caching: SingleFileBackingStore.WriteDocument and DecodeDocument own element-id planning; CultElementIds is internal again
Mesh's typed single-file read and write call the store's typed members instead of planning ids through a public CultElementIds. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 00:15:27 +02:00
Meta Vens
6463f34203 CachelessSingleFileTests: null arguments, the lock wait, and the registry deciding the header for copied records
Stryker on WriteRecord left three NoCoverage statements (the two null guards, AttachRegistry) and a survivor (AcquireLock wait: true to false). Each now has a test that fails when it is mutated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 00:07:30 +02:00
Meta Vens
bd40c05dff cultnet-ts: pin the text() sanitiser around a chunk's hash and id
The lone-surrogate row now carries the value in the chunk hash and id slots, so removing text() there fails. Closes follow_up content-id-sanitiser-pin's pin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:58:08 +02:00
Meta Vens
74bcf075a4 PreCut2 tests: the v6 suffix refusal asserts the counted suffix, not the absence of the readable stamps it also names
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:57:51 +02:00
Meta Vens
bc6d524bdf Contracts: cacheless single-file access; never reuse a dropped slot
Store-composition gains the Cacheless single-file access section; schema-compatibility states the slot-reuse consequence per variants:ruling:slot-reuse-documented. Owns follow_up slot-reuse-contract-doc.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:57:38 +02:00
Meta Vens
388e2eaa25 CHANGELOGs: every public break since cultlib-unity-v2.0.0, the unity package releases as 3.0.0
Owns finding c2a-public-breaks-not-in-changelog. Leaf changelogs gain v3 read and write and element ids carried; cultcache-rs gets its first changelog (the whole-store writer fix is not on main, so no 0.3.1 section).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:57:37 +02:00
Meta Vens
73ffa6f8d0 Caching: CultElementIds.Plan, Holds and IdPlan are public, the third internal CultMesh reached
CultMeshSingleFileDocuments mints, checks and counts element ids through CultElementIds, which the deleted GameCult.Mesh InternalsVisibleTo also granted; without the grant Mesh does not build. A cacheless typed writer has to plan ids before it serializes, so the three members it needs are public and nothing else of the class is.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:54:54 +02:00
Meta Vens
6c9e4aa509 cultcache-rs: one grown decoder setup for every decode of stored bytes
store_header and decode_grown both run through grown(); the copied maybe_grow and red-zone setup is deleted. Owns finding store-header-second-grown-decoder.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:53:11 +02:00
Meta Vens
52e6095f5c Pin the stamp rules: empty-array directory manifest refused, stamp-first rows name their refusal, DescribeHeader cases
DirectoryMessagePackBackingStore judges the parsed snapshot's stamp after the stamp-first read (an array with no stamp parses to v1 and was opened as a store). The NewerStampOverUnreadableBody rows assert the fragment each refusal names, plus a binary stamp over a valid body. DescribeHeader gets its suffix, digit-bound and zero-digit cases. Owns findings empty-array-directory-manifest-opens, stamp-first-unpinned-at-store-paths, describe-header-suffix-unpinned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:53:10 +02:00
Meta Vens
26bf0986d0 Caching: public ReadStoreSnapshot and WriteRecord replace Mesh's InternalsVisibleTo reach
SingleFileBackingStore gains the cacheless single-file read and write CultMesh used through internals; CultMeshSingleFileDocuments is now a caller of the public API only, and the GameCult.Mesh InternalsVisibleTo grant (with the duplicate Tests/MessagePack grants) is deleted. Owns finding mesh-ivt-moved-not-removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:52:33 +02:00
Meta Vens
5baa02fa54 Merge origin/main (CultLib 2.0.1) into the C2a landing branch
CHANGELOG: C2a's Unreleased above main's [2.0.1], verbatim.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:46:31 +02:00
Meta Vens
8aac0d0b27 cultcache-ts tests: a stamp followed by bytes that are not UTF-8 is a stamp, as a read judges it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:43:37 +02:00
Meta Vens
1a3780621e cultcache-ts: the whole-store writer decodes the stamp with the decoder a read uses
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:40:21 +02:00
Meta Vens
0820681533 cultcache-ts: the whole-store fix joins the untagged 0.14.2 section; the version stays 0.14.2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:40:21 +02:00
Meta Vens
b8fc5eb077 Merge hands/cultlib-2-0-1-ref-list-drift: org.gamecult.cultlib 2.0.1, 1.0.x stores whose ref collections gained reference semantics open as compatible drift
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:31:22 +02:00
Meta Vens
eacc3240fc cultnet-ts test: a non-string id refusal names its field (kills the StrykerJS survivor on the field label)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 23:00:14 +02:00
Meta Vens
d7cc039028 cultcache-ts tests: bytes that only look like an array or a string are not a stamp
Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:47:23 +02:00
Meta Vens
1eeb1a01fb org.gamecult.cultlib 2.0.1: plugins regenerated by build-unity-package.ps1 -UpdateTemplate
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:42:28 +02:00
Meta Vens
edabd05ae4 Caching tests: the lost-reference case uses its own schema name
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:42:27 +02:00
Meta Vens
b4a906c59c cultcache-ts tests: the header peek over every MessagePack array and string form, soft pushAll over a newer store
Kills the StrykerJS survivors in refuseUnreadStoreBytes's header peek. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:40:40 +02:00
Meta Vens
126714c82a Caching tests: a lost reference still refuses
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:37:49 +02:00
Meta Vens
c53789f197 Caching tests: the ref-list store reports compatible drift, then flushes through PushAll
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:35:53 +02:00
Meta Vens
5a2234950f Caching tests: readable ref-list assertion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:28:10 +02:00
Meta Vens
d3573f79fa Caching tests: the ref-list store flushes and reads back
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:26:05 +02:00
Meta Vens
86c58c8695 cultcache-ts 0.14.3: a full pushAll reads the store header under the lock and refuses a format it does not read
refuseUnreadFormat is shared by isV1Snapshot and the writer's header peek; message text unchanged. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:26:00 +02:00
Meta Vens
6809e74137 cultcache-rs 0.3.1: push_all reads the store stamp under the lock and refuses a format it does not read
One owner of the stamp rule, refuse_unread_format, serves read_all_unlocked and push_all. A file push_all cannot read is written as before. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:26:00 +02:00
Meta Vens
4adcc21674 cultcache-rs, cultcache-ts tests: a whole-store write refuses a format it does not read (red)
Both new tests fail at this commit; it builds. Vector copied byte-identical from C2a (blob 9fc9b64f).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:22:05 +02:00
Meta Vens
ba05fb7351 Caching tests: the ref-list vector names the test type as the runtime does
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:21:34 +02:00
Meta Vens
972e7b2ed4 Caching: a reference gained on an otherwise identical slot is compatible drift (1.0.x ref-list stores open); 2.0.1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:16:14 +02:00
Meta Vens
eabb50e68c Caching tests: the persisted descriptor carries its own content hash
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:14:21 +02:00
Meta Vens
a88b5930ec Caching tests: ref-list drift test compiles
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:11:28 +02:00
Meta Vens
3e65477f22 Caching tests: a 1.0.x store with a ref-list slot opens at 2.0 (red); reference gain is the only drift excused
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:01:34 +02:00
Meta Vens
68f70ba490 content-vectors.ts-written.json regenerated by the script: five-slot chunk references
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:00:41 +02:00
Meta Vens
2c9043357e cultnet-ts: chunk references carry the element id (slot 4), as the reference reads and writes it; changelogs for the flush-over-damage refusal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:55:15 +02:00
Meta Vens
a70c9844e5 Content acceptance vectors regenerated from the reference: slot 4 is the id, extras sit past it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:55:07 +02:00
Meta Vens
e324ef48fd Content vectors tests: chunk-ref extras sit past the id slot; add id-slot cases (chunk_id_str, chunk_id_int, chunk_id_str8, chunk_6)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:33:49 +02:00
Meta Vens
7964d81acc org.gamecult.cultlib CHANGELOG: flush over damage refuses; stamps judged before the body (Unreleased, major)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:11:21 +02:00
Meta Vens
87957b2d1a Land fixes: ported conditional-commit pins state C2a's identity, not main's (storedAt, message text)
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>
2026-10-22 21:06:10 +02:00
Meta Vens
f2cdb7897d Land fixes: directory flush records the schema id it wrote; ported drift and variant tests meet C2a's write set and typed refusal
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>
2026-10-22 20:59:54 +02:00
Meta Vens
c14b2c65d1 Land fixes: stamp judged before the body, deep stores on the grown stack, ported tests refuse by their typed errors
Stamp rule runs on slot 0 before the body is scanned (JudgeStamp). DescribeHeader names a v<N> header that carries a suffix. Rust store_header walks on the grown stack. Drift tests declare Wide's id compatible (ownership by declaration). Mesh write-guard tests expect the typed refusals.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:49:43 +02:00
Meta Vens
f2b3569822 Merge main at cultlib-unity-v2.0.0 (42dce361) into C2a: one owner per collision
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>
2026-10-22 20:35:33 +02:00
Meta Vens
0888ce6a71 Merge readme-front-door: README front door
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:58:51 +02:00
Meta Vens
8c14df1a93 README front door: entry points and live/planned up front; drop absent NewtonsoftJson package
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:57:46 +02:00
Meta Vens
42dce3611a Merge hands/cultlib-unity-release-2.0.0: org.gamecult.cultlib 2.0.0 and org.gamecult.caching.unity 2.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:03:47 +02:00
Meta Vens
ef45e4ed82 org.gamecult.cultlib 2.0.0: the parser's comment and the flush bullet state the exact stamp rule; declare the nil-stamp refusal (Soul s5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:03:29 +02:00
Meta Vens
3994f5d4c6 org.gamecult.cultlib 2.0.0: Unity plugins regenerated; the public snapshot parser reads any stamp again
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 18:53:39 +02:00
Meta Vens
4a14f17270 Public DeserializeSnapshot parses any stamp as in 1.0.61; stores judge the v1/v2 stamp in their own read paths; flush prose states the exact rule; the I/O refusal test asserts no write was reached
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 18:34:15 +02:00
Meta Vens
17b0288e42 org.gamecult.cultlib 2.0.0: Unity plugins regenerated with the header-first stamp check and the read-only I/O refusal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:53:52 +02:00
Meta Vens
6adc321a73 Caching: a binary-encoded stamp is refused; directory manifest readers name their stamp rule; the I/O refusal test fails the real read
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:33:24 +02:00
Meta Vens
39e8ab5efd Single-file and directory snapshot reads judge the format stamp before parsing the body; a flush heals an empty or truncated file
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:30:57 +02:00
Meta Vens
b02cb0ab3d org.gamecult.cultlib 2.0.0: Unity plugins regenerated with the base-owned flush guard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:14:12 +02:00
Meta Vens
8012b68863 CHANGELOG 2.0.0 Breaking: the nesting bound, SendSchema's SocketException, RUDP session endings; the flush guard rule
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:03:07 +02:00
Meta Vens
0d043f7347 Mesh tests: the variant record carries its empty payload
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:03:07 +02:00
Meta Vens
b151b11bed Mesh tests: the single-file write waits for the store lock and refuses a variant at its own key
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:01:42 +02:00
Meta Vens
0bf11ddc94 Caching: SingleFileBackingStore.PushAll owns the flush guard; it reads the disk as attach and commit do, ReadFormatVersion is deleted
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:00:10 +02:00
Meta Vens
b2c08e6ac6 org.gamecult.cultlib 2.0.0: Unity plugins regenerated with the flush and directory format guards
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:50:28 +02:00
Meta Vens
0f9d57eb48 CHANGELOG 2.0.0: a flush and a directory write refuse a store stamped newer since attach
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:45:00 +02:00
Meta Vens
afd4858b9a Caching: qualify FormatNumber at the single-file flush guard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:43:37 +02:00
Meta Vens
d44188637d Caching: a plain flush and a directory write refuse a store another process stamped newer since attach
Single-file PushAll reads the format stamp under the lock and refuses cultcache.store.v<N> with N > 2;
the directory store throws NotSupportedException for N > 4 from the manifest parse every write already runs.
An unreadable stamp keeps today's behaviour.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:43:11 +02:00
Meta Vens
5fd72ba216 org.gamecult.cultlib 2.0.0: native QUIC bridge rebuilt from the source that changed since 1.0.61
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:22:20 +02:00
Meta Vens
5e53b7ff67 org.gamecult.cultlib 2.0.0: Unity plugins regenerated by build-unity-package.ps1 -UpdateTemplate
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:16:49 +02:00
Meta Vens
8b47a2e8d9 Caching: PersistedSchemaId and MarkWritten are internal for 2.0; Mesh single-file write guard in the CHANGELOG
C2a's StoredVersion owns the same identity and deletes both. No consumer outside CultLib uses them,
so they do not ship public in 2.0.0 and C2a's release is not a major.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:57:01 +02:00
Meta Vens
7678148c1a CultMesh single-file write reads what it replaces: refuses a newer-format, unreadable or larger store
The write replaced the file without reading it, so a 2.0 writer pointed at a store a newer
format stamped (or one holding other records) destroyed it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:55:42 +02:00
Meta Vens
6df826f5c2 Research: consumer pinning at a breaking land (serves variants❓unpinned-consumers-at-land)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:52:52 +02:00
Meta Vens
81814d7854 C2a land map: what C2a must still prove to merge to main (measured against main 7a8933b0 and the 2.0.0 candidate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:49:58 +02:00
Meta Vens
6bdf88e281 Release prep: org.gamecult.cultlib 2.0.0 and org.gamecult.caching.unity 2.0.0 versions, changelogs, install docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:44:39 +02:00
Meta Vens
5b5d857375 Merge remote-tracking branch 'origin/release/cultlib-unity-1.0.x' into hands/cultlib-unity-release-2.0.0
# Conflicts:
#	unity/org.gamecult.cultlib/CHANGELOG.md
2026-10-22 15:40:35 +02:00
Meta Vens
f91e30eded Caching tests: a store stamped with a newer format version is refused and never rewritten
Pins the release-now-c2a-bumps-format ruling's premise for 2.0.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:39:40 +02:00
Meta Vens
7a8933b085 Merge hands/cultmath-release-0.5.0: CultMath 0.5.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:59:20 +02:00
Meta Vens
6afc2b438b CultMath 0.5.0 changelog: no serializer in this package, site functions without shader twins named, license expression change stated
Finding cultlib-gaps:finding:cut-cultmath-release-0-5-0.s1.changelog-claims-absent-surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:53:54 +02:00
Meta Vens
6d211cad68 CultMath 0.5.0 changelog: the SNOISE_HESSIAN const re-pin is a break (semver-policy, unmeasured changes)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:26:13 +02:00
Meta Vens
251e42552a CultMath 0.5.0: Unity plugin DLL and pdb rebuilt on Windows at 0.5.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:18:49 +02:00
Meta Vens
23c40420ec CultMath 0.5.0: version, changelog (CultPhasor rename, SNOISE_HESSIAN re-pin, Cycles, Expr, site and lattice cells, span stages), cultlib dependency pin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:08:31 +02:00
Meta Vens
4a1c2df698 Store-composition contract: MarkWritten guards accidental marks; a mark is the store's own claim
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:10:26 +02:00
Meta Vens
22eaaabeb2 Caching docs: MarkWritten refuses an entry the store does not hold (contract, CHANGELOG)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:59:38 +02:00
Meta Vens
afd799ba8d Caching: CacheBackingStore.MarkWritten is an instance method that refuses an entry the store does not hold
A protected static mark let any subclass mark another store's or cache's
entry. The instance form throws ArgumentException unless the entry is the
one this store holds in its own Entries at that key. Both library stores
already mark only their own entries, so their calls are unchanged.

Finding cultlib-gaps:finding:cut-expect-persisted-schema.s2.markwritten-unscoped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:59:38 +02:00
Meta Vens
2c8b44ac1d Caching tests: a custom store cannot mark an entry it does not hold (red)
Finding cultlib-gaps:finding:cut-expect-persisted-schema.s2.markwritten-unscoped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:58:54 +02:00
Meta Vens
0625b86ce6 org.gamecult.cultlib CHANGELOG: CacheBackingStore.MarkWritten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:41:53 +02:00
Meta Vens
73d438dbcf CultCache: every backing store marks the records it rewrites, through a protected CacheBackingStore.MarkWritten
The directory store's write of a generation marks each page it wrote once the
manifest names it, so a Push+PushAll of a loaded drifted entry no longer leaves
its persisted schema id stale. The single-file store marks through the same
member, and a store outside GameCult.Caching now has a supported way to mark
its own rewrite. The getter's doc names how a store records a rewrite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:41:53 +02:00
Meta Vens
3a320d144d Caching tests: a store's own rewrite of a drifted entry marks it, including a consumer's store; a merge is judged on another writer's change to a held record
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>
2026-10-21 21:40:08 +02:00
Meta Vens
e88bf11920 Caching tests: compare the persisted schema id with the writer's descriptor, not a version label
Schema ids are content hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:06:51 +02:00
Meta Vens
bc3407d017 Caching tests: a directory re-read refreshes a record another writer rewrote under another schema
The directory store's pull filter compares the persisted schema id too; without a same-storedAt rewrite on that store nothing pinned it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:06:29 +02:00
Meta Vens
2bf4e21ba8 org.gamecult.cultlib CHANGELOG: Expect under compatible schema drift
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:05:31 +02:00
Meta Vens
c681af39d9 store-composition contract: identity is the persisted schema id plus storedAt
The claim that every write mints a later storedAt is false for a whole-file write, which keeps storedAt and rewrites a drifted record under the writer's schema. State what the identity is and what a rewrite does to another cache's Expect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:05:29 +02:00
Meta Vens
3f6dda8b36 CultCache: conditional commit compares the schema id a record is persisted under
The six reads that took Descriptor.SchemaId as a record's durable identity (ConditionsHold, Expect, the store-less commit, both pull filters, JudgeMerge) now read CultStoredDocument.PersistedSchemaId. The store sets it from the record it read, a stamped entry starts at its descriptor's id, and a completed whole-file write (single-file PushAll, whole-view CommitBatchCore) resets it under the hold. A variant and its resolved view share it. An unchanged record loaded under compatible drift now satisfies Expect and ExpectUnchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:05:23 +02:00
Meta Vens
4ecea25c49 Caching tests: Expect and ExpectUnchanged on a record loaded under compatible schema drift
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>
2026-10-21 21:02:57 +02:00
Meta Vens
5d81761ee1 cultlib-gaps map: Expect under compatible schema drift (E1-E7) and the cut line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:57:54 +02:00
Meta Vens
b549d73183 cultcache-ts CHANGELOG: scope the C# claim to what the code does
Fixes variants:finding:cut-ts-store-write.s2.changelog-csharp-any-write-overclaim:
a C# conditional commit on a clean store lands onto the current file; other
C# writes can still overwrite concurrent TypeScript records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:31:05 +02:00
Meta Vens
4a2acf0e35 cultcache-ts: state plainly what the 0.14.2 lock protects
push and delete no longer lose records across concurrent TypeScript and Rust writers. A full
pushAll and any C# write still replace the whole store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:43:28 +02:00
Meta Vens
a3137cf3a6 scripts: the pack gate asserts checkPayloadDepth is exported, and CI runs it
cultcache-ts 0.14.1 re-exports checkPayloadDepth from the document entry on purpose, but the
closure script still asserted it undefined, so test:pack:ts was red and no workflow ran it. The
assertion now expects a function and the TypeScript job of cultmesh-portability.yml runs the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:43:28 +02:00
Meta Vens
289d7f7d49 cultcache-ts: pin the default write path's sync and close order, and a directory at the lock path failing fast
The default StoreFileOps were never observed. One test now wraps fs/promises.open and each handle's
sync and close, and records the store's bytes at every call: the staged file is synced and closed
while the store still holds its old bytes, and on Unix the directory is synced and closed after the
store changed. A second test puts a directory at <store>.lock and requires the lock to fail within
3 s with the lock path named, under the default 30 s wait bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:42:24 +02:00
Meta Vens
bf8c72f04a cultcache-ts: Windows tests that no handle outlives a replace or a lock wait
A share-none open (what the C# lock and readers need) must succeed after the replace
and after a waiter gave up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:56:56 +02:00
Meta Vens
6696ab6db6 cultcache-ts: drop a redundant clause in the Windows replace, export the bound kernel for real-kernel tests, add survivor-killing tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:44:39 +02:00
Meta Vens
d5a4f7a9da cultcache-ts: tests for push/delete/pushAll semantics, lock-free reads and the replace fallback rules
Written to kill the StrykerJS survivors of the first scoped run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:28:53 +02:00
Meta Vens
ed432b59da cultcache-ts 0.14.2: version, CHANGELOG, README persistence model, store-composition contract
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:54:14 +02:00
Meta Vens
e6d8e7e30b cultcache-ts: cross-runtime store lock tests against the C# and Rust peers
Both directions: a TypeScript write waits on a C# or Rust holder; a C# or Rust writer
waits on a TypeScript holder; the lock wait bound names the lock path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:51:48 +02:00
Meta Vens
3cd9880c04 interop peers: hold-lock mode in the C# peer and the Rust example
Each holds <file>.lock by its own runtime's protocol (share-none open; fs2 exclusive),
prints {"held":true}, sleeps --ms, releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:50:19 +02:00
Meta Vens
0010ee92a2 cultcache-ts: single-file store runs every write under the <path>.lock cross-process lock
#enqueue takes withStoreWriteLock; the legacy heal moves out of pullAll's read into a
queued, locked operation that re-reads; push and delete read through #readDisk so they
never enqueue from inside the locked queue (a self-deadlock). Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:50:18 +02:00
Meta Vens
43124c9ad5 cultcache-ts: tests for the store commit order, failure paths and Windows replace
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:47:19 +02:00
Meta Vens
0e6452c91b cultcache-ts: store-file.ts owns the store commit and replace; delete renameWithRetry
commitStoreFile stages a unique temp, fsyncs it, replaces the store and fsyncs the
directory on Unix. Windows replaces POSIX-semantics first with a write-through
MoveFileExW fallback and a 25x20 ms retry on 5/32/33 (koffi, loaded lazily). The
cross-process lock functions land in the same file but are wired in a later commit.
package-lock.json also carries pre-existing version drift from regeneration.
Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 14:47:19 +02:00
Meta Vens
b9497d3501 cultmesh-browser 0.4.0: version, ranges, CHANGELOG, README floor and redirect statement
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:13:34 +02:00
Meta Vens
92c6f42346 cultmesh-ts 0.2.0: version, cultnet-ts ^0.3.0, CHANGELOG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:13:33 +02:00
Meta Vens
7cc3bf88fa cultnet-ts 0.3.0: version, cultcache-ts ^0.14.1, CHANGELOG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:13:33 +02:00
Meta Vens
a98b09c732 cultcache-ts 0.14.1: version and CHANGELOG
Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:13:32 +02:00
Meta Vens
48c1937674 Merge campaign docs (target, map, research) into release/cultmesh-ts-content
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:10:42 +02:00
Meta Vens
ae8d655cf8 Merge C3 reader branch into release/cultmesh-ts-content
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:10:42 +02:00
Meta Vens
7edaf08c17 cultmesh-browser fixtures: .NET 10 output for the 201 endpoints
Generator run in mcr.microsoft.com/dotnet/sdk:10.0; the three added rows are exceptions UriBuilder throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:06:22 +02:00
Meta Vens
0d7f9a8433 cultmesh-browser fixtures: three endpoints whose query holds a scheme-like text, for the generator
These pin that the authority-form check is anchored at the start of the endpoint; their .NET output follows in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:06:01 +02:00
Meta Vens
f0be628be6 cultmesh-browser: the unallocatable-size test uses chunks an int can hold
The owner now refuses a chunk past 2147483647, so the oversize manifest is 32768 chunks of that size (about 64 TiB) instead of one 2^53 - 1 chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:50:04 +02:00
Meta Vens
cdeda66a90 cultmesh-browser: URL parity from the .NET fixture, option-independent hash tests, rejection timed where it is decided
The reader refuses a scheme without an authority form, escapes of bytes past ASCII and IPv6 literals that embed an IPv4 address; parity is tested against chunk-uri-dotnet.tsv. Wrong-bytes tests run at maxBytes equal to the size and at concurrency 1 and 32. The stall test measures settlement against a 100 ms timer and counts requests started after the failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:48:07 +02:00
Meta Vens
1082b55f51 cultnet-ts: validateManifest refuses sizes the reference's types cannot hold
Manifest total and chunk sizes must be whole numbers below 2^53, and a chunk size must fit an int (2147483647), for hand-built values as well as decoded ones. Messages for the whole-number rule now name the bound. No C#-written vector changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:48:06 +02:00
Meta Vens
508625e20e cultmesh-browser fixtures: the whole .NET 10 output of chunk-uri-dotnet.fsx over 198 endpoints
The generator's run in mcr.microsoft.com/dotnet/sdk:10.0 (UriBuilder body of BuildChunkUri); 8 rows are exceptions UriBuilder throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:48:05 +02:00
Meta Vens
11218b8889 C4 fix: parity fixtures carry a recordKey with every Uri.EscapeDataString special character
A fourth fixture (escape) has a chunk recordKey of k!'()* <e-acute>%41+~. The provider's honest mount answers that one key as the canonical key
after logging the raw URL, so both consumers fetch the bytes and the log holds what each actually sent. The orchestrator asserts the C# and TS
URLs are identical and equal the RFC 3986 escaping. Deleting the reader's !'() * escape now fails test:interop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:45:35 +02:00
Meta Vens
9585586424 cultmesh-browser fixtures: .NET 10 output for the first 150 endpoints, and the endpoints the old tables held
Rows past 150 still await the generator; the next commit holds its whole output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:44:43 +02:00
Meta Vens
0d90f20886 cultmesh-browser fixtures: the endpoint list and generator for the .NET chunk-URI expectations
The second column is filled by the next commit, from the generator's run on .NET 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:41:59 +02:00
Meta Vens
424913bfb7 C4: cross-process HTTPS content parity (C# transfer service, TS provider, TS reader)
The TS provider runs in its own process on node:https with the fixture credential. The TS reader and the C# transfer service over the unmodified HTTPS connector (CustomRootTrust, fixture certificate only) each fetch from it; the orchestrator judges URL parity, the empty chunk, corrupt-store faults and the untrusted control. test:interop is wired in cultmesh-browser and the workflow, and the cultmesh-ts step now precedes the browser step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:45:10 +02:00
Meta Vens
cebe6ae724 C4: loopback HTTPS test credential for the content parity test
Self-signed P-256 pair for 127.0.0.1, CA:FALSE, serverAuth, with the openssl recipe in the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:40:24 +02:00
Meta Vens
51a00bb326 Merge origin/hands/cultmesh-ts-content-c3-reader (67ff3f64) into c4-parity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:36:46 +02:00
Meta Vens
64862d4a15 Docs: cultmesh-ts-content map pass 3 (C4 facts F28-F33, cross-process probes, C5 dependency note)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:26:18 +02:00
Meta Vens
67ff3f6410 cultmesh-browser: pin cause messages, escape grammar rows, drop redundant once options
Mutation sweep survivors: manifest-invalid causes now asserted by field and rule, escaped-unreserved and
broken-escape endpoint rows added from .NET probes, and the two {once:true} options that the finally removal
already makes redundant are deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:20:18 +02:00
Meta Vens
326223e5d1 cultmesh-browser: drop the post-read abort check from fetchVerifiedChunk
No await separates the read from the return there, and the failure race already rejects an abort that lands during the chunk hash; a mutant deleting the line survived every test because no window exists to pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:52:21 +02:00
Meta Vens
7adbf3cbd9 cultmesh-ts/content tests: hold the FIFO writer until every started read settles
Releasing once freed only the four running opens; the queued four blocked again
and the reverted-open run still hung. The writer now outlives all started reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:34:52 +02:00
Meta Vens
3479f7426a cultmesh-ts/content tests: release the FIFO's blocked opens without the thread pool
The finally block hashed (a pool job) before releasing, so under a reverted
non-blocking open it deadlocked behind the full pool; the path is now kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:31:11 +02:00
Meta Vens
5e90f3d3df cultmesh-browser README: redirects, plain endpoints and unallocatable sizes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:26:58 +02:00
Meta Vens
d2590a6fd3 cultmesh-browser: pin the reader's bounds in every spelling, and make stalls fail instead of spin
Wrong-bytes refusals at 1..4 MiB+1 for both exports and mid-manifest; the .NET URL table (accepted rows
equal, divergent rows refused); redirect, late abort, stalled fetch and unallocatable size cases; port,
trailing-empty, 2xx and multi-chunk length rows; waits are bounded and abort waits fail on timeout.
Test types adapted to C1's nullable chunk refs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:26:26 +02:00
Meta Vens
5998ee95a0 cultmesh-browser: reader refuses redirects and URL-divergent endpoints, surfaces failure at once
- redirect: "error" on every request, so no byte comes from a place the endpoint gate did not see
- endpoints on which .NET UriBuilder and WHATWG URL differ are refused (insecure-endpoint) instead of translated
- the first failure or caller abort rejects without waiting for a fetch that ignores its signal
- a caller abort landing after the last byte rejects instead of returning bytes
- an unallocatable size is too-large, not a raw RangeError
- the reader's own whole-number size rule is deleted; validateManifest owns it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:26:26 +02:00
Meta Vens
c2bb187843 cultnet-ts: validateManifest owns the whole-number size rule
A manifest or chunk size that is a fraction, NaN or Infinity is refused in the one
validator. C# sizes are long, so the wire cannot carry one; the rule guards hand-built
manifests. The reader's private copy is deleted in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:23:47 +02:00
Meta Vens
9a2b74d14f cultmesh-ts/content tests: pin blank-key edges, every-hop buffer reuse, char devices, fast FIFO failure
U+FEFF/U+0085 key cases; microtask sweep 0..10 asserting never 200 with other
bytes; /dev/null and /dev/zero symlinks refused as non-regular; the FIFO test
releases blocked opens in finally so a blocking-open revert fails in seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:21:51 +02:00
Meta Vens
2ab66c8922 cultmesh-ts/content: decide a blank recordKey with C1's IsNullOrWhiteSpace owner
cultnet-ts/content re-exports isNullOrWhiteSpaceCSharp; the provider uses it
instead of key.trim(), which disagreed with C# on U+FEFF and U+0085.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:21:51 +02:00
Meta Vens
3f266688d8 Merge C1 shapes into C3 reader
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:20:59 +02:00
Meta Vens
196222962f Merge C1 shapes (d7911589) into C2 provider 2026-10-19 18:20:23 +02:00
Meta Vens
d7911589d8 Content vectors: regenerate acceptance cases with the document reader's depth rule
Written on Yggdrasil under CULTNET_WRITE_VECTORS=1. 253 existing cases keep
their verdict and bytes unchanged; 14 deep_extra_N / deep_chunkextra_N cases
are added (extra slot accepted through depth 255 / 253, refused from 256 / 254).
cs-written file is unchanged (trailing newline only, not committed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:51:30 +02:00
Meta Vens
9dfda89e9f Content vectors: the C# judge reads manifests with the production document reader
Judge and Reencode (and the other manifest reads in the fixture) now go
through CultDocumentMessagePackSerialization DeserializeUntyped/SerializeUntyped
instead of the CultNet schema options, so the 256-depth rule decides the
vectors as it decides production. Adds deep_extra_N and deep_chunkextra_N
cases each side of 256. Committed acceptance file is regenerated in the next
commit; this commit's committed-file test is red until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:49:47 +02:00
Meta Vens
70325e61b4 cultnet-ts/content: write strings well-formed at every length, through utf8-bound's owner
encodeManifest wrote a lone surrogate as CESU bytes for short strings and
U+FFFD past the msgpack TextEncoder threshold; C# writes U+FFFD always.
utf8-bound.ts now exports wellFormed (truncateUtf8Bytes uses it) and the
manifest encoder routes every string slot through it. Test crosses the
short/long boundary for every slot. Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:49:31 +02:00
Meta Vens
d850f16a3a cultnet-ts/content: cut the nil pre-checks the array header read already refuses
A nil manifest, chunks slot or chunk element reaches the array header read, which refuses it with the same field
name; the three pre-checks were equivalent mutants in the Stryker sweep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:43:48 +02:00
Meta Vens
c2a6b69879 Content vectors: regenerate acceptance cases (truncation, header families, integer widths)
Written by the C# test under CULTNET_WRITE_VECTORS=1 on Yggdrasil (sdk:10.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:24:44 +02:00
Meta Vens
d5468e40c3 Content acceptance: truncation, header families, integer widths; cut redundant null checks
Stryker found 30 survivors in the reader. Those that were real gaps get C#-judged vectors (early ends, a fixint or
fixmap where an array or map header must be, every integer family at a value needing its width and sign, zero-filled
fixext, extras at the header-family edges) and a test that a refusal names its true cause (negative size, nil chunk,
reserved code, early end). The null checks ahead of isNullOrWhiteSpaceCSharp were redundant with it and are gone.
Vectors regenerate in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:23:56 +02:00
Meta Vens
5a23835739 cultmesh-browser content tests: pin the query trim and the error name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:11:07 +02:00
Meta Vens
4820ac74c9 cultmesh-ts/content: serve a private copy of the hashed bytes, open the store non-blocking, test the input-shaped mutants
The handler copies the store's bytes before hashing and sends that copy, so a
reused store buffer cannot change the answer. The directory store opens with
O_NONBLOCK so a FIFO named by a hash is refused by fstat instead of holding a
libuv thread. New tests: exact record-key compare and blank keys, empty-hash
prefix lookalikes, single store read, buffer reuse, FIFO (POSIX, uid-independent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:09:15 +02:00
Meta Vens
7bb1733569 cultnet-ts README: the decoder follows the reference's acceptance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:53:10 +02:00
Meta Vens
f7579e2999 cultmesh-browser content: trim every leading '?' like TrimStart, drop the unneeded sort, test the survivors
BuildChunkUri trims all leading '?' of an existing query; the reader trimmed one.
Chunk placement is by offset, so sorting the refs decided nothing and is gone;
the size guard checks sizes, which fix the offsets. New behavioural tests for
the mutation survivors: absent recordKey, fractional/infinite sizes, the
fetchVerifiedChunk byte-cap boundary, undefined crypto, error messages,
abort-listener removal.

Builds and passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:53:10 +02:00
Meta Vens
29f9c701ea Content vectors: regenerate acceptance cases with chunk-reference extras
Written by the C# test under CULTNET_WRITE_VECTORS=1 on Yggdrasil (sdk:10.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:52:33 +02:00
Meta Vens
fe2cfee877 Content acceptance vectors: extras inside a chunk reference, one per MessagePack family
An extra slot that ends the manifest hides a skip that stops short; one inside a chunk reference is followed by more
bytes. Two hand mutants (skip-map-counts-pairs-once, skip-ext-length) survived without it. The vector file is
regenerated in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:51:24 +02:00
Meta Vens
9d12ec392b cultnet-ts content test: encode 64-bit boundary values as bigints
The encoder writes a number past uint32 as float64 under useBigInt64; the old tests leaned on the decoder accepting
that float. The reference refuses a float, so the fixtures now use bigint (uint64/int64) encodings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:44:12 +02:00
Meta Vens
edc6f5fbbd Content vectors: reference-written acceptance cases, trap keys, padded pack vector
content-acceptance-vectors.json and cdn-record-key-vectors.json are written by the C# test under
CULTNET_WRITE_VECTORS=1 (Yggdrasil, sdk:10.0); content-vectors.ts-written.json gains the whitespace-version, padded
kind and MIME vector from the TS writer. The cs-written and rs-written vectors are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:42:48 +02:00
Meta Vens
3de72c6333 Content vectors: C#-written acceptance cases, trap inputs for keys, padded pack vector
CultMeshContentVectorTests writes content-acceptance-vectors.json (what the reference accepts, refuses, rewrites and
keys over nil, arity, trailing, integer/float, string-family and invalid-UTF-8 shapes), extends the record-key
vectors with the prefix-fold, fallback-hash traps, and the TS writer adds a whitespace-version, padded kind and
MIME pack vector. GameCult.Mesh exposes internals to its test project so the judge can call ValidateManifest.
The generated files follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:40:55 +02:00
Meta Vens
4db5118af6 cultnet-ts/content: read the manifest as MessagePack-CSharp does
A purpose-built reader replaces the generic decoder: the integer rule sees the wire family (float-encoded
integers refused), nil in a reference slot reads as null and writes back as nil, missing trailing slots take the
reference's initialisers, extra slots and trailing bytes are skipped, invalid UTF-8 decodes to U+FFFD with the BOM
kept, and a map-form manifest is refused as 'manifest'. Validation still refuses nil chunks. Public type change:
string, tags and metadata fields are nullable. The acceptance vectors that judge this land in the next commits, so
content.test.ts is red until the C# writer has produced them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:40:54 +02:00
Meta Vens
ad8ca99d16 cultmesh-ts/content: triage the first mutation sweep (18 survivors)
Builds. Tests added for error bodies and content type, maxChunkBytes 1, a non-ENOENT open failure, the not-a-file message and handle closing; the dead ?? fallbacks in toNodeListener and the unread parameter name are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:39:25 +02:00
Meta Vens
55530a941f cultmesh-browser: document the verified content reader; name it in the portability job
README section for fetchVerifiedContent / fetchVerifiedChunk; the CI comment
records that the content reader is proved to release only verified bytes.

Builds unaffected (docs and a comment).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:28:46 +02:00
Meta Vens
f243d8b512 cultmesh-ts/content: README section and CI step naming
Builds. Docs and a workflow name/comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:28:26 +02:00
Meta Vens
11fd0aa4ff cultmesh-browser: fetchVerifiedContent and fetchVerifiedChunk release only verified bytes
The reader judges received bytes: each chunk streams into its slot of a private
buffer, is cut at the first byte past its size, and is hashed; the whole body is
hashed before the buffer is returned. Length and coding headers are never read.
URLs equal the C# BuildChunkUri; concurrency 1..32, maxBytes, AbortSignal, typed
errors. Hashing, hex, keys and the loopback rule stay in cultnet-ts.

Builds and passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:28:21 +02:00
Meta Vens
2e9a44925c cultmesh-ts/content: a chunk handler that verifies before serving, a read-only directory store, a node:http listener
Builds. The ./content subpath is the only way to reach it, so the package root gains no node:fs import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:28:02 +02:00
Meta Vens
035d851127 Docs: cultmesh-ts-content map pass 2 (C2/C3 facts F19-F27); target verify-before-use judges received bytes, not length headers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:20:47 +02:00
Meta Vens
b8b825e147 cultnet-ts/content: triage the first mutation sweep (47 survivors)
Source: stableToken carries no blank check of its own (artifactRecordKey owns the artifactId refusal, a blank version
never reaches it), the single-unit regex loses its anchors, and the integer reader is Number.isSafeInteger over a decoder
without useBigInt64 (a 64-bit value a JS number cannot hold rounds to 2^53 or beyond and is refused either way).
Tests: error messages name their field (table), map-header bytes at 0/15/16/65535/65536, WebCrypto absent, schema id and
default chunk size, chunk size 1, int32 chunk-size boundary, depth error subject, default parameter names.

Builds. Co-Authored-By below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:51:08 +02:00
Meta Vens
2c40937447 CI: run the C# content-vector suite; document cultnet-ts/content
cultmesh-portability.yml gains 'Verify the content vectors with the C# reference' (CultMeshContentVectorTests), the first
CI run of the C#/Rust content parity and of the TypeScript manifest vectors. The README describes what cultnet-ts/content
owns and how the ts-written file is regenerated.

Builds. Co-Authored-By below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:36:04 +02:00
Meta Vens
0ca81f52da cultnet-ts/content: the TypeScript CDN manifest, codec, hashing, record keys and packer
One browser-safe module owns the manifest and chunk-ref shapes, their MessagePack form (positional ten-array, metadata as
ordered entries, duplicate keys and unsafe integers refused, payload depth through cultcache-ts's own checkPayloadDepth),
normalizeHash/parseSha256Hex, both record-key grammars (StableToken per UTF-16 unit), WebCrypto sha256Hex and packContent.
cultcache-ts/document re-exports checkPayloadDepth. scripts/write-cultmesh-content-vectors.mjs writes the TypeScript
manifest vectors for the C# reference to judge.

Builds. Co-Authored-By below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:35:15 +02:00
Meta Vens
c21bfd03f5 Contracts: C#-written CDN record-key vectors (StableToken and NormalizeHash traps)
contracts/cultmesh/cdn-record-key-vectors.json is written by the reference under CULTNET_WRITE_VECTORS=1 (19 artifact
keys, 7 chunk keys; ids as UTF-16 unit arrays so a lone surrogate survives). The judge compares text with line endings
normalised so a Windows checkout and a Linux one agree.

Builds. Co-Authored-By below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:35:12 +02:00
Meta Vens
26767ca4e1 Mesh tests: C# writes CDN record-key vectors and judges the TypeScript manifest vectors
WriteContentVectors also writes contracts/cultmesh/cdn-record-key-vectors.json, every key derived by the public
CreateRecordKey of the reference. CommittedRecordKeyVectorsEqualAFreshDerivation and
TypeScriptManifestVectorsMatchTheReference judge the committed files; a missing file fails.

Builds. The two new tests are red until the vector files they read are committed (next commits).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:29:27 +02:00
Meta Vens
2de070652d Research: content-addressed HTTP answers (IPFS, OCI, Nix, Bazel, git; Fetch CORS and encoding), for cultmesh-ts-content C2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:27:25 +02:00
Meta Vens
416b4eda9a Docs: cultmesh-ts-content map (body facts F1-F18, model page, cut sequence)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:21:10 +02:00
Meta Vens
8990bd466d Docs: TypeScript CultMesh content plane target (campaign cultmesh-ts-content)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:02:07 +02:00
Meta Vens
a88c629e82 Merge hands/cultmath-glsl-build-dxc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:21:35 +02:00
Meta Vens
ab86876362 build-glsl.sh: delete the [128] shape arm; the shape check runs on a library array of length 128 through the real script
Selection is by name, so a non-site 128 array is legitimate output. The shape check is a function and the array-128 variant passes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:37:44 +02:00
Meta Vens
263fc4b435 spvedit.py: drop the sentinel value check (the wrapper owns the sentinel); test the element-type check with a float3 array of the sentinel length
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 23:42:01 +02:00
Meta Vens
f736994942 build-glsl.sh: test that a consumer's own SPIRV_CROSS_CONSTANT_ID_0 and the built core.glsl coexist
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 23:30:35 +02:00
Meta Vens
72af377a05 build-glsl.sh: the length-in-expression refusal test spells the sentinel in hex, which dxc keeps as the capacity constant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 23:20:48 +02:00
Meta Vens
7416e8a775 glsl build: the capacity constant carries CultMath's own SpecId, so a consumer's SPIRV_CROSS_CONSTANT_ID_0 cannot clash
SPIRV-Cross names the macro from the SpecId and offers no option to rename it, so the SpecId is CultMath's own
('CMS1'); partition.py reads it from spvedit.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 23:00:04 +02:00
Meta Vens
ae838b60fd glsl build: the capacity edit names the site arrays by a sentinel length, with tests for the tool refusals
wrappers.py compiles the library with CULTMATH_MAX_SITES defined as a sentinel no shader file spells, so the three
site arrays get an array length id of their own and spvedit.py converts exactly that constant to the overridable
specialization constant (default from the HLSL guard). A library array of literal length 128 keeps its own id and
stays a plain constant. build-glsl.sh gains the pipeline function and scratch variants: a macro outside a declarator,
a folded expression reaching the capacity id, and a non-site float4 x[128] parameter that a consumer's override must
not resize. The [128] shape arm goes: a non-site 128 array is legitimate output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:59:55 +02:00
Meta Vens
68b74eaee3 cultmath README: Shader Tooling names the strict-form gate and the overridable site capacity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:58:59 +02:00
Meta Vens
38668b6ec0 nocontract.py and run-lavapipe.sh: the strict-form gate under GLSL_DIR (-Gis, NoContraction on both modules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:41:56 +02:00
Meta Vens
7b121f6ab2 build-glsl.sh: capacity checks (a) to (e), and a [128], _cos, _sin shape refusal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:36:35 +02:00
Meta Vens
c2658fa0b2 partition.py: the capacity header at the head of core.glsl, the one spec-constant block accepted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:36:34 +02:00
Meta Vens
40ab7a20c5 spvedit.py: the capacity edit, one array length id becomes a spec constant (SpecId 0)
Does not build alone: partition.py refuses the spec-constant block until the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:36:34 +02:00
Meta Vens
51f284ac6c wrappers.py: array arguments by declarator name, a capacity column, a refusal for CULTMATH_MAX_SITES outside its guard
Does not build alone: spvedit.py still reads four columns until the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:36:31 +02:00
Meta Vens
7c47cbaffd Merge origin/main (phasor-field-rename a085c0b3) into hands/cultmath-glsl-build-dxc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:32:08 +02:00
Meta Vens
a085c0b3f1 Merge hands/cultmath-phasor-rename 2026-10-17 21:05:44 +02:00
Meta Vens
4391ed23f9 CHANGELOG: the old CultPhasor reader decodes zeros by default and throws under Disallow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:33:45 +02:00
Meta Vens
3bf9f7040a CultMathJson tests: legacy phasor names under Disallow, and a third occurrence is a duplicate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:33:44 +02:00
Meta Vens
841b11db94 CultMathJson tests: unknown names are skipped under case-insensitive reading
Kills the surviving Stryker mutant in Fields.IndexOf's case-insensitive loop and covers its not-found return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:44:10 +02:00
Meta Vens
1330362235 CultPhasor rename: README, design note and both changelogs
cultmath-unity [Unreleased] and cultlib [Unreleased] carry the Breaking entries; no version bump, no tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:28:25 +02:00
Meta Vens
58baccce78 CultMathJson writes CultPhasor as cosine/sine and still reads cos/sin
Fields takes an optional legacy name per field, matched on read only and mapped to the same index as the current name, so a legacy and a current name in one object are the same property and the duplicate rules decide (refused when AllowDuplicateProperties is false, last wins otherwise). Writing never emits a legacy name. MessagePack bytes are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:28:09 +02:00
Meta Vens
c027d3f884 CultMath.Phacelle.glsl regenerated for cosine/sine
Written by CULTMATH_WRITE_GLSL=1 on Yggdrasil (sdk:10.0) at d605c70b; sha256 21eef3c38cc609909444c906a1ef965f01b541f2301023a722e64ecc4c846387. glsl-parity.json and device-bounds-lavapipe.txt are untouched by the regeneration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:28:00 +02:00
Meta Vens
d605c70bba CultPhasor.cos/.sin become .cosine/.sine in C# and HLSL (GLSL regenerated next)
SPIRV-Cross renames members matching GLSL builtins, so the source names change. Resolver and JSON writer accessors follow the members; the wire (positional MessagePack, JSON keys) is untouched in this commit. The committed GLSL is stale until its regeneration commit, so CommittedGlslEqualsLowering fails here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:25:39 +02:00
Meta Vens
28e35dc448 CultMath tests: legacy CultPhasor JSON fixture written by the 86cd84a3 encoder
Two encodings with keys cos/sin, produced by CultMathJson at base (Yggdrasil scratch run, sha256 2903179720e2a2c974a3c64efea4ee95dec823943dd67f50df01adf650823e1a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:25:20 +02:00
Meta Vens
485fe27ca4 cultmath map: Pass 9d (merge probe for glsl-build-dxc r2, phasor rename), stale open questions marked ruled; target: decided since the unpark
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:06:15 +02:00
Meta Vens
86cd84a315 Merge hands/cultmath-shader-lattice-loop
CultMath shaders: cultmath_power_border, cultmath_lattice_cell_distance and
cultmath_weighted_lattice_cells over a fixed-capacity site array
(CULTMATH_MAX_SITES, overridable). Spec cultmath-tapes:cut_spec:cut-shader-lattice-loop.r1,
report h1, Soul s1 holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 17:20:23 +02:00
Meta Vens
92d4d5eb71 CultMath: regenerate CultMath.glsl on Yggdrasil after the capacity note
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:50:50 +02:00
Meta Vens
b6f61ea8c4 CultMath.hlsl: the capacity note names no resource type
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:50:15 +02:00
Meta Vens
8a0b98f70f CultMath: re-record the fixture and lavapipe bounds on Yggdrasil after the exit-bound draws; the mutant test prints its kill counts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:48:54 +02:00
Meta Vens
ce6e39a282 SiteArrays: ExitBoundPoints, lattice draws where an exit one ring early reads another result
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:46:17 +02:00
Meta Vens
70914d9a95 CultMath: the fixture cases separate each lattice-loop mutant from the shader text
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:30:19 +02:00
Meta Vens
773a3efbc9 CultMath: regenerate the GLSL and re-record the lavapipe bounds on Yggdrasil for the site-array families
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:26:03 +02:00
Meta Vens
a88df479b8 cultmath_weighted_lattice_cells: the ring exit takes (float)(k - 1), GLSL ES has no int-to-float promotion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:23:43 +02:00
Meta Vens
96d83c7f1b CultMath: regenerate CultMath.glsl and glsl-parity.json on Yggdrasil (Ubuntu) for the site-array families
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:21:45 +02:00
Meta Vens
8f74369dee SiteArrays: WidestLook initialises before FarPoints
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:18:56 +02:00
Meta Vens
150ea0a060 CultMath shaders: cultmath_power_border, cultmath_lattice_cell_distance and cultmath_weighted_lattice_cells over a fixed-capacity site array
CULTMATH_MAX_SITES defaults to 128 under #ifndef. The consumer-written ring-loop and border-loop comment recipes are deleted. The C# mirror takes the array as T[]; the span twins live in SiteArrays; the GLSL lowering keeps the capacity macro; gen.py learns the array leaf; the smooth-step family gains its cap-point draw; CULTMATH_WRITE_GLSL refuses non-Linux.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:09:18 +02:00
Meta Vens
3cd6815575 Research: variable-length shader inputs (array+count vs macro vs constants), for shader-lattice-loop; map section shader-lattice-loop
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:55:03 +02:00
Meta Vens
e2051c5a41 cultmath map: Pass 9c root cause of the span_fbm/span_ridged 1-ulp diff (entry -O3 reassociation, lavapipe use-count fusion); strict-form probes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:38:36 +02:00
Meta Vens
23ac4e895b cultmath README: name tools/glsl/build-glsl.sh and GLSL_DIR, not yet the owner of the committed GLSL
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 13:32:37 +02:00
Meta Vens
b7f18ff5b1 build-glsl.sh: fail on inout, param_var_, wrapper names, export and mix(mix( in the output
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 13:24:11 +02:00
Meta Vens
9f0c20929c cultmath device run: GLSL_DIR runs the built route and compares lavapipe-glsl to lavapipe-hlsl bit for bit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 13:04:16 +02:00
Meta Vens
330290dbd3 partition.py: fix duplicate-signature key
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:59:52 +02:00
Meta Vens
cd94a2185d partition.py: duplicate chunks are duplicate signatures, not names (overloads)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:58:55 +02:00
Meta Vens
bcb263f2cc cultmath glsl build: build-glsl.sh, pinned tools, pipeline, ES 3.00 and consumer checks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:54:26 +02:00
Meta Vens
951bd15635 cultmath glsl build: partition.py, origin partition and constants with refusals
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:53:48 +02:00
Meta Vens
b10f4530d0 cultmath glsl build: spvedit.py, the one SPIR-V edit with by-value parameters
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:49:50 +02:00
Meta Vens
da082e7e7c cultmath glsl build: wrappers.py, build-only export wrappers (188) with a count refusal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:49:12 +02:00
Meta Vens
774bebb0d3 cultmath map: Pass 9c (-Od temporaries, by-value lowering, HLSL-equal gate); od-temporaries probes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:45:42 +02:00
Meta Vens
e626bf26cc Merge hands/cultmath-lattice-weighted-shaders (cultmath-tapes cut 4b, lattice-weighted-shaders)
Spec cultmath-tapes:cut_spec:cut-lattice-weighted-shaders.r1; report h1; Soul s1 holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 12:04:05 +02:00
Meta Vens
7ddb10408e cultmath tests: lavapipe bounds with the four lattice families, recorded on Yggdrasil (cut lattice-weighted-shaders)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 11:34:05 +02:00
Meta Vens
8b31e47eee CultMath shaders: HLSL twins of the four lattice steps, both copies, regenerated GLSL and parity fixture (cut lattice-weighted-shaders)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 11:31:46 +02:00
Meta Vens
b496131a6d Merge hands/cultmath-lattice-weighted: lattice-weighted-cellular (4a)
Spec cultmath-tapes:cut_spec:cut-lattice-weighted-cellular.r1; reports h1, h2; Soul s2 holds.
No conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 11:14:56 +02:00
Meta Vens
20ecec4040 cultmath tests: lattice refuses a query whose far cell centres overflow, as p
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 10:22:10 +02:00
Meta Vens
df1b2dca89 cultmath tests: lattice cap reached by a point that needs ring 17, exact tie keeps the earlier row-major cell, winner swaps are continuous
Findings max-ring-unpinned and the tie-le survivor; also the spec's F1-swap and F2 continuity checks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 09:14:39 +02:00
Meta Vens
5745c2c3b2 cultmath lattice: stretch normal normalised by its largest component; refusals name their own parameter
Findings normal-not-unit-breaks-exit and refusals-misnamed: a tiny or huge normal acts as its direction, cellSize and the body span are refused under their own names, and a drift times time product that overflows is refused up front.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 09:14:30 +02:00
Meta Vens
c65f70e3cd cultmath tests: lattice stretch normal is normalised 2026-10-17 07:45:43 +02:00
Meta Vens
1a8b04f36f cultmath docs: weighted lattice cells section and README bullet; rings pinned to their measurement 2026-10-17 07:29:51 +02:00
Meta Vens
e835745e63 cultmath tests: lattice orbit slow enough for the 60 Hz weight bound 2026-10-17 07:27:09 +02:00
Meta Vens
202a0bd41f cultmath tests: lattice orbit speed, boost-0 look for the 0.5k bound 2026-10-17 07:22:59 +02:00
Meta Vens
ae0c871b8f cultmath tests: lattice cells oracle, exit-bound mutants, smooth band, continuity, refusals, rings, probe parity 2026-10-17 07:18:51 +02:00
Meta Vens
1c913f5c6d cultmath: weighted_lattice_cells with exact ring-exit search, four public steps, range constants; power_border splits a private unchecked core
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 06:44:07 +02:00
Meta Vens
9bf1e82f7e cultmath map: lattice-weighted-cellular section (LW1-LW7), Pass 7 line 4 split into 4a/4b, Q4/Q5/Q9 and site-ground-field marked ruled; weighted-node probes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:17:25 +02:00
Meta Vens
911fc75531 cultmath tests: re-record lavapipe bounds for trophic-nodes families (follow-up trophic-families-rerecord)
Ten new families per route; no existing row changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 23:38:05 +02:00
Meta Vens
28c9005b61 cultmath tests: the tripwire states that a qualifier between // x /* and // */ passes (finding s2.tripwire-slashslash-star-hides)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 23:34:30 +02:00
Meta Vens
4bf6e6750c Merge hands/cultmath-shader-conforming-run (verdicts s1, s2) 2026-10-16 23:33:53 +02:00
Meta Vens
84589e0c05 cultmath tests: a reference value moved by one ulp fails its sampled-form family through the recorded bits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:19:28 +02:00
Meta Vens
352bf5b27e cultmath tests: the device-tolerance pin uses fixture values in [1, 1.1), where k ulps is within a tenth of k u1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:57:54 +02:00
Meta Vens
3ad421c482 cultmath tests: the stated device tolerance is pinned at its limit in ulps of the fixture's magnitude
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:46:56 +02:00
Meta Vens
632ffd1f7b cultmath: re-record lavapipe bounds with result sha256 per family and route, and phacelle's device clause
Recorded by run-lavapipe.sh WRITE_BOUNDS=1 on Yggdrasil; the repeat run in the same job judged the file green and
produced the same all-outputs hash (46e0c8e0...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:46:13 +02:00
Meta Vens
87299d2f02 cultmath tests: the precision tripwire reads vector and matrix spellings of min10float, min16float and half
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:33:12 +02:00
Meta Vens
7e6881af9b Merge hands/cultmath-trophic-nodes
Verdicts s1, s2 and s3 of cut trophic-nodes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:23:11 +02:00
Meta Vens
9a3ad78c11 cultmath: lavapipe bounds carry exact result bits, a reduced-precision tripwire, and the judge reads the stated device tolerance
Per verdict s1: the judge keeps the fixture tolerance as correctness and adds a recorded sha256 of each family's
result per route (a change in the computation fails until re-recorded); a test refuses mediump/lowp/precision
other than highp and min16float/min10float/min16int/half in the committed shaders; phacelle's tolerance text states
a device clause (2^-18) that the judge parses and enforces. The committed bounds file lacks the new column until
the next WRITE_BOUNDS=1 job, so this commit's device run fails and it does not pass DeviceResultsHoldFixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:22:14 +02:00
Meta Vens
e151b1b4e0 CultMath GLSL: regenerated lowering and parity fixture (float2 segment overloads, close-site border step samples)
Only the trophic families after the inserted segment families change; every family before them is identical.
Builds; the parent commit does not pass CommittedGlslEqualsLowering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:09:57 +02:00
Meta Vens
16657fa20f CultMath shaders: restore the float2 segment overloads, sample close distinct sites, document the border step's owner contract
Shader overloads stay native (ruling shader-overloads-stay-native): cultmath_segment_closest and
cultmath_distance_to_segment have float2 and float3 forms again, with mirror asserts and fixture families.
The rejected "avoid overloads" comment and design text are gone. The border step's outside-cell behaviour
(distance 0 with that plane's normal) is documented in C# and the HLSL header and pinned by a test; the mirror
and fixture gain distinct sites closer than 1e-2. GLSL and fixture regenerate in the next commit; this one does
not pass CommittedGlslEqualsLowering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 21:01:16 +02:00
Meta Vens
ea7e665e6c cultmath: judge tests kill the survivors of the first mutant table (exact by one case, bound excess, infinity class, fBm reference, row identity)
Not yet built.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:25:02 +02:00
Meta Vens
d02da58d7e cultmath: run-lavapipe.sh prints one hash of all device outputs, so a repeat run shows determinism
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:08:52 +02:00
Meta Vens
79c450e54b cultmath docs: the lavapipe conforming leg in design.md and README
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:06:55 +02:00
Meta Vens
72c0358f0f cultmath: recorded lavapipe device bounds (llvmpipe, mesa 25.2.8, dxc 1.9.2607) from run-lavapipe.sh WRITE_BOUNDS=1
Re-record needed when trophic-nodes merges (new families).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:04:36 +02:00
Meta Vens
cc00ee09b9 cultmath: generator maps integer vector and matrix return types to HLSL (applied)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:02:37 +02:00
Meta Vens
d1f53ed51f Interval ground map: Pass 9b, overloads stay native (build-only export wrappers, -Od, one SPIR-V edit); overload-renames cut deleted; wrapper probe scripts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:01:15 +02:00
Meta Vens
89bf977dcf cultmath: generator maps integer vector and matrix return types to HLSL
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:01:10 +02:00
Meta Vens
37bc6de8b1 cultmath: lavapipe shader generator, Vulkan host and job script
tools/device/gen.py spells one HLSL and one GLSL ES 3.10 compute shader per work
item, vkrun.c dispatches it on the first Vulkan device, run-lavapipe.sh runs the
whole conforming job. None of them compares. Also fixes the metric test's family
choice. Not yet run: these may not build or run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:59:42 +02:00
Meta Vens
4342fb692f cultmath: C# work list and device judge for the lavapipe conforming run
GlslMirrorTests becomes partial; GlslMirrorTests.Device.cs holds WriteDeviceWork
(the work list, with reference families for the four sampled-form families) and
DeviceResultsHoldFixture (exact, check, sampled-form and recorded-bound judging),
plus simulated-device tests for each rule. Not yet built or run: this commit may
not build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:41:14 +02:00
Meta Vens
fcd580a8ed SiteCellsTests: the 2^24 span refusal is tested on a real overlong span, so a doubled limit fails it
The earlier version read past a one-element array and passed under the mutant by hitting a non-finite site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:36:01 +02:00
Meta Vens
ddd9e63586 SiteCellsTests: power_border_step ignores a site coincident with the owner
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:29:32 +02:00
Meta Vens
189677c63d CultMath GLSL: regenerated lowering and parity fixture (power_border_step, segment_closest 3D; 2D segment families removed)
Earlier families unchanged (checked by JSON comparison against f697e03f). Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:25:41 +02:00
Meta Vens
09940e202c Interval ground map: Pass 9, the shader pipeline (lavapipe run, overload renames, dxc switch, ANGLE captures) with probes and consumer audit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:20:20 +02:00
Meta Vens
bcd8a58055 CultMath sites: exact power cell border (radical planes, midpoint form), validated cell spans, one shader name per segment function
power_border_step/power_border give the exact distance to the owner's power cell boundary: the least radical-plane
distance over every other site, built from the offset and (w1-w2)(w1+w2) so it does not cancel. site_cells_border is
documented as the two-winner bisector distance. Cell spans refuse non-finite positions, queries and spans over 2^24.
segment_closest is public in C#; the HLSL segment functions are 3D only (no new overload sets). Tests added.
Does not build the GLSL checks: CultMath.glsl and glsl-parity.json are regenerated in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:15:55 +02:00
Meta Vens
ee8b15ec2c SiteCellsTests: the 2D segment distance is checked against a double reference, not only against the out form
The closest-unclamped-2d mutant survived because both forms share one segment_closest.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:34:55 +02:00
Meta Vens
7d75b45240 compile-glsl.ps1: a function with no parameters is called with no arguments
The wrapper built one empty argument for an empty parameter list, so cultmath_site_cells_empty() failed glslang.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:34:34 +02:00
Meta Vens
5a94abfef4 CultMath docs: the Sites section, struct consumers and the surface list
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:27:09 +02:00
Meta Vens
955126697b CultMath GLSL: regenerated lowering and parity fixture for the site-cell twins
Regenerated with CULTMATH_WRITE_GLSL=1 on Yggdrasil; the fixture adds seven families and deletes no line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:26:43 +02:00
Meta Vens
a1aac7c495 CultMath tests: GLSL mirror families for the site cells, scores, border, Gabriel margin and segment distance
CommittedGlslEqualsLowering and GoldenFixtureMatchesCSharp fail at this commit until the next one regenerates the GLSL and the fixture.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:21:50 +02:00
Meta Vens
e584bfaff2 CultMath HLSL: site cells, scores, border, Gabriel margin and segment distance twins (both copies, mirror asserts)
CommittedGlslEqualsLowering fails at this commit until the GLSL is regenerated in the next one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:17:40 +02:00
Meta Vens
fc3bdc70e8 Caching: MessagePack and JSON shapes for CultSiteCells, pinned
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:16:37 +02:00
Meta Vens
f65bde6f15 Merge eureka/cultmath-tapes-interval-ops-r5 (LOD-far footprint pin; closing verdict cut-interval-ops.s4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:16:07 +02:00
Meta Vens
a3c3bd9695 CultMath: site cells (power and multiplicative scores, fold, border), Gabriel margin and gate, out-free distance_to_segment
Not yet verified on Yggdrasil; this commit builds only if the next run says so.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:14:50 +02:00
Meta Vens
e598c44333 Research: shader runner probes (dxc SPIR-V library + SPIRV-Cross, lavapipe on Yggdrasil, native ANGLE D3D11 on Starfire) for the three questions replacing device-run-driver
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:24:47 +02:00
Meta Vens
8f825205c8 Interval ground map: trophic-nodes section (TN1-TN6), Pass 7/8 lines; trophic work split into trophic-nodes, lattice-weighted-cellular, expr-trophic-ops
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:23:33 +02:00
Meta Vens
658c09ecd3 CultMath tests: LOD-far depth agreement asserted under the headline's 2x contract
Pins LodStep's footprint branch, which only bites at LOD-far.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:17:01 +02:00
Meta Vens
f697e03f4e Research: native shader testing (Amber, lavapipe/SwiftShader, native ANGLE, DXC exec tests, Slang, SPIRV-Cross) and HLSL-to-GLSL ES toolchains, for cultmath-tapes device-run-driver
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:48:51 +02:00
Meta Vens
c9d3e67214 Research: device capture runners, real-GPU capture practice and browser-driver fallbacks, for cultmath-tapes device-run-driver
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 07:02:42 +02:00
Meta Vens
de584f5a39 Merge hands/cultmath-stage-shaders-device (part 1: allowance seam, band read families, one owner of the read's absolute terms)
Verdicts cut-stage-shaders-device.s1/s2 hold. main's interim changes are Rust packages only (cultnet-rs, cultcache-rs, cultmesh-rs), disjoint from the verified C#/shader tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 06:55:43 +02:00
Meta Vens
b37e07a52c cultmath: regenerate CultMath.glsl for the snoise read constants
Generated by CULTMATH_WRITE_GLSL=1 on Yggdrasil; glsl-parity.json is unchanged (the af and
span families keep their bytes). Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 06:35:34 +02:00
Meta Vens
58a2d50a76 cultmath: one owner of the snoise read's absolute terms (SNOISE_READ_VALUE, SNOISE_READ_GRADIENT)
span_slack and af_snoise each spelled 2^-14 (value) and 2^-12 (gradient) inline, in C# and
HLSL. Both now read the constants beside SNOISE_HESSIAN (math.Affine.cs, CultMath.Affine.hlsl,
Unity copies), so a device-judged widening moves every reader. Bits unchanged. The test
oracles in AffineTests and GlslMirrorTests read the constants; a new test pins af_snoise's
allowance to span_slack's terms. span_slack's comment no longer claims it is the only
allowance. Does not build green yet: CultMath.glsl is not regenerated in this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 06:34:21 +02:00
Meta Vens
8df4528707 Merge hands/cultnet-rs-door
cultnet-rs 0.2.0: every received MessagePack byte decodes once through
cultcache-rs's door; the dynamic document lane is parked at
parked/cultnet-rs-dynamic-document-lane; one decoder per message family;
duplicate keys refused; refusals name schema and fields, never values;
cultmesh-rs 0.2.0. Cut cultnet-rs-door r2, held by Soul s1-s3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 06:07:12 +02:00
Meta Vens
0daa2f1017 cultnet-rs: kind-array refusal no longer quotes the value; README says delete carries ids
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:55:05 +02:00
Meta Vens
a83a7153cd cultnet-rs tests: pin which schemaVersions a refusal may name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:03:16 +02:00
Meta Vens
4de26d8655 cultmath tests: the mirror twin count is 127 (span_rounding and span_slack twins)
EveryMirrorFunctionMatchesCSharpMath compared both new twins bit for bit against the C# seam;
only its pinned count of mirrored functions moves from 125 to 127. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 05:00:37 +02:00
Meta Vens
369e5793a7 cultmath: regenerate CultMath.glsl and glsl-parity.json (four band families appended)
Generated by CULTMATH_WRITE_GLSL=1 on Yggdrasil (dotnet/sdk:10.0) at 5ccbb094; the fixture
diff is additions only, every earlier family byte-identical. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 04:51:34 +02:00
Meta Vens
5ccbb09487 cultmath tests: band read families with an allowance check, a reach test, GradDouble
Four coordinate-band snoise_grad / snoise_hessian families with Verify AssertSpanAllowance,
SpanAllowanceCoversReadsAlongTheSpan, NoiseBoundTests.GradDouble and its pin, design.md
paragraph. The fixture and CultMath.glsl are regenerated in the next commit; this one compiles.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 04:49:13 +02:00
Meta Vens
2478a36a22 cultmath: span_rounding and span_slack own S_o and the span allowances (C#, HLSL, Unity copy)
The inline rounding and the three allowance literals of span_stage and cultmath_span_stage
move into internal span_rounding / span_slack and their HLSL twins; operations and order are
unchanged. CultMath.glsl is not regenerated in this commit, so
GlslMirrorTests.CommittedGlslEqualsLowering fails here; it compiles.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 04:49:12 +02:00
Meta Vens
fd944072b6 cultnet-rs: three more refusals stop quoting received values; the networking descriptor's repeated key is pinned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:33:27 +02:00
Meta Vens
7ab3092550 cultcache-rs tests: a field name is kept only when it looks like one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:31:34 +02:00
Meta Vens
d9d2ce4aea cultnet-rs tests: the unknown-encoding case spells payloadEncoding once
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:30:57 +02:00
Meta Vens
020360e238 cultmesh-rs 0.2.0: its public functions take and return cultnet_rs::CultNetMessage, whose variants 0.2.0 removed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:22:29 +02:00
Meta Vens
377c24b8cd cultnet-rs: one decoder per family, duplicate keys refused, value-free receive errors
Raw v1 is decoded by the enum's derive alone (SnapshotResponseRawV1Rows,
the v1 arm and its helpers deleted); the five raw families carry
skip_deserializing so the hand reader is their only decoder. The four hand
readers refuse a duplicated key; refusals carry a schema-shaped
schemaVersion context and never a received value. README describes only
the raw lane. A same-length str-payload credential forgery is pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:22:27 +02:00
Meta Vens
c0a2b890f6 cultcache-rs: the decode door names a missing or duplicated field and never quotes a value
describe_decode_error keeps serde's own value-free texts for missing and
duplicate fields (static names from the type), maps unknown-variant and
invalid-type to kind descriptions, and leaves everything else opaque.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:22:22 +02:00
Meta Vens
a30cb7d254 Interval ground map: stage-functions and stage-shaders merged; stage-shaders-device and the cultmath-unity span release next
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:12:47 +02:00
Meta Vens
d7d1bcf38e Merge hands/cultmath-stage-functions (stage-functions + stage-shaders)
Span stages in C#, HLSL and GLSL: SpanPath/SpanStage inputs, float-rounding allowances on curvature and fold, zero-length flat spans, pinned shader guards, exact noise pins, compile-hlsl.ps1 with -WX, struct-aware shader mirror. Soul verdicts cut-stage-functions.s2-s4 and cut-stage-shaders.s1 hold; main's interim changes are cultcache-rs and docs only, disjoint from the verified C#/shader tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:12:29 +02:00
Meta Vens
a5c7954eed Merge element ids parts 1 and 2 into C2a
hands/variants-c2a-element-id-holes at 64ec14f9 (cuts c2a-element-id-holes r6
and c2a-id-closed-world r2, held by Soul s5/s6 and closed-world s1/s2). C2a's
side since the fork is two prose-map commits; no code conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:10:01 +02:00
Meta Vens
338a952be0 cultmath: near-origin fold test checks the exact departure to 1e-13
The additive 1e-9 absorbed the whole exact departure at scales 1e-4 and 1e-2 (1e-11 to 1e-9),
so dropping the value allowance's absolute term 2^-14 survived the test. The tolerance is 1e-13.
Test only; builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:11:04 +02:00
Meta Vens
4250dd28d5 cultmath: compile-hlsl.ps1 fails on warnings and on a missing shader without its path; compile-glsl.ps1 passes struct arguments
dxc runs with -WX; a ShaderPath that is not a file throws naming the parameter and entry index,
never the path; the GLSL wrapper builds a struct parameter from its declaration's field zeros.
design.md and README state the struct-argument twin rule, the span twins and the missing shader identity.
No C# change; the compiles run on Starfire after the last commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:02:13 +02:00
Meta Vens
d172e7f71f cultmath: regenerate CultMath.glsl and glsl-parity.json for the span stage twins (CULTMATH_WRITE_GLSL=1, Yggdrasil)
CultMath.glsl gains the three structs and cultmath_span_stage/fbm/ridged by the lowering; the fixture
gains the two span families after the last one, earlier families byte-identical (0 deleted lines).
Builds; CultMath.Tests 452 passed at this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:02:12 +02:00
Meta Vens
9548e1d0e6 cultmath: HLSL span stage twins over struct arguments; mirror twin rule; span_l1 becomes csum(abs(..))
cultmath_span_stage, cultmath_span_fbm and cultmath_span_ridged with SpanPath, SpanStage and
SpanNoise, statement for statement from math.span_stage (no shader identity). They follow the
includes, not cultmath_ridged_grad: the bounds read the SNOISE_* constants the includes define.
The mirror comparison matches every mirror function to the math function of the same name,
public or private, with struct parameters mapped by name and fields; MirrorOnly is gone.
New tests: identity and realistic-span mirror sweeps, a large-octave-step fold test; the
near-origin fold test gains scales 1e-4 and 1e-2; the integral_clamp_linear tests also read the
committed fixture's cases.

The GLSL, the fixture families and the docs follow: CommittedGlslEqualsLowering and
GoldenFixtureMatchesCSharp are red at this commit. It builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:58:05 +02:00
Meta Vens
64ec14f989 Element ids: pin the inherited string key that makes a type one whole value
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:27:52 +02:00
Meta Vens
0c797ec3f7 Element ids: string keys read from the members MessagePack writes; AllowPrivate, inherited-formatter and unwritten-string-key pins; README states the whole-value exception
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:16:32 +02:00
Meta Vens
7e69daa85e Test: a KeyAsPropertyName type with an integer key is still one whole value
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:38:13 +02:00
Meta Vens
5b9c77480b cultmath: compile-hlsl.ps1 comment no longer names a dxc architecture directory
The spec's negative check greps the script for arm64; the comment matched. Comment
only; script rerun on Starfire (compiles both copies, exit 0; arm64 dxc control throws). Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 01:30:23 +02:00
Meta Vens
e5a4f60bc7 Tests: inherited attributes, AllowPrivate subclass, null-formatter reason
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:28:00 +02:00
Meta Vens
e4c1ffa4f1 cultmath: span tests: a zero-length span through the sloped branch is +0 by bit pattern
Stryker survivor on the previous head: direction t1 <= t0 changes a zero-length
sloped span from +0 to -0, which no test observed. Builds; test-only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 01:24:20 +02:00
Meta Vens
bf337f0135 cultmath: regenerate CultMath.glsl and glsl-parity.json (CULTMATH_WRITE_GLSL=1, Yggdrasil)
glsl: line 307 only (the flat-branch guard). Fixture: one family appended
('zero-length spans and flat infinite clamps'), earlier families byte-identical.
Builds. Together with the previous commit the lowering and fixture tests pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 01:03:57 +02:00
Meta Vens
157276cf6e Test: the whole-holder variants override their names
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:02:45 +02:00
Meta Vens
b2852304b1 cultmath: flat integral_clamp_linear returns +0 for a zero-length span; HLSL guard, lo-normalise and pin tests behavioural; owned dxc compile script
The b == 0 branch now guards t1 != t0 like the sloped branch (C# and both HLSL
copies), so an infinite bound times a zero length no longer forms NaN.
Edge-grid mirror test and a counting-clamp shadow test replace the text-order
check; the four pin tests assert equality with their measured expression;
tools/compile-hlsl.ps1 is the one dxc compile verdict for the HLSL copies.

Builds. CultMath.glsl and glsl-parity.json are not yet regenerated, so
CommittedGlslEqualsLowering and GoldenFixtureMatchesCSharp are red at this
commit; the next commit regenerates them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 01:01:54 +02:00
Meta Vens
a3a03be589 cultnet-rs: pin message validation after the door and the current 18-slot health wire
Mutation survivors of the first run: validate_message -> Ok(()) and the
Some(18) arm of decode_canonical_signed_payload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:57:53 +02:00
Meta Vens
45c6db6cb8 Interval ground map: stage-functions r1-r3 status; stage-shaders struct mirror and SpanStage.Identity form question
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:56:44 +02:00
Meta Vens
19963293ae Contracts: whole-value shapes, closed list of owned shapes, door only verifies
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:54:39 +02:00
Meta Vens
19f475ca92 Element ids part 2: one recogniser; shapes the walk does not own are whole values
Delete Hidden, HiddenProblem, UnreadKeyedMembers/UnreadMembers, Settle's
[MessagePackFormatter] refusals and the door's written-as-object branch.
Classify gains Leaf for attribute-formatted, [DataContract] and string-keyed
types; MembersOf honours the most derived AllowPrivate; the door refuses a
null StandardResolver formatter with the throwing path's text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:54:37 +02:00
Meta Vens
bca8a70a5a cultnet-rs: decode_door credential test spells the envelope as a map with one unknown key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:44:28 +02:00
Meta Vens
7161eb2c53 variants map: whole-list holders under amended q3 (r2 probe facts, model page, cut sequence)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:43:35 +02:00
Meta Vens
7fb7474cc9 cultnet-rs: decode_door credential test adds an unknown slot to the positional envelope
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:40:08 +02:00
Meta Vens
a38768202a cultnet-rs: fix decode_door credential test and unused import
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:34:55 +02:00
Meta Vens
bdae8b831a cultnet-rs: retire the dynamic document lane; every received byte decodes once through the door
Deletes CultNetDocumentRecord, the DocumentPut and SnapshotResponse variants,
parse_cultnet_message, require_typed, round_trip_typed, the eight replication
functions and the two catalog schemas (parked at tag
parked/cultnet-rs-dynamic-document-lane). decode_cultnet_message_from_slice is
the one decode; raw v1 rows, identity credentials and the signed-record readers
decode through cultcache_rs::decode_payload. Adds tests/decode_door.rs. 0.2.0.

Not yet compiled at this commit (verification runs on Yggdrasil).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:33:31 +02:00
Meta Vens
98f4c44170 cultmath: span rounding margins stated as observed samples with seeds, not "at most" bounds (comment and design.md); no allowance changed
Two samples of the probe: the cut's own (seed not recorded) and an audit's (seed 0x501) give 0.057/0.046/0.075 and 0.064/0.0485/0.057 of e_n/e_g/e_h; a new seed moves them. Comment only in math.Span.cs: builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 23:15:33 +02:00
Meta Vens
15730d4e99 cultmath: span tests: the float arm of the near-zero midpoint test carries the accumulation bound of its octave reads; previous commit's new test failed on 7e-9 of float noise (builds, one new test red)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 23:13:30 +02:00
Meta Vens
8939af134f cultmath: span tests: long path from rest, near-zero midpoint under a point span and a loose speed bound, fold's float arm read from span_ridged's own field
The fold test's float arm no longer re-spells the octave coordinate (OneOctave): each octave's float value is the difference of span_fbm / span_ridged at o + 1 and o octaves, with a stated bound on what the accumulating float sum loses.
New fixed span at halfLength 6 pins the Gl reach operator (nu / halfLength fails); a near-zero midpoint on a stationary path (halfLength 0, and halfLength 1 under a loose speed bound) pins the halfLength-0 fold rule and S_o's reach term.
Builds: yes (to be confirmed by the Yggdrasil job).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 23:12:41 +02:00
Meta Vens
dd131b3b2e variants map: element-id body facts (2026-10-15) and a refreshed cut sequence
Probe facts from the id-holes Imagination passes (r5, r6) and Soul s5/s6;
the cut sequence names what landed since 2026-10-12 and what part 2 absorbs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:14:07 +02:00
Meta Vens
3de67b9f9a cultmath: span tests: octave-sum identity, stretched-velocity and transformed-acceleration folds, zero axis with stretch below 1, bands lifted through octaveStep
Kills the Stryker survivors of the first allowance commit: the velocity stretch sign, the norm at
a zero axis, gain-weight and acceleration-factor arithmetic, the octaveStep term of S_o. Not yet
built or run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:25:22 +02:00
Meta Vens
57fdf70c21 Contracts: the codec door refuses only a proven family mismatch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:52:37 +02:00
Meta Vens
d89dd26507 GeometryDocumentTests: a generated formatter passes the element-id door
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:52:25 +02:00
Meta Vens
a5241112de Codec door refuses only a proven family mismatch; source-generated formatters pass
WrittenAsWalked required StandardResolver's formatter to equal one of the walk's own families, which refused every source-generated formatter. WrittenAgainstWalk asks one contrary resolver per walk kind and refuses only on a reference match. DcSeq is refused as a collection written as an object; FormattedRoles passes (registration owns the attribute).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:52:13 +02:00
Meta Vens
ffb25b6657 cultmath: span tests: MaxMagnitude as a loop (a lambda cannot capture a struct's this); previous commit did not build the test project
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:51:58 +02:00
Meta Vens
52d25740ac caching: MessagePack and JSON shapes for SpanPath and SpanStage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:49:31 +02:00
Meta Vens
0a1b49a231 cultmath: span stages take SpanPath and SpanStage and widen curvature and fold by float rounding allowances
span_fbm/span_ridged/span_stage lose the 14-argument signatures for SpanPath, SpanStage,
octaves, lacunarity, gain. Per octave S_o and e_n, e_g, e_h (af_snoise's derivation plus the
Hessian's 2^-10) widen the local Hessian tier, the gradient bound and the fold travel. Tests
compare against the exact double kernel in coordinate bands, add the pinned rank-one span,
widen the stage parameter ranges, and state the lab-form mapping. Not built or run yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:49:30 +02:00
Meta Vens
6e70626bf9 ElementIdEngineTests: the keyed self-enumerating node round-trips through MessagePack with its ids
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 03:36:15 +02:00
Meta Vens
fb2be32b60 Revert "UnionArmGuardTests: a non-generic list that enumerates arms joins the shape sweep"
This reverts commit 127432efb1.
2026-10-15 03:35:30 +02:00
Meta Vens
127432efb1 UnionArmGuardTests: a non-generic list that enumerates arms joins the shape sweep
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 03:35:21 +02:00
Meta Vens
48d3f667e6 Element ids: a keyed map counts as enumerating objects; family check tests assert the exact refusal
EnumeratesObjects took ElementTypeOf first, which for a dictionary is the
KeyValuePair (a leaf), so a keyed Dictionary subclass never reached the path; the
map's value type is asked first. The unreachable null-standard guard in
WrittenAsWalked is removed. Tests pin the direction and wording of the refusal, the
attribute-formatter collection case and the unwrapped inner exception.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 03:09:39 +02:00
Meta Vens
956c077a1a UnionArmGuardTests: pin the refusal for a type MessagePack cannot build a formatter for
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 03:00:58 +02:00
Meta Vens
744f2f78b1 Contracts: the written-member rule, readable ids and the codec's family check
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 02:58:10 +02:00
Meta Vens
1956145ea2 UnionArmGuardTests: maps in the shape sweep hold a list-carrying object
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 02:56:18 +02:00
Meta Vens
7d4224bc96 UnionArmGuardTests: name the collection shape whose id path is empty
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 02:54:25 +02:00
Meta Vens
5157c647af Codec: confirm MessagePack writes every id-path type the way the walk reads it
CultElementIds is the walk's model of a type; MessagePack's StandardResolver owns how
a type is written, and nothing checked that the two agree. ResolverClaim now asks,
for each id-path type, whether StandardResolver's formatter is the object or union
resolver's (a type read by key) or a collection resolver's (a collection), and refuses
the document at first encode or decode when it is not, naming the type. A keyed type
that enumerates objects goes on the path so a keyed List subclass is caught. A type
MessagePack cannot build a formatter for, and a resolver ahead that throws, are typed
refusals; the AllowPrivate catch is pinned (s4: keyed-enumerable-misclassified,
allowprivate-catch-unpinned).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 02:53:28 +02:00
Meta Vens
ee3e262064 CultElementIds: the walk reads the members MessagePack writes, and an id it cannot read back is refused
MembersOf is now MessagePack's StandardResolver write set: public fields and
properties with a public getter, none of them [IgnoreMember]. UnreadKeyedMembers
(renamed from NonPublicKeyedMembers) is every other [Key] member, including a
public property whose getter is private. IdProblem takes id candidates from every
instance member up the base chain, refuses an id MessagePack does not write, and
refuses a property id with a non-public setter, which MessagePack writes but does
not read back (s4: walk-member-set-differs, private-setter-id-dropped-on-read).
An oracle test pins the member set to MessagePack's ObjectSerializationInfo.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 02:51:24 +02:00
Meta Vens
9fd5ae1f09 CultElementIds: a keyed type is read by key before it is read as a sequence
Classify tested IEnumerable<T> before [MessagePackObject], so a keyed class that
enumerates itself recursed until the stack overflowed during registration (s4,
keyed-enumerable-misclassified), and a keyed class that merely enumerates had its
keyed lists neither judged nor minted. Keyed first: MessagePack writes both by key.
VisitMemberType also stops at a type already on its own through chain, which ends
an unkeyed Node : IEnumerable<Node>.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 02:49:18 +02:00
Meta Vens
e9c563ebd1 Research: serializers fail closed on shapes they don't own (Protobuf, Bond, Orleans, MessagePack, System.Text.Json), for variants c2a-unowned-holders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:47:08 +02:00
Meta Vens
494ad9abfb UnionArmGuardTests: drop the unbuildable-AllowPrivate fixture
No fixture makes StandardResolverAllowPrivate throw when asked for a formatter (Stryker: the catch stays
NoCoverage), so the test pinned nothing the claim tests do not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:39:17 +02:00
Meta Vens
8ddb8a03e1 UnionArmGuardTests: the unbuildable-AllowPrivate claim test uses a private delegate member
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:37:11 +02:00
Meta Vens
70ffa2af9a Element ids: Classify keeps only the string special case; tests pin nullable structs, unkeyed structs, inheritance and an AllowPrivate formatter that cannot be built
Stryker found the primitive, enum and object tests in Classify redundant with the value-type and unkeyed
branches, so they are gone. The new tests kill the survivors that were behaviour: a nullable keyed struct
walked as its struct, an unannotated struct treated as a value, [MessagePackObject] reaching a subclass,
and a claim on a type StandardResolverAllowPrivate cannot format.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:32:25 +02:00
Meta Vens
8bbe56f555 Contracts: the id walk follows declared types; keyed structs, every map interface and AllowPrivate named
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:24:35 +02:00
Meta Vens
908d9ac29d Codec: StandardResolverAllowPrivate's formatter for an id-path type is no claim
It writes the [Key] layout plus non-public keyed members, and registration now refuses any of those that
reaches an object list, so no element id can be dropped. The comparison is made lazily, only after an
ahead resolver returned a non-standard formatter, and an exception from it reads as not equal.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:23:40 +02:00
Meta Vens
143e4f7db2 Element ids: a non-public [Key] member on the way to an object list is refused at registration
MessagePack writes a non-public [Key] member only under AllowPrivate, and the element-id walk reads
public members, so the ids such a list holds could never be checked. Graph entries carry Hidden; a hidden
list or a hidden edge whose child reaches adds one problem and nothing to the id path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:22:47 +02:00
Meta Vens
1e7b3b25da Element ids: one Classify of the declared type; registration, Plan and Holds walk declared types
IsLeaf, IsObjectType and DictionaryValueType are deleted. Classify(declared) says Leaf, Map
(IDictionary<,> or IReadOnlyDictionary<,>), Sequence or Owner; a keyed struct is walked as an owner and
is never an element. Plan and Holds take the declared root type and follow declared member types, so a
member declared object or a subclass's extra members under a base-typed member are neither minted into
nor read. Minted path text is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 01:21:22 +02:00
Meta Vens
c9c96642e8 UnionArmGuardTests: a later resolver's claim on a type the standard formatter already answered is never asked
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:35:57 +02:00
Meta Vens
9eb43be7f1 TheIdPathHoldsEveryTypeThatReachesAnObjectList: the expected paths now include the list types the walk passes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:34:39 +02:00
Meta Vens
56e25d245b UnionArmGuardTests: global:: reaches MessagePack.Resolvers from inside GameCult.Caching.Tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:33:46 +02:00
Meta Vens
d46e000b5c UnionArmGuardTests: the standard-formatter resolver names MessagePack.Resolvers.StandardResolver
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:31:38 +02:00
Meta Vens
95ddc59680 CultMesh element-id test: the catalog entry's content hash differs, so the record really is unresolvable
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:28:30 +02:00
Meta Vens
b0cc36bc7e ResolverClaim: a resolver returning MessagePack's standard formatter for an id-path type claims nothing
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:26:53 +02:00
Meta Vens
73b4b47451 CultElementIds: collection types join the id path, member attributes read through overrides, each unkeyed element type refused once
Graph edges and lists carry the collection types between member and child; Settle adds them to the path so
[MessagePackFormatter] on a collection class and a resolver claim on List<Elem> are judged. Declares replaces the four
MemberInfo.IsDefined member reads, which ignore inherit for properties. Survey.Judged dedupes the unkeyed-element refusal.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:26:52 +02:00
Meta Vens
328e9b37e1 HeaderForWrite comment: only a reader with the record's type can tell from its bytes whether it holds an element id
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:23:25 +02:00
Meta Vens
fa90c6cc1e CopiedHoldIds: the copied.Length == 0 early-out is deleted; an empty copy runs the loop zero times
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:23:25 +02:00
Meta Vens
e118f0995f CultMesh element-id tests: an unresolvable copied record is copied and decides nothing (Soul s2 P5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 00:23:17 +02:00
Meta Vens
b6977e276e ElementIdTests: the staged-holds-an-id gate is pinned with two written records, only one holding an id
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 23:48:21 +02:00
Meta Vens
bd3477450e Tests: the Mesh test's unmarked file holds ids of a type defined in the Mesh assembly (a persisted layout is matched by CLR type name); the appended python record brings its catalog entry beside the cache's
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 23:39:50 +02:00
Meta Vens
a1c887cde7 Tests: the appended python record brings its catalog entry; the Mesh test declares the python document member for member
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 23:37:10 +02:00
Meta Vens
5237b24ab4 Tests: the copy check marks over ids a writer never read, decodes only what could mark, never refuses; CultMesh typed and raw writes (Soul P8, P8b, P13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 23:35:31 +02:00
Meta Vens
fb065f0de9 CultCache: a write decides from the copied records' bytes whether an unmarked store holds an element id; the per-instance read map is deleted
_readHoldingIds and the pull's Holds walk are removed in the same commit that adds
SingleFileBackingStore.CopiedHoldIds: under the store's lock, while the header on disk is
unmarked, every plain copied record the writer's registry claims and whose type reaches an
object list is decoded, and the first holding an id marks the store. Positive-only, never
refuses a write. CultMesh's typed write attaches the registry its payload was encoded with;
the raw write attaches none. Contract text states the rule and its residual.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 23:35:31 +02:00
Meta Vens
5ebdc01b32 Element id tests: the id path holds union bases and inherited formatters; the options entries refuse null
Stryker survivors on the id-path walk: dropping the union-subtype edge, dropping the
element base from the path. Both now have a test that fails without them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 22:33:39 +02:00
Meta Vens
9f20d1be2a PythonWrittenStoreTests: the replaced-record test swaps the record's bytes in the file
The py record is undeclared by LeafRecord (read-only), so a second cache cannot rewrite it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:54:49 +02:00
Meta Vens
e5736e49f3 CultCache: a write that copies a record it read holding an element id marks the store
The single-file store records, per pull, which plain records decoded holding a non-empty
element id and at what version. A write whose copied record is still at that version
marks the header (v3). The record is positive-only and version-gated, so nothing a load
records can unmark a store. The directory store passes false: only C# writes it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:52:36 +02:00
Meta Vens
b091d21f81 CultCache: list elements are written by their [Key] layout; registration and the codec refuse otherwise
Registration: a concrete element type of an object list that is not keyed, and
[MessagePackFormatter] on any type or member on the id path or on the id member, are
refused. The id path is a fixed point over the walk's edges. The codec refuses, once per
document type and on every encode and decode path, a resolver ahead of StandardResolver
that formats a type on that path. UnionArmGuardTests moves StubArm out of the list union.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:52:36 +02:00
Meta Vens
9634ab5b19 Research: header flags one writer sets for content another cannot see (ext4, SQLite, git extensions, protobuf, Avro/Parquet), for variants element ids
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:45:19 +02:00
Meta Vens
bbb98eaf66 Merge hands/decode-stack-safety 2026-10-14 21:24:23 +02:00
Meta Vens
9d2b64bd88 cultcache-rs docs: Windows line names the revision of the run it describes
The decode_stack Windows run is at 10a39dac (tests unchanged since 4b55c7d3);
the head after this commit awaits a Soul rerun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:19:19 +02:00
Meta Vens
10a39dac98 cultcache-rs docs: debug floor stated for a plain caller; first-frame sentences removed; Windows line names its run
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:19:13 +02:00
Meta Vens
f722f7c177 cultmath: span fold test for the acceleration term alone
SpanFoldCoversAccelerationAlone draws spans with zero midpoint velocity and an acceleration along the gradient
that carries n across zero, so the fold's acceleration term and its gradient bound are the only thing holding
the branch departure under fold; two hand mutants survived without it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:03:52 +02:00
Meta Vens
4b55c7d30b decode_stack tests: store reads and the outer check pinned at release-reachable sizes
Legacy and v1 reads on 64 KiB, redb pull_all and compare-and-swap on 104 KiB,
and one Largest level on 88 KiB, the sizes where a plain from_slice or a missing
outer maybe_grow dies in release as well as debug.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:57:59 +02:00
Meta Vens
dbafbf52bc cultcache-rs docs: state the decoder's fixed stack floor, record the Windows run
The README and decode_payload claimed the calling thread needs only its own
frames. Measured, the door has a payload-independent floor (release 56 KiB vs
16 for a same-signature non-decoding function; debug 104 vs 88). Say so, and
replace "Windows not yet verified" with the one by-hand Starfire run of
decode_stack on x86_64-pc-windows-msvc.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:57:58 +02:00
Meta Vens
6a3132f139 cultmath: span stage value and gradient against an independent octave assembly
SpanStageGradientMatchesDifferences now also compares field.w and field.xyz with an octave sum assembled
in the test from Stage.Point and snoise_grad, so shift, offset, octaveStep, M, lacunarity and gain are pinned
numerically, which a value/gradient self-consistency check cannot do.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:53:37 +02:00
Meta Vens
427d81feda GameCult.Caching: SpanNoise MessagePack and JSON shapes
MessagePack [field, curvature, fold] beside CultPhasor in CultMathResolver, JSON {field, curvature,
fold} in CultMathJson; round-trip, hex and JSON golden cases beside CultPhasor's.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:48:44 +02:00
Meta Vens
c2722c651b cultmath: span_fbm and span_ridged return SpanNoise (field, curvature, fold)
The octave stages of a warp chain over a span, in C#: stretched, shifted, offset octave sums whose
field equals fbm_grad and ridged_grad bit for bit at the identity transform and whose curvature
and fold bounds derive from SNOISE_LIPSCHITZ, SNOISE_HESSIAN and SNOISE_THIRD only (local tier from
the midpoint Hessian's Frobenius norm plus the third-derivative term, capped by the global pin).
SpanStageTests encloses the Hessian norm and the branch departure over seeded spans in both regimes
and checks the gradient against differences and the stretch/axis conventions. README and design.md
state the span, the transform and the bound rules. No shader twin: stage-shaders is part 2.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:48:43 +02:00
Meta Vens
6fd60a979f cultmath: integral_clamp_linear normalises lo before the flat branch (gpu-clamp-lo-above-hi-undefined)
lo = min(lo, hi) now precedes the b == 0 branch in C# and HLSL (Unity copy byte-identical, GLSL
regenerated), so no clamp ever sees lo above hi; GLSL leaves that undefined. The b == 0 path used to
call clamp(a, lo, hi) with the raw bounds. New tests: the flat and sloped lo > hi values, the
statement-order check on the committed HLSL and GLSL, and an appended glsl-parity.json family of lo > hi
and infinite-bound edge cases (every earlier case byte-identical).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:42:04 +02:00
Meta Vens
b042bf19be cultmath: integral_clamp_linear is finite for infinite clamp bounds (integral-infinite-bound-nan)
Each held stretch contributes only when its length is positive, so an infinite bound times a zero
length never forms: relu (hi = +inf) and its mirrors give the finite integral instead of NaN, in C#,
HLSL (Unity copy byte-identical) and the generated GLSL. Finite cases are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:39:27 +02:00
Meta Vens
38a2f50eb0 cultmath: closed-form third tensor, SNOISE_THIRD re-pinned to 535.8009 (third-provenance-fd-noise)
ThirdTensor is now snoise_hessian's per-corner derivation differentiated once more, in double, on the
kernel's lattice, tie order and hash; central differences of the float Hessian are gone from the
measurement and survive only as the oracle in ThirdTensorMatchesDifferencesOfTheHessian (Richardson
extrapolated differences of the double Hessian). MeasureThird on the closed form: refined 487.0917,
x 1.10 = 535.8009 (the differenced tensor read 488.1047, its roundoff). SNOISE_THIRD, ThirdWitness,
CULTMATH_SNOISE_THIRD (both HLSL copies), the generated GLSL constant and the design.md provenance
paragraph follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:37:36 +02:00
Meta Vens
6e360cf56d cultmath: shader-constant pin reads the mirror's private const fields
The compiled mirror declares its static consts without public access; the first version of
EveryShaderConstantEqualsItsCSharpTwin found none and failed. Reflect NonPublic too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:37:35 +02:00
Meta Vens
6afa8ed9c4 cultcache-py, -rs and -ts: unit-arm tests assert against the bytes the C# reference wrote
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:33:24 +02:00
Meta Vens
ef4326258d Union parity vectors: unit-arm-denied, -floor and -ceiling, written by the C# reference
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:33:01 +02:00
Meta Vens
106002518f UnionParityVectorTests: a unit arm on a union no list reaches is pinned against the reference; comments no longer call id arms unit arms
Vectors follow in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:31:44 +02:00
Meta Vens
d545898fad Caching.Tests: an ExprGraph record survives a cache, is inherited or overridden whole by variants, and follows its base
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:31:44 +02:00
Meta Vens
e3f497197d Caching: README section on element ids; the hex refusal names the derived form for readable ids
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:31:44 +02:00
Meta Vens
5588e95fa2 cultmath: pin every shader constant to its C# twin (shader-third-constant-unpinned)
EveryShaderConstantEqualsItsCSharpTwin reflects each CULTMATH_* literal of the compiled HLSL
mirror and asserts it is bit-equal to the math constant of the same name, so CULTMATH_SNOISE_THIRD
(and every other constant) edited in one runtime fails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:26:51 +02:00
Meta Vens
d13cab8b76 cultmath-tapes map: Pass 8 cut sequence line 2 (stage-functions split in three; shader constants reachable by reflection)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:25:10 +02:00
Meta Vens
74ecbb2e0c decode_stack tests: the at-bound union fixture is allowed its large variant
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:22:34 +02:00
Meta Vens
13ac96c84f Research: ground motion law prior art (Nubis wind offset and density-motion reprojection, flow maps, curl noise, advection stretch)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:22:13 +02:00
Meta Vens
2fd9431a76 cultmath-tapes map: target r2 (tape, affine forms, noise pins, stage functions, span framework ground, lattice node); stale parked lines swept
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:10:40 +02:00
Meta Vens
b43c4dfc98 decode_stack tests: v1 snapshot and redb reads through the grown decoder
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:08:16 +02:00
Meta Vens
57243090ec MAX_RECORD_INLINE_BYTES doctests: serde-able fixtures, positive twins of each refusal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:02:52 +02:00
Meta Vens
f7c0902d8d decode_stack tests: box the decoded result on the small thread, measured thread sizes; doctest fixture serde-able
Measured on Yggdrasil: the decode needs 56 KiB (Wide32) and 80 KiB (Largest) in
debug on a thread that returns a bool; returning the record through
std::thread's own frames adds several copies of it, so the tests box it and
size threads at 96 KiB and from 160 KiB.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:56:57 +02:00
Meta Vens
95b98e588b cultcache-rs README: security model without the wasm claim, the record bound, build dependencies
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:48:58 +02:00
Meta Vens
40a089ff9a decode_stack tests: shallow depths, 64 KiB door, largest record on every thread size, legacy envelope
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:48:58 +02:00
Meta Vens
fc630d3b85 cultcache-rs 0.3.0: one grown decoder for stored bytes, derives refuse records over 16 KiB
decode_grown checks the stack before any frame that holds T, then serde_stacker
at every container; decode_payload walks the nesting bound and calls it; the
legacy envelope array, redb reads and v1 snapshots decode through it too.
Red zone 512 KiB against MAX_RECORD_INLINE_BYTES = 16 KiB, which the
DatabaseEntry, CultSlots and CultUnion derives assert at compile time.
cultcache-rs-derive 0.2.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:48:57 +02:00
Meta Vens
0e964f45ac Regenerate union-parity unknown-arm-list.msgpack with its Pending id
Written by UnionParityVectorTests under CULTCACHE_WRITE_VECTORS=1 on Yggdrasil.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:47:56 +02:00
Meta Vens
74170198e1 Regenerate the Python-written stores with element ids on the leaf records
Written by PythonWrittenStoreVectorTests under CULTCACHE_WRITE_VECTORS=1 on Yggdrasil; not hand-edited.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:47:43 +02:00
Meta Vens
bd27f7a34b Union parity: the unknown-arm-list case carries its Pending id; rust and ts keep unit-arm codec tests on a separate union
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:46:22 +02:00
Meta Vens
d15f5a33f3 Changelogs and variants map: C2a's entries sit under Unreleased, not 0.15.0
The manifests stay at main's versions; C2a's bumps are not releases (c0-ships-with-c2a).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:45:28 +02:00
Meta Vens
c05c7919d0 cultcache-ts and cultcache-py tests: golden models carry element ids; python-written store asserts them
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:44:28 +02:00
Meta Vens
e0747912c6 cultcache-rs and cultcache-py union parity tests: golden models carry element ids
Wrap and the Outcome arms gain id slots (unit arms become one-slot id records);
the Python unit-arm codec behaviour is kept on a separate unit union.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:43:28 +02:00
Meta Vens
b6b6572abf Regenerate the leaf and union golden vectors with element ids
Written by the C# reference under CULTCACHE_WRITE_VECTORS=1 (NumericLeafVectorTests,
UnionParityVectorTests) on Yggdrasil; not hand-edited. Mutation testing does not
apply to regenerated vectors.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:43:28 +02:00
Meta Vens
8baebcec83 UnionArmGuardTests: ShapeDoc's list arms Circle and Square carry element ids
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:42:39 +02:00
Meta Vens
b86becca93 Caching.Tests fixtures: element ids are 12-hex literals, the form the id rule accepts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:41:50 +02:00
Meta Vens
f84428b9b9 Caching.Tests fixtures: every list-reachable record and union arm carries an element id
LeafInner, the Node arms (Wrap new), the Outcome arms, and the GuardNode arms
(Branch, Unit8, StubArm new) take [Key] [CultElementId] string Id, with fixed
literal ids and hand-written emitters updated to the new slots. Goldens are
regenerated in the next commits.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 19:40:37 +02:00
Meta Vens
0f6edfbfbb Merge remote-tracking branch 'origin/main' into hands/variants-c2a-main 2026-10-14 19:38:48 +02:00
Meta Vens
1a12bc2d82 cultmath design: SNOISE_HESSIAN re-pinned at 55.642143, snoise_hessian float3x3, SNOISE_THIRD
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:36:15 +02:00
Meta Vens
4784c25cc6 Merge hands/cultmath-expr-ir 2026-10-14 13:35:46 +02:00
Meta Vens
9282836fa5 Merge hands/cultmath-snoise-hessian 2026-10-14 13:35:45 +02:00
Meta Vens
d47811c323 ExprGraph resolver: nil is nil, decode refuses non-canonical encodings, pin nested shape, nil-name and depth rules
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:54:32 +02:00
Meta Vens
646cc59426 ExprGraph lists are read-only views over private arrays; test short evaluator inputs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:54:31 +02:00
Meta Vens
9bb36a47bf cultmath: regenerate the af_snoise and af_fbm fixture families for the re-pinned SNOISE_HESSIAN
Their results carry the curvature constant; the platform-bound families stay as generated on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:13:49 +02:00
Meta Vens
2c5f71b959 Close the Stryker and hand-mutant survivors: port-name alphabet, refusal messages, zero outputs, column shape, substitution checks, self operand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:01:34 +02:00
Meta Vens
0e26f1e059 cultmath: regenerate CultMath.glsl and append the snoise_hessian and integral_clamp_linear fixture families
CultMath.glsl is GlslLowering's output (CULTMATH_WRITE_GLSL=1, run on Yggdrasil); the fixture keeps every
earlier case byte for byte (the platform-bound families stay as generated on Windows) and appends the three
new families, which depend on no libm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:55:40 +02:00
Meta Vens
e3ea6d08d4 cultmath hlsl: type-neutral comment on snoise_hessian's matrix layout (lowers to GLSL unchanged in meaning)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:54:48 +02:00
Meta Vens
97de733f1b cultmath: pin SNOISE_THIRD (536.9152) and re-pin SNOISE_HESSIAN (55.642143) on the analytic Hessian
MeasureThird on the tied kernel: refined 488.1047, x 1.10 = 536.9152, witness (-168.3755, -3.8205261,
-109.16948). MeasureHessian with the analytic Hessian: refined 50.58377, x 1.10 = 55.642143, witness
(-13.7920685, -9.672134, 61.563934); the central-difference value 56.050385 carried the quotient's
roundoff. README bullet for the span primitives. CultMath.glsl and the fixture are not yet regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:53:08 +02:00
Meta Vens
00a477e387 cultmath tests: snoise second-difference check tolerates kernel-edge points by a stated fraction
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:51:42 +02:00
Meta Vens
f7c228a98f Research: lattice-site-weight corrected; the fauna probe weights at the cell centre (the cellc variant), not the jittered site
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:48:38 +02:00
Meta Vens
276484e15b cultmath tests: second differences of snoise compared against a two-level Richardson extrapolation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:45:41 +02:00
Meta Vens
b8a5a9cc14 cultmath tests: ThirdNorm ascends from the six strongest starts only (cost of MeasureThird)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:43:57 +02:00
Meta Vens
0a4a14dd5e Test that Substitute carries params, constants and picks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:40:09 +02:00
Meta Vens
38473f3fa5 cultmath: snoise_hessian, integral_clamp_linear and SNOISE_THIRD scaffold (not yet measured or regenerated)
math.Span.cs adds the analytic Hessian of snoise(float3) on snoise_grad's tied kernel and the closed-form
integral of a clamped linear ramp; HLSL twins and the Unity copies follow, HessianNorm now reads the
library Hessian, the GLSL lowering gains float3x3 to mat3, and MeasureThird/ThirdConstantPinsSampled and
SpanPrimitiveTests land. SNOISE_THIRD is a placeholder (1) until MeasureThird runs; CultMath.glsl and the
fixture are not yet regenerated. Does not pass its tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:31:43 +02:00
Meta Vens
59cf29032e Research: span-chain shape (Taylor forms, Mitchell, Knoll, MPR, Engel) and site-ground field (WebGL2 splats, OIT, 3DGRT, EVER)
Eyes pass (2026-10-14) serving cultmath-tapes❓span-chain-shape and
cultmath-tapes❓site-ground-field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:27:08 +02:00
Meta Vens
21281a39d6 Research: where a weighted lattice cell gets its weight (literature, CultMath today, 2D probe)
Eyes pass (2026-10-14) serving cultmath-tapes❓lattice-site-weight-source.
Key measurement: a query-point weight is common to all candidates and never
changes the winner; site and cell-centre weights differ from each other on
11-15% of pixels; no variant pops in space or time while the window holds
the winner; the window must scale with the weight ratio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:24:47 +02:00
Meta Vens
9545c80c56 cultmath-tapes map: Pass 8 (volumetric prioritized: frontier, Lipschitz history, Span Lab pieces and owners, curl note, cut sequence)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:20:36 +02:00
Meta Vens
ed2c5843e4 Tests for CultMath.Expr: evaluator equals direct composition, canonical builds, substitution, validator rules, resolver round trip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:09:31 +02:00
Meta Vens
dfc720e754 Research: tape register model prior art (Fidget, MPR, GPU indexed locals) and body-Voronoi metrics
Eyes pass (2026-10-14) serving cultmath-tapes❓tape-register-model,
with a scope addition on weighted diagrams over gravity bodies and metric
parameterisation for question lattice-site-weight-source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:05:27 +02:00
Meta Vens
defe2b80de Document CultMath.Expr: design.md Expressions section, resolver README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:05:19 +02:00
Meta Vens
dc302a96a7 CultMath.Expr: scalar SSA expression graph, builder with substitution, one validator, evaluator, MessagePack shape
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>
2026-10-14 06:05:18 +02:00
Meta Vens
b136ad9bd4 Merge hands/cultmath-cycles-converters: Cycles shader-node semantics in CultMath, exact to unfused Cycles
Cut cultmath-tapes cycles-converters (spec r2, attempts h1-h3), held by Soul
s3 (verdict cultmath-tapes:verdict:cut-cycles-converters.s3) under ruling
cycles-kernel-reference-ruled. Main carried only docs since the branch base
b3bd8dd5, so the merged code is the Soul-verified head 53f4c213.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 05:57:52 +02:00
Meta Vens
f1ee728d26 cultmath-tapes map: Pass 7 (tape unparked for trophic graphs: body facts T1-T7, model page, cut sequence); tape target status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 05:57:41 +02:00
Meta Vens
9ec66d77f7 cultlib-gaps map: section D r2 (D10-D14: stack between checks scales with record size; door-top check, 512 KiB red zone, 16 KiB record bound)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 05:00:47 +02:00
Meta Vens
53f4c21311 CultMath: infinite.json written by oracle.py on Yggdrasil (Blender 5.2.2, SSE4.2); CultMath.Cycles.glsl regenerated for the power parity
infinite.json holds 47 cases of infinite operands for cycles_power, cycles_modulo and Map Range Smooth Step, made inside Blender's kernel; only that family was regenerated. CultMath.Cycles.glsl changed by the generator alone (CULTMATH_WRITE_GLSL=1); glsl-parity.json and CultMath.glsl are byte-identical. This commit builds and its tests are expected green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:46:11 +02:00
Meta Vens
77a60d2910 CultMath Cycles: an infinite exponent is even in cycles_power; the oracle's infinite family and the pins for infinite operands
cycles_power read b - 2 floor(0.5 b) != 0 as odd, which is NaN for b = +-inf, so a negative base got a negative result where Cycles' powf(a, +-inf) is never negative. Odd now means the remainder is exactly 1 (C#, HLSL, Unity copy). oracle.py gains the infinite family (operands made in-kernel as +-exp(100), results read through comparisons with 0) for power, modulo and Map Range Smooth Step; the infinite.json fixture and the regenerated GLSL follow in the next commit. This commit builds; InfiniteOperandsFollowBlender and CommittedGlslEqualsLowering fail until it lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:42:39 +02:00
Meta Vens
c1962181a2 Merge remote-tracking branch 'origin/hands/variants-c2a' into hands/variants-c2a-main 2026-10-14 04:38:07 +02:00
Meta Vens
b0956ebc10 cultmesh-ts tests: drop the reads that found a record under another schema id by parsing it
Main's codec-document tests and the RUDP document-handle test read a record
stored under codec_note, plain_note or a foreign id through a handle for an
alias schema, and one store test picked among foreign-id records by whether
they parse. retire-schema-alias makes that read refuse; C2a's own tests
(a handle reads only records under an id its definition declares) pin the
rule. The codec reads and syncs under the definition's own id stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:31:43 +02:00
Meta Vens
908bbd93ff cultcache-ts tests: payload-depth store tests read the record through its catalog entry
Main's two payload-depth tests staged a record with no catalog entry and
asserted that the inspector and the single-file store infer its schema from
the payload's first element. C2a refuses a record whose schema id the catalog
lacks (merge-r1-precedence, exact binding), so the guess door is gone. The
inspector test keeps the bound check with a catalog entry that names the
schema; the store inference test goes with the door.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:31:43 +02:00
Meta Vens
a114d6a9db cultmesh-ts tests: drop the bare-id-string snapshot handle assertion
Main's documentFromPeerSnapshot takes a definition only, so the string-handle
read (first record at the key) that C2a held for Eve Electron cannot be
expressed; the assertion and requestCultNetRawSnapshotDocument's
firstRecordAtKey option went with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:24:12 +02:00
Meta Vens
25275e76ba cultcache-ts tests: C2a's store tests build envelopes with main's DocumentPayload
C2a's writer, catalog and soft-push tests handed raw Uint8Array payloads to
the store. Main made the envelope payload an opaque DocumentPayload, so they
wrap with documentPayloadFromBytes and read back with documentPayloadBytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:24:10 +02:00
Meta Vens
3b83cb6c08 Merge origin/main into hands/variants-c2a-main (main-into-c2a)
Sixteen conflicted files resolved by the spec's rule: main owns payload
ownership, bounds and the Clock seam; C2a owns persistence semantics and exact
binding. MintStoredAt is internal static (previous, now); the cache and the
store pass their cache's Clock. Package versions are main's. The cultmesh-ts
payload-parse acceptance and its test are deleted under retire-schema-alias;
peer snapshot reads take the schema's own id, else an id the definition lists
as compatible. The held firstRecordAtKey fallback is removed: main's handles
take definitions only, so nothing could reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:06:41 +02:00
Meta Vens
6c902fd449 cultlib-gaps map: P17 records the mixed-width NaN key refusal as a deliberate exception (py-msgpack-default r5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:19:03 +02:00
Meta Vens
b5ef551d84 JudgeChecks lock contract: tests pin the read-after-lock order and the directory judge's wait=false
A non-home store (single-file and directory) is read only once the commit holds its own lock, so a foreign record landed at a checked key
while the commit waits is refused; and a directory judge whose lease is held makes TryCommit Contended at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:14:46 +02:00
Meta Vens
d8f38057a1 CultMath: the six cycles-oracle fixtures regenerated by oracle.py on Yggdrasil (per-function seeds, arcsine/arccosine/arctan2 rows, overflowing smoothstep rows); CultMath.Cycles.glsl and glsl-parity.json regenerated
Fixtures are the oracle's output (SSE4.2 kernel, Blender 5.2.2 Linux), two runs cmp-identical; CultMath.glsl line 1 (the
Cycles function list in the generated header) carries the three new names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 03:00:32 +02:00
Meta Vens
2505c0c45f decode_stack test: repeat_n for the clippy lint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:55:21 +02:00
Meta Vens
c65725da8f CultMath Cycles: arcsine, arccosine and arctan2 with Cycles' guards; per-function oracle seeds; the ruled unfused reference, empty Tolerances and the class table
Fixtures and the regenerated GLSL follow in the next commit, so the tests that read the new rows
(InverseTrigonometry..., OperandStreams...) are red at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:45:51 +02:00
Meta Vens
4dd19467c9 cultmath-tapes map: the Cycles cuts under cycles-kernel-reference-ruled (AVX2 single-rounding probe, documented bound)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:38:35 +02:00
Meta Vens
d3f8dea75a cultcache-rs: decode_payload grows its stack through serde_stacker
A payload inside the 256 nesting bound no longer depends on the caller's
thread stack: the rmp-serde deserializer runs under serde_stacker with a
256 KiB red zone and 2 MiB segments. Adds tests/decode_stack.rs (child
process per test, sized decode thread) and renames the 2 MiB union_parity
stack test to a 1 MiB pin.

Not yet built or run at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:38:23 +02:00
Meta Vens
b6118cfd45 cultlib-gaps map: section D (decode-stack-safety probes D1-D9, authority) and cut-order item 7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:36:15 +02:00
Meta Vens
696a712088 JudgeChecks (directory): drop the CreateDirectory the commit lease already makes; tests for a never-written non-home store and for a check-less batch beside a held lock
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:23:14 +02:00
Meta Vens
7be966b485 Contracts: every writable store judges a batch's checks; a store kind lists foreign records at the version it read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:07:21 +02:00
Meta Vens
045db8f98a Tests: non-home stores judge checks under their lock, Contended before the home writes, refusal-listed foreign version, outside store versions foreign records
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:07:20 +02:00
Meta Vens
1893410fe1 Every writable store judges a batch's unheld removals under its own lock; a foreign record's version comes through Foreign
CacheBackingStore.JudgeChecks (default: the view; single-file and directory stores read under their lock) replaces Land's view-only loop over non-home stores. Foreign takes the version the store read; CultForeignRecord.StoredVersion is get-only and set by the ctor, and every assignment after Foreign is deleted. One commit carries both spec commits 1 and 2 (the edits share hunks); unverified until the final job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:07:19 +02:00
Meta Vens
beb8362fbc cultlib-gaps map: py-msgpack-default r4 (convert to a new file under py-migration-shape-ruled; P14-P17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:01:36 +02:00
Meta Vens
ae6ef5a842 Research: Cycles FMA contraction across kernels and backends, for the kernel-pin fork
Eyes pass (2026-10-14) serving cultmath-tapes finding
cut-cycles-converters.s1.kernel-pin-unraised-fork: Cycles' per-ISA flags
and contraction history, MSVC /fp:fast, GPU fast-math, shader-language
contraction rules, .NET FMA, and image-threshold test policies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:59:11 +02:00
Meta Vens
e48f948892 CultMath: mix.json regenerated with Burn rows that put a colour above 1 in every channel
CyclesOracleTests and the mirror tests pass (29 of 29) on it. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 10:02:37 +02:00
Meta Vens
3ea9c32382 CultMath oracle: the exact-zero Burn rows put a colour above 1 in every channel
The z channel's boundary mutant survived because 0/0 reads as 0 after the clamp. mix.json follows. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 10:00:57 +02:00
Meta Vens
e9f6de2164 CultMath: mix.json regenerated with the exact-zero Burn divisor cases
Byte-identical regeneration for the other five families. CyclesOracleTests and the mirror tests pass (29 of 29).
Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 09:51:51 +02:00
Meta Vens
3a83ee361a CultMath: Burn cases with a divisor of exactly 0, and tests for what the film hides (guards returning 0) and for rgb_to_hsv's primaries
Stryker's survivors in math.Cycles.cs that were not equivalent: Burn's divisor <= 0 boundary, Logarithm's
a <= 0 guard, and rgb_to_hsv's h < 0 wrap. mix.json is regenerated in a later commit, so
FamilyMatchesBlender is unchanged here and the new Burn rows are absent. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 09:50:14 +02:00
Meta Vens
cf6354584f CultMath: CultMath.Cycles.glsl regenerated for the power change
CultMath.Tests passes 386 of 386 at this commit's tree. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 09:11:09 +02:00
Meta Vens
7d42eb3f20 CultMath: cycles_power has no int-range quirk (Blender returns 1 for (-1)^(2^31)); math.json regenerated with the -1 base and Compare epsilon cases
The oracle showed Cycles treats a huge exponent as integral for a negative base, so the speculative
b >= 2^31 rule is gone. CultMath.Cycles.glsl is stale at this commit (the HLSL changed), so
CommittedGlslEqualsLowering fails here; the regenerated file follows. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 09:07:53 +02:00
Meta Vens
602c88444d CultMath oracle: power cases with a base of -1 and exponents past the int range, and compare cases inside FLT_EPSILON
The film zeroes an infinite result, which had hidden both the power quirk and Compare's epsilon floor
from the fixtures (two hand mutants of the HLSL survived). The fixtures are regenerated in a later commit.

Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 09:04:28 +02:00
Meta Vens
6196dcde17 CultMath: CyclesOracleTests also holds the HLSL mirror (the shader text compiled as C#) to Blender's results at the fixture operands
A mutant of CultMath.Cycles.hlsl that changes a Cycles result the fixtures reach now fails a test on its own;
before, only the random-input mirror comparison reached the HLSL. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 08:46:41 +02:00
Meta Vens
5a3cf7e9a9 Tests: a cache disposed with one writable store holding staged checks beside a read-only one still judges them
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 07:46:10 +02:00
Meta Vens
d0775706e5 Tests: the non-home store case gives its second store a home type of its own
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 07:39:32 +02:00
Meta Vens
7b604d41bd Tests: check-only writes write no file and clear their checks, dispose and read-only stores judge them, a non-home store answers from its view and the home under its lock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 07:36:45 +02:00
Meta Vens
f496df999a cultlib-gaps map: py-msgpack-default r3 (backup-and-compare migration, one key owner, one depth walk)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:30:03 +02:00
Meta Vens
72ca2c745f Research: stack-safe typed decoding prior art, for follow-up decode-stack-safety
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:51:40 +02:00
Meta Vens
abe98fc40a CultMath: the oracle test names its one tolerance (power and logarithm, one ulp for another OS's libm)
Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 06:20:31 +02:00
Meta Vens
8ec0446e33 CultMath: the Cycles oracle fixtures (Blender 5.2.2 LTS d13f752e3b9c, Linux, SSE4.2 kernel), CultMath.Cycles.glsl and the golden-fixture families
Written by tools/cycles-oracle/oracle.py and GlslLowering on Yggdrasil; the tool's second run was
byte-identical. CultMath.Tests passes 379 of 379 at this commit's tree. Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 06:19:42 +02:00
Meta Vens
ddca0252e6 CultMath: a Blender ramp table has 257 entries (size + 1 at i / 256); the oracle samples it so, and the oracle test leaves unread operands unread
Builds. Fixtures and CultMath.Cycles.glsl are still to come.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 06:14:44 +02:00
Meta Vens
67c79d5cf8 cultlib-gaps map: section P (payload format authority, atomic migration, Brokkr landing contract) for py-msgpack-default r2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:09:04 +02:00
Meta Vens
892cfbaa6a CultMath: rgb_to_hsv divides by 6 the way Cycles' kernel does (a multiply by 1/6); the ramp is checked by a constant ramp (index) and a linear one (weight); Map Range sockets by their identifiers; the mirror pins 115 functions
Builds. Fixtures and CultMath.Cycles.glsl are still to come.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 05:51:30 +02:00
Meta Vens
51c5da11c4 CultMath: cycles_modulo is exact (power-of-two subtraction, no quotient form); RGB to BW coefficients are Blender's; oracle.py pins the SSE4.2 CPU kernel
The oracle's first fixtures showed Cycles' AVX2 kernel fuses multiply-adds and its film zeroes
non-finite results; the tool now exits 4 off the SSE4.2 kernel (CYCLES_CPU_NO_AVX2=1) and the
test skips cases Cycles cannot report. The Unity copy of CultMath.hlsl carries the new include.

Builds. Fixtures and CultMath.Cycles.glsl are still to come.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 05:28:03 +02:00
Meta Vens
cec09806b0 Caching: an empty direct flush still applies as before; only a write that checked keys and has nothing else to write writes no file. Tests: the delisting writer declares the widget; the checked-key flush also writes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 05:06:19 +02:00
Meta Vens
e3c94f8edc CultMath: oracle.py reads Mix and Map Range sockets by identifier; GlslMirrorTests compiles; compile-glsl.ps1 takes -Cycles
Builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:41:18 +02:00
Meta Vens
649ac1860e Tests: a flush clears the checks it judged and a refusal forgets the check it refused
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:09:43 +02:00
Meta Vens
355cfa2a3d CultMath: cycles_ramp_index casts its int operands explicitly (GLSL ES 3.00 has no implicit int to float); README and GLSL file-contract tests name the Cycles files
Builds. The oracle fixtures and CultMath.Cycles.glsl are still to come, so the oracle and golden tests fail here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:07:08 +02:00
Meta Vens
99884cc34c CultMath: the Cycles oracle tool (oracle.py) and CyclesOracleTests
oracle.py bakes each Cycles node under Blender 5.2.2 LTS (build d13f752e3b9c, refusing any other)
into float images; CyclesOracleTests holds math.cycles_* to the fixtures it writes. No fixtures
exist at this commit, so the tests fail here; builds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:04:39 +02:00
Meta Vens
2e3a5b8e16 Contracts: an unheld removal is a check-only key judged under the store's lock; ExpectUnchanged compares foreign records
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:03:42 +02:00
Meta Vens
2067dcd1a2 Tests: unheld-removal checks judged at the commit and flush, checks never remove, ExpectUnchanged over foreign records, delisting and variant-refusal listing pinned
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:03:41 +02:00
Meta Vens
3a18f01a83 Caching: a removal of a key the cache does not hold is a check-only key judged under the store's lock; ExpectUnchanged compares foreign records too
Remove and Commit of an unheld key no longer read the store at the call: the key travels as CultCommitRequest.Checks (commit) or a staged check (flush),
and each writable store judges it in RefuseUndeclaredOnDisk with the keys it writes, never removing it. A batch that only checks writes no file.
CultForeignRecord.StoredVersion records the version a store read, and the one ConditionsHold takes the store's foreign records.
Folds spec commits 1 and 2 (their hunks share the same methods). The src projects build; tests/GameCult.Caching.Tests does not at this commit
(CultCommitRequest gained a parameter; the next commit updates the test).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 04:03:41 +02:00
Meta Vens
559800f494 CultMath: Cycles node semantics (Math, Map Range, Clamp, Mix, RGB to BW, ramp index) as math.Cycles.cs and CultMath.Cycles.hlsl under Apache-2.0
Ports of Blender v5.2.2's intern/cycles kernel functions, one function per node
operation, with the HLSL mirror, the Unity copy, the lowering table entry for a
separate Apache-2.0 GLSL file, the golden-fixture families, the package licence
expressions and the notices section.

Builds. Does not pass yet: CultMath.Cycles.glsl and the regenerated golden
fixture arrive in a later commit, so CommittedGlslEqualsLowering and
GoldenFixtureMatchesCSharp fail at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 03:59:59 +02:00
Meta Vens
9276942f45 Variants map: body at 2026-10-12 (C2a vs main probes) and the cut sequence under map-only-what-runs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:58:08 +02:00
Meta Vens
b3bd8dd5e5 Merge hands/py-typed-leaves: C# leaf vectors (leaf-vectors-cs) and cultcache-py typed leaves that own each slot's C# type name and checks, held to them
leaf-vectors-cs Soul-held at pass 3 (cultlib-gaps:verdict:cut-leaf-vectors-cs.s3); py-typed-leaves Soul-held at pass 2 (cultlib-gaps:verdict:cut-py-typed-leaves.s2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:28:58 +02:00
Meta Vens
10521f3598 cultcache-py test: the any-row comment cites the ruling in force, any-rewrite-binds-kind-and-value
Comment only. Soul finding cut-py-typed-leaves.s2.stale-superseded-ruling-comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:28:26 +02:00
Meta Vens
8e0a6cf428 Merge hands/union-bounds-ts: cultcache-ts payload nesting bound (256) in the one document-payload owner, held to the C# bounds vectors
Soul-held at pass 3 (cultlib-gaps:verdict:cut-union-bounds-ts.s3). Lands follow-up ts-codec-pin-gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:10:50 +02:00
Meta Vens
4282d507cf Caching tests: the wrong-catalog refusal is raised while the store is added, so both steps sit inside the asserted call
Previous commit 122894f5 failed these four cases (AddBackingStore threw outside Assert.CatchAsync).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 02:06:26 +02:00
Meta Vens
122894f5f8 Caching tests: the reference opens a Python-written typed-leaf store and refuses wrong type names
Vectors py-store/*.cc are written only by cultcache-py (CULTCACHE_WRITE_VECTORS=1).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 02:01:48 +02:00
Meta Vens
100088520e cultcache-py test: the any-row comparison tells bool, int and float apart; README points at the kind-and-value ruling
Ruling any-rewrite-binds-kind-and-value: kind-strict, exact value, recursive, numeric width free.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 02:01:47 +02:00
Meta Vens
3b2af2b232 cultcache-ts: previewPayload reads through decodeBoundedPayload (the inspector's one walk site); the map32 and array32 counts are pinned above 65535
The wide-count test announced map32 as 32770 pairs, which fits in 16 bits, so a
count read as its low 16 bits survived. map32 now announces 65537 pairs and
array32 65540 children; both low halves close the container early.

Builds: yes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:50:05 +02:00
Meta Vens
0451e4afc1 cultcache-ts test: the plain-document write refusal names its document type (kills the Stryker survivor on the encode subject)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:32:15 +02:00
Meta Vens
eb01c414a6 cultcache-py: one number path for the float leaves (no write/read flag); tests pin the refusal texts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:24:35 +02:00
Meta Vens
986faf1f33 test:pack:ts probe shows each exported function a map and a slot record (readWire refuses a map, so a wrapper of it passed the map-only probe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:22:47 +02:00
Meta Vens
ed30592ea7 cultcache-py: an extension is reported as an extension (a namedtuple was read as an array); boundary tests for the leaves
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:17:28 +02:00
Meta Vens
542a3d904e cultcache-ts test: the deep codec document uses a slot codec that writes N nested arrays (a self-recursive list codec has no finite type name)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:11:18 +02:00
Meta Vens
d64f688459 test fixes: the grey-image schema is a custom check, and the pack probe treats a throwing result as not a reader
Fixes the previous commit (05f9937b, mislabelled wip): its test file did not compile (TS2322) and the pack probe threw on a lazy codec's getters. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:08:44 +02:00
Meta Vens
8ebe43fe7a cultcache-py test: the leaf-record mirror is one function
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:07:52 +02:00
Meta Vens
05f9937bbc wip 2026-10-12 01:07:51 +02:00
Meta Vens
58ee315d11 cultcache-py: typed leaves own each slot's C# type name, write check and read check; the catalog derives from them
leaves.py (string, boolean, bytes, int32, int64, float32, float64, any, list, array, nullable, lazy) held to the
numeric-leaves vectors. Removed: DatabaseEntryField.many, callable codecs and _resolve, the System.Object literal,
the present-nil default, the union's nil pass-through, interop.py's hand-written catalog. A float32 keeps its width
through _pack_tree. Nil is refused unless wrapped in nullable; a default fills only a slot past the payload's end.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:06:24 +02:00
Meta Vens
27b9e2b315 test:pack:ts denies payload readers by identity and behaviour, not by name
Finding pack-denial-by-name: the package-internal readers (readWire, resolveDocumentFormatter, @msgpack decode) are loaded from the packed dist by path and searched by identity through every packed entry, including default and namespace members; every other exported function is shown a MessagePack map and must not return it decoded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:04:27 +02:00
Meta Vens
c40cc555e1 cultcache-ts: the payload walk covers library-written MessagePack only; a user formatter's bytes are its own, and the 32-bit walk lengths are pinned
Findings custom-formatter-bytes-walked (ruling depth-walk-covers-library-msgpack: the owner skips the walk when the definition carries a formatter; the grey 0x91 image round-trips, plain and codec stay bounded; README says so) and wide-length-unpinned (bin32/str32/ext32 of 65539 bytes and array32/map32 of more than 65535 children before a chain at and past the limit). The same tests pin the inspector refusing to recover a stamp from a past-limit payload. Not yet built.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:04:27 +02:00
Meta Vens
ef682ad7b0 cultcache-ts inspector: recoverCatalogEntry reads through the same bounded door as previewPayload
Finding inspector-recover-unwalked: one decodeBoundedPayload (walk, then decode) serves both the stamp recovery and the preview, so a payload past the limit is never decoded by either.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 01:04:26 +02:00
Meta Vens
7f38134155 Caching tests: regenerate leaves-reads.msgpack from the C# reference (11 leaves x 28 tokens, 308 rows) and the README: timestamp rows, any rewrite binds value
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:35:06 +02:00
Meta Vens
2bcbe17253 Caching tests: leaves-reads tokens for the timestamp32/64/96 exts and same-shaped non-timestamp exts (vector tests fail here: vectors stale)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:34:09 +02:00
Meta Vens
51631f07d5 Caching tests: regenerate leaves-reads.msgpack from the C# reference (11 leaves x 23 tokens, 253 rows) and the README rows
leaf-record.msgpack is unchanged. The any leaf now has a ext refusal (d40100) and the record leaves read a complete second slot.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:19:25 +02:00
Meta Vens
08e1a2cd37 Caching tests: drop the timestamp ext token, which the any leaf accepts (vectors not yet regenerated)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:18:46 +02:00
Meta Vens
719cf655b6 Caching tests: leaves-reads tokens for ext values and a complete record, and a list-of-records leaf (vectors not yet regenerated; the vector tests fail at this commit)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:17:41 +02:00
Meta Vens
9df32ec64f cultcache-ts test: the walk's header counts, the never-used 0xc1 and the longest fixstr, and a scalar that ends a full-depth chain
Kills the survivors of the first mutation sweep (fixext2 skip, 0xc1, the fixarray/fixmap/fixstr upper bounds, map16 and map32 child counts).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:17:09 +02:00
Meta Vens
85d8d575cf Caching tests: write slot-nil/reads.msgpack from the C# reference with the two four-byte UTF-8 rows (CULTCACHE_WRITE_VECTORS=1 on Yggdrasil)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:09:37 +02:00
Meta Vens
c114820c0c Caching and cultcache-ts tests: slot-nil rows for a four-byte UTF-8 character in a string slot and in a byte[] slot
The C# cases and the TS assertions are here; the vector file is written from the reference in the next commit, so the
slot-nil tests of both runtimes fail at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:08:23 +02:00
Meta Vens
8dfdcd8a53 cultcache-ts test: the Node arm list names Wrap, and the put-refused assertion keeps the store's type
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:05:27 +02:00
Meta Vens
b69d3057a5 cultcache-ts: the payload nesting bound (256) in the one document-payload owner, so every TS payload door shares it
checkPayloadDepth is an iterative header walk run by decodeDocumentPayload before the formatter and by
encodeDocumentPayload on the formatter's bytes; the two plain-decode inspection doors call it first. The plain
formatter's encoder depth is set above the limit so the walk decides. Tests: the C# bounds vectors, the walk's
header and scalar rules, the owner, the inspector, the legacy version inference, and the mesh network doors;
test:pack:ts denies decoder-shaped exports by enumerating the packed entries.

Builds and passes untested at this commit (verification runs on Yggdrasil next).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 00:01:59 +02:00
Meta Vens
067d015122 Caching tests: write leaves-reads.msgpack and leaf-record.msgpack from the C# reference (CULTCACHE_WRITE_VECTORS=1 on Yggdrasil)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 23:58:04 +02:00
Meta Vens
9df776d385 Caching tests: the wide-leaf read and leaf-record vector builders and tests, README rows (vectors not yet written; the two new tests fail until the next commit)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 23:57:05 +02:00
Meta Vens
c770db6e77 Merge hands/union-codec-ts: cultcache-ts slot record and union codecs, and one typed owner of document-payload reads and writes (DocumentPayload)
Soul passes cultlib-gaps:verdict:cut-union-codec-ts.s1-s9. Operator rulings ts-payload-opaque-type and ts-opaque-type-kept. Low pin gaps deferred to follow_up ts-codec-pin-gaps.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:40:37 +02:00
Meta Vens
d877d26a05 cultcache-ts test: the slot-nil vector pins a non-ASCII str in a bytes slot as UTF-8 (C#-written)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:03:21 +02:00
Meta Vens
99682077a1 cultcache-ts docs: README and DocumentPayload list the routes that read a payload without the owner
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:03:20 +02:00
Meta Vens
71d8ff3080 cultcache-ts: codecFormatter moves into the private document-formatter module; pack smoke refuses it from the root and ./document
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:03:20 +02:00
Meta Vens
ce52d9aa60 Caching tests: slot-nil case for a non-ASCII str in a byte[] slot
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 22:01:42 +02:00
Meta Vens
788dee3bd2 cultmesh-ts: drop AetheriaEve from the QUIC-free consumer list (archived, not a target)
Operator ruling cultlib-gaps:ruling:aetheria-eve-not-a-consumer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:38:30 +02:00
Meta Vens
897be76fa8 Research: how JSON became cultcache-py's payload default, and the MessagePack-default probes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:13:41 +02:00
Meta Vens
6fbce23f92 cultcache-ts test: the cache keeps its own copy of an admitted envelope's bytes and of the ones it hands out
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:05:09 +02:00
Meta Vens
12a0fd7744 cultcache-ts: resolveDocumentFormatter moves out of the ./document entry into a module no export names
The packed-consumer smoke now fails if the document entry exports it, at run time and at compile time.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:05:09 +02:00
Meta Vens
c85edf90bb cultcache-ts slots: a default fills only a missing slot, a nil is null in a reference slot and refused in a value slot, bytes reads a str
Held to tests/vectors/slot-nil/reads.msgpack, written by the C# reference (SlotNilVectorTests): every accepted row
rewrites to the reference's bytes, nil in an int slot is refused. 94a26431c000c0 round-trips byte-identical.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:05:09 +02:00
Meta Vens
6d410f9caf slot-nil vectors: the C# writer for what the reference reads and rewrites for a nil or short record in each slot kind
Does not build the vector yet: reads.msgpack is written in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 21:01:33 +02:00
Meta Vens
8cff9b7f30 verify-eve-browser-network: a waiter matches only lines whose newline has arrived
A stdout chunk can end inside the benchmark JSON line; the waiter matched the half line and JSON.parse threw (seen once
in two runs on the rust image). Waiters now read complete lines only.

All commits build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 20:51:35 +02:00
Meta Vens
b25d86a873 Docs: DocumentPayload stops direct reads and assignment, not runtime narrowing or a cast
The cultcache-ts README, the DocumentPayload comment (comment only) and the cultmesh-browser README say plainly what the
opaque type guarantees and what it does not; documentPayloadBytes is the escape for bytes.

All commits build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 20:37:29 +02:00
Meta Vens
a6478a9cab CultMeshRudpDocumentPut discriminates a decoded document from an undecoded payload; the store read's schema-matched find is pinned
The put is a union on `decoded`: true carries the document read through the registry's definition, false carries the
opaque DocumentPayload, so the owner read on the undecoded branch compiles without a cast (compile fixture in
payload-type.test.ts) and the three test casts are gone. A store holding a foreign parsing record, a matched record that
does not parse and a matched record that parses must return the last, which kills the survivors on the schema-matched
second find in resolveCultMeshStoreDocumentRecord.

All commits build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 20:37:29 +02:00
Meta Vens
195c37b867 Eve browser sample reads its payloads through definitions, and the verifier type-checks the page
main.ts called decodeCultNetPayload with no definition, which the required-definition signature refuses; esbuild strips
types, so only the browser noticed. The sample now holds a counter definition and plain-map definitions for the Eve
documents, and verify-eve-browser-network.mjs runs tsc over main.ts against the module map the bundle uses (and builds
cultcache-ts, which cultmesh-browser now imports) before bundling. The receipt is read as the typed wire object instead
of a cast of the Eve receipt type.

All commits build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 20:37:29 +02:00
Meta Vens
13b0b02d82 Provider command refusals name the field and echo none of the values
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:51:08 +02:00
Meta Vens
9d7b584285 Research: Python typed-leaves probes (C# opens a leaf-cataloged Python store; prototype leaves hold the numeric vectors)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:47:37 +02:00
Meta Vens
9d2d25dae1 A RUDP put naming a schema with no registered definition reaches the handler as the untouched opaque payload
The put handler no longer plain-decodes such a payload, and does not refuse it either: five
existing server tests put through an empty registry, a catch-all server is a capability a general
library keeps, and the payload type already stops a plain read. CultMeshRudpDocumentPut.payload
documents the two cases; a handler that holds the definition reads it with decodeDocumentPayload.
Builds in every package; suites run at the end of the pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:45:59 +02:00
Meta Vens
0c5207b55f One typed owner of document-payload reads and writes: DocumentPayload is opaque, the definition is required
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.

Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.

Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:33:42 +02:00
Meta Vens
a3a45c79eb Delete the text-scan payload guards, the schemaId overloads and the definition-less decode door
The two payload-decode-owner text-scan tests go, as do the string-schemaId overloads and
branches of the cultMeshDocumentFrom* functions and statics, decodeCultMeshPayload,
decodeCultNetRawDocumentPayload, toUint8Array, replication's second decodeDocumentValue, and the
undefined branch of decodeDocumentPayload. decodeDocumentPayload now requires the definition and
also runs its schema (the one reader). A publication binding names a definition, never a bare
schema id; the RUDP put handler refuses a schema with no registered definition instead of
plain-decoding it. Builds in every package; tests not yet rerun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 19:21:32 +02:00
Meta Vens
4ec4bfb7e6 Union parity map: the payload depth bound's owner and doors per runtime at c028c511; the pre-union probes U7-U9 dated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:05:39 +02:00
Meta Vens
c028c51102 Merge hands/union-bounds-rs: one 256 nesting bound at every Rust payload door, cache, mesh network puts and cultnet decoders, held to the C# bounds vectors
Soul passes cultlib-gaps:verdict:cut-union-bounds-rs.s1, .s2 and .s3.
Low residuals deferred to follow_ups depth-vectors-nest-every-container
and rust-past-limit-record-handling; encode-stack-safety stays open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:01:38 +02:00
Meta Vens
595ca47c63 cultnet-rs tests: a 15-slot record grows to array16 in the payload test
This commit builds; the previous commit's test panicked on the header.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 18:35:54 +02:00
Meta Vens
84bb1e28a5 cultnet-rs: the exact-length record decoders are left on rmp_serde, and the payload test covers all five media records
The lifecycle brake, signed daemon health and runtime authority records check
an exact positional array length before decoding, so they have no undeclared
slot to skip; they return to the plain decoder. The media records have no
length check and their derive skips undeclared trailing slots, so all five
are held by the test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 18:35:19 +02:00
Meta Vens
8831539831 cultnet-rs: every wire payload decoder goes through cultcache-rs decode_payload, and a test pins the bound
Document payloads decoded with rmp_serde::from_slice in provider_session,
media_stream_wire, idunn_lifecycle_brake, provider_health_contracts,
runtime_authority_contracts and the CultNet message decode in contracts.rs now
decode through the one depth decision. The duplicated schema-v0 branch in
decode_cultnet_message_from_slice is deleted.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 18:33:36 +02:00
Meta Vens
86c6f19c67 Research: a document payload decoder the TypeScript type system enforces
Probe table (brands, class wrapper, opaque object type), every TS payload
read site and consumer, the test-wiring gaps, and prior art, for
cultlib-gaps❓ts-payload-opaque-type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:06:31 +02:00
Meta Vens
5618d8dde7 Merge hands/union-bounds-py: one 256 nesting bound on read and write in cultcache-py, with the arm-key, slot-key and nil-slot refusals, held to the C# bounds vectors
Soul passes cultlib-gaps:verdict:cut-union-bounds-py.s1 and .s2. Low
residuals deferred to follow_ups py-depth-bound-residuals and
py-default-payload-json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:01:50 +02:00
Meta Vens
7fecb32ae8 cultmesh-rs tests: pull_rudp_catalog_snapshot against a served snapshot refuses a 257-deep payload
Delete the apply_raw_snapshot_documents extraction (no production change in cultmesh-rs remains):
the test serves a snapshot over loopback RUDP and pulls it through the node. Two at-limit documents
land; a document one level past the bound is refused with PayloadTooDeep and the store file is
byte-identical afterwards. Supersedes the seam test of 735654f0, which this commit removes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 18:00:38 +02:00
Meta Vens
66eeb22052 cultcache-rs tests: drop the leftover mixed case so union_parity compiles
The previous commit's union_parity.rs did not compile (mixed_past was removed from its definition but
not from the case list). a5c0f876 does not build its tests; this commit does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 17:55:19 +02:00
Meta Vens
e1b8deec16 Research: the C# reference refuses a Python define_database_entry_type store
Section 1.6: probe on Yggdrasil at 2037dd44. Python's untyped catalog
stamps System.Object, and the reference refuses the whole store at
open. Evidence for cultlib-gaps❓py-slot-scalar-types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:52:43 +02:00
Meta Vens
735654f077 cultmesh-rs: snapshot documents are applied by one method, and a 257-deep peer payload is refused
pull_rudp_catalog_snapshot hands its documents to apply_raw_snapshot_documents (the network-free
seam); the test feeds it a payload one level past the bound and checks nothing reaches the store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 17:52:25 +02:00
Meta Vens
d47cd03890 cultnet-rs: stored envelope payloads decode through cultcache-rs decode_payload
replication's document_record_from_envelope and the service identity credential read no longer
call rmp_serde::from_slice on a stored payload, so the 256 bound holds there too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 17:52:25 +02:00
Meta Vens
a5c0f876de cultcache-rs: the payload nesting bound at put_raw_envelope and put_prepared_batch
Both raw write doors run the one check_payload_depth that decode_payload runs first, before any
store is touched; decode_payload is public so envelope readers elsewhere decode through the same
helper. Tests: union arms, maps and arrays at 256 and 257 through both doors, a batch that stages a
valid entry beside a deep one persists neither, the read vectors enter by a store file written
behind the cache.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 17:52:24 +02:00
Meta Vens
2037dd44c9 Research: Python slot scalar types against the C# reference's refusals
Prior-art and Body facts for the cultlib-gaps fork raised by
cut-union-bounds-py.h1 (undone): whether Python slots grow a scalar
type vocabulary to match the C# reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:47:58 +02:00
Meta Vens
7f6649f9a1 cultcache-py: the payload depth count takes every mapping, sequence and set; a codec's RecursionError is no longer relabelled as a depth refusal
The tree walk counted only list, tuple and dict, so a codec whose tree used deque,
mappingproxy or another abc container was unbounded. Every Mapping, Sequence and Set
that is not text or bytes now counts. RecursionError from a user encode or decode,
and from the JSON decoder, reaches the caller as itself with its traceback; only
msgpack's StackError (raised by depth alone) is mapped to the depth refusal.
All commits build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 17:47:55 +02:00
Meta Vens
89e1755281 cultcache-rs tests: ext arm keys class as non-integer, and nil slots on a unit arm and an arm with slots
Pins ext-key-class-arity (fixext 1, timestamp, ext 8) and the nil-slot refusal at the
derive's own seam. Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:38:20 +02:00
Meta Vens
294469e300 Payload-owner guard finds its package root from the file; refusals pinned in the decode tests
The guard test counted its package root from the compiled layout and failed under tsx and for a one-file src; it now walks up to
the package.json. The browser's non-messagepack refusal and the cache's codec-and-formatter refusal are pinned by message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:31:08 +02:00
Meta Vens
f06cd84596 cultcache-rs: element_header owns truncation; the walk tests fill skipped bytes with array markers
Mutation survivors of the first run: the unwrap_or(usize::MAX - k) fallbacks were equivalent
mutants, so a truncated header now returns None through the one helper. Child counts and long
lengths (map16/32, array16/32, str/bin/ext 16 and 32 bit) are tested with skipped bytes that
read as containers when a skip stops short. Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:30:10 +02:00
Meta Vens
f1e7b36a39 Payload decode has one owner: cultcache-ts decodeDocumentPayload; wire values go through cultnet-ts/wire
The RUDP put handler and the browser payload reader decoded a codec document with plain msgpack. cultcache-ts now
exports decodeDocumentPayload (definition or plain), the RUDP server reads a put through the registry's definition,
and decodeCultNetPayload takes the formatter. cultmesh-ts and cultmesh-browser no longer depend on @msgpack/msgpack:
frames and commands use cultnet-ts/wire, and a test per package scans src and the manifest. The envelope probe fixture
now runs the plain record first and last. Not yet built or tested on this commit; the verify job follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:25:29 +02:00
Meta Vens
4328ddb40e cultcache-py: a scalar payload of any type has no depth (integer and nil roots pinned)
Every commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:24:22 +02:00
Meta Vens
99b0ff4e1a cultcache-py: depth walk starts only from a container root; refusals of a stack-exhausting codec and the validator's arguments pinned
Removes the equivalent continue in the walk (the stack only ever holds containers).
Pins the exact refusal text on the RecursionError and StackError paths, both sides,
and that the validator receives the written and the decoded value. Every commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:20:35 +02:00
Meta Vens
94f61a14f6 cultcache-rs tests: a depth refusal through the cache names the entry type, not the C# type name
Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:15:16 +02:00
Meta Vens
4482e297cf cultcache-rs tests: through_cache unwraps the stored envelope (fixes the previous commit's type error)
The previous commit does not build its union_parity test target; this one does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:14:38 +02:00
Meta Vens
066ee4e177 cultcache-rs tests: the C# bounds vectors, the write refusal, the 2 MiB stack budget and the walk
Wrap, Floor and Ceiling fixture arms; bounds-outcomes.msgpack looped through the raw decoder
for arm-key and nil-slot classes and through the cache for depth; put of the 257 chain refused
with the cache and store unchanged; 2 MiB debug thread decode/encode at 256 and refusal at 257
and 100000; iterative walk on 256 KiB; header-width and scalar-width seams; derive names D, S,
A, E, ArmKey compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:13:50 +02:00
Meta Vens
ca6671aefb cultcache-rs: payload nesting bound through one decode, and the derive findings deferred to this cut
check_payload_depth walks MessagePack headers iteratively and refuses the first container past
MAX_PAYLOAD_DEPTH (256); decode_payload is the only payload decode (get, get_all,
get_all_with_keys, finish_prepared_entry, envelope validation), so write is refused through
finish_prepared_entry. Derive: nil arm slots refused naming union and key, ext arm keys classed
non-integer, generated generics and local items prefixed (types named D, S, A, E, ArmKey now
derive), slot keys capped at 65535 at definition, SlotRecord requires a hidden method only the
derives emit, DatabaseEntry/CultSlots expansions are functions the type/schema/generics refusals
are unit-tested through. Tests follow in the next commit; this commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:13:50 +02:00
Meta Vens
5d4753e2f0 cultcache-py: slot keys capped at 65535 at definition; a short record reads with nil as the reference does; bool arm key pinned
DatabaseEntryField validates its key in one place (0..MAX_SLOT_KEY, no bool), so
database_entry_field, direct construction and field tuples share the rule. A record
shorter than its declared slots no longer refuses a missing slot: the slot is nil
and its default applies, as the C# reference reads it. define_union refuses True
and False as arm keys, each pinned alone. Every commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:12:24 +02:00
Meta Vens
44da432e96 cultcache-py: one 256 nesting bound on read and write in DocumentDefinition, and the arm-key and nil-slot refusals, held to the C# bounds vectors
Iterative container count over the decoded and the encoded tree (MAX_PAYLOAD_DEPTH,
CultCachePayloadDepthError); msgpack StackError and RecursionError map to the same
refusal. Union decode names an out-of-Int32, non-integer or nil-slots arm as the
reference does. Fixture arms Wrap, Floor, Ceiling; loop over bounds-outcomes.msgpack.
Pins the bool arm-key refusal's callers in the vector loop only; every commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:12:17 +02:00
Meta Vens
4ea296ae10 Merge hands/union-derive-rs: cultcache-rs CultUnion derive held to the C# golden
Soul holds at cultlib-gaps:verdict:cut-union-derive-rs.s2; depth and remaining Lows move to union-bounds-rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:06:32 +02:00
Meta Vens
ead5260c07 Merge hands/union-bounds-cs: one 256 nesting bound on read and write in the C# reference, and the arm-key, depth and nil-slot boundary vectors
Soul holds at cultlib-gaps:verdict:cut-union-bounds-cs.s1; limit per cultlib-gaps:ruling:payload-depth-limit-256.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:06:32 +02:00
Meta Vens
8fa2d11042 codec-documents test: the alias found by parsing skips a plain record, and the snapshot probe picks the codec record among two
Builds; run at the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:54:10 +02:00
Meta Vens
8d5c3c0923 union-parity test: only a quiet-bit-clear NaN is signalling; a finite float32 with bit 22 clear still reads
Builds; kills the Stryker survivor at wire.ts:111 (the exponent test replaced by true).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:47:01 +02:00
Meta Vens
cf9d2fd2b5 codec-documents test: the alias record is found by parsing, and a peer snapshot handle reads a codec document
Builds; TS suites run at the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:38:56 +02:00
Meta Vens
5d2a880ffc numeric-leaves vectors: the 2^24+1 reads row and float32-nan.msgpack, written by the C# reference
Regenerated with CULTCACHE_WRITE_VECTORS=1 on Yggdrasil (dotnet 10 image, 16 vector tests green). Completes the previous commit; both build, and the TS suites are checked once at the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:31:45 +02:00
Meta Vens
1e0c5a9209 batch 4 code: mesh readers decode through the definition's formatter, float32 sNaN refused, slot keys capped at 65535
cultmesh-ts reads document payloads through resolveDocumentFormatter at its five plain-msgpack sites; WireFloat carries a signalling flag and the float32 leaf refuses it; slots() refuses a key above 65535 at definition. The C# vector writer gains the 2^24+1 reads row and float32-nan.msgpack, whose committed files land in the next commit: this commit does not build its tests (union-parity numeric vectors, row count 160 and float32-nan.msgpack are absent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:18:18 +02:00
Meta Vens
58712e3992 Payload depth tests: the largest fixmap and a container header cut short at the limit
Stryker survivors of the first scoped run: the fixmap boundary code and the truncated
header return. All commits build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:58:50 +02:00
Meta Vens
26ff9d27e9 numeric-leaves vectors gain two float32 rows; float32 integer rounding drops its redundant guard
An odd kept value just past exact pins the tie rule (the Stryker survivor). The excess <= 0 guard was equivalent to the
general path for the only values that reach it, so it is deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:45:48 +02:00
Meta Vens
6640b9d358 NumericLeafVectorTests: an odd float32 value just past exact, the case a wrong tie rule rounds up
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:45:04 +02:00
Meta Vens
0789ec8327 Payload depth walk: tests of every header and scalar width at the rule's own seam
Each case is a two-slot document whose chain puts the payload at the limit (accepted)
or one past (refused), so a scalar or container header read at the wrong width flips
one of the two. Also pins the message, truncated payloads and the first-value rule.
All commits build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:45:00 +02:00
Meta Vens
44dbf41362 union-parity test: the unknown-arm payload is one array, not two values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:39:42 +02:00
Meta Vens
9c1b06cd70 Merge hands/cultmath-release-0.4.1: CultMath 0.4.1 2026-10-11 08:37:23 +02:00
Meta Vens
fa1c006bee cultcache-ts and cultnet-ts: replication writes a codec document through the codec; numeric oracle read from the C# vectors; float32 rounds once; arm and slot keys span the reference's Int32
- cultnet-ts replication calls cultcache-ts's resolveDocumentFormatter (now exported) and loses its private
  @msgpack/msgpack default, so a raw put and a snapshot of a codec document carry the codec's bytes.
- the hand-written numeric table is deleted; the tests read tests/vectors/numeric-leaves.
- an integer past 2^53 in a float32 slot rounds once (ties to even), not through a double.
- a union arm key is any Int32 (negative too, sorted numerically); a slot key is 0 to Int32 max.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:34:31 +02:00
Meta Vens
35386acc51 numeric-leaves vectors: reads, writes and a mixed record, written by the C# reference
Regenerated twice on Yggdrasil under CULTCACHE_WRITE_VECTORS=1 and byte-identical. The cultcache-ts test still
imports the deleted hand table until the next commit, so this commit and its parent do not build the TS tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:34:30 +02:00
Meta Vens
abf624ab04 C# reference: one payload nesting bound (256) on read and write, nil arm slots refused, bounds vectors
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>
2026-10-11 08:33:15 +02:00
Meta Vens
3eb4cb641f NumericLeafVectorTests: a mixed-record vector, the shape a replicated document has
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:32:18 +02:00
Meta Vens
42f746de20 NumericLeafVectorTests: the coverage floor matches the cases (int32 accepts 8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:30:48 +02:00
Meta Vens
c38974f3c6 C# numeric-leaf vector writer: reads and writes of int, long, float, double slots, gated by CULTCACHE_WRITE_VECTORS
The vector files follow in the next commit; the new tests fail until they are committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:30:04 +02:00
Meta Vens
624fff4f89 Merge hands/union-codec-py: cultcache-py union codec held to the C# golden
Soul holds at cultlib-gaps:verdict:cut-union-codec-py.s2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:24:32 +02:00
Meta Vens
8dd9bd5300 Research: payload nesting-depth limits across serialization libraries
Prior art for cultlib-gaps❓payload-depth-limit: defaults, what each
library counts, read/write enforcement, and the stack-exhaustion advisories
behind them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:12:56 +02:00
Meta Vens
0882eee9d2 cultcache-ts wire: delete the dead guards the mutation run found; pin the width and Int32 boundaries
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:04:23 +02:00
Meta Vens
0f40e42904 cultcache-ts README: leaves, type names, arm keys
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:56:29 +02:00
Meta Vens
36aa1a44bf cultcache-py: a directly constructed DatabaseEntryField requires its default; test corrected
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:55:55 +02:00
Meta Vens
17df29cc9c cultcache-ts tests: numeric leaves held to the C# reference's reads and writes
The table is the reference's own answer for 120 one-slot reads and 46 writes, from a job-only probe of src/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:55:22 +02:00
Meta Vens
7cbe8049ff cultcache-py: pin the defaults of a directly constructed DatabaseEntryField
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:54:36 +02:00
Meta Vens
b4f41385e3 cultcache-ts tests: arm keys, wire, catalog parity, one codec-or-formatter owner (not yet run; does not build until the numeric table lands)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:51:51 +02:00
Meta Vens
485b570cf5 cultcache-ts codec: reference-typed numeric leaves, a wire reader and writer, one owner for codec-vs-formatter (work in progress, tests not yet run)
The codec path owns a small MessagePack reader and writer so it can tell an integer from a float and keep 64-bit
integers whole. Leaves int32, int64, float32, float64 replace number; every codec carries the reference's type name and
a slot record derives its catalog members from them. Not built or run at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:49:13 +02:00
Meta Vens
ae3af9feb8 CultMath 0.4.1: Unity plugin DLL and pdb rebuilt on Windows at 0.4.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:48:31 +02:00
Meta Vens
eae7207df5 cultcache-py: the record decode applies a slot default in one place; plain field helper, SlotCodec and field immutability pinned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:47:52 +02:00
Meta Vens
ff1138ca4c cultcache-py: union arm keys bounded to Int32 at definition; Missing slot typed Node; nil list elements, unit-arm extra slots and UnionArm frozen-ness pinned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:40:33 +02:00
Meta Vens
fb51df9d75 Union tests: the undeclared negative key assertion matches the message wording
Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:39:34 +02:00
Meta Vens
c586640e81 cultcache-rs-derive: arm keys are Int32 and given once, an arm is a slots record, refusals classified like the reference
Key attributes (variant, field, entry type/schema) are refused when repeated. A CultUnion key
parses as the reference's Int32, negatives included; a key outside it is a compile error. Decode
classifies the key as the reference does: non-integer, outside Int32, undeclared (named), and a
pair of the wrong length is refused before its key is read; no value is echoed beyond the
undeclared key the reference names. The SlotRecord trait, implemented by the shared slot
generator, bounds an arm payload, so a String or Option arm is a compile error. compile_fail
fixtures use keys other than 0; the derive crate's unit tests assert each refusal's message.

Unverified: committed before the first build; the verification job runs next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:37:47 +02:00
Meta Vens
61c2d76de6 CultMath 0.4.1: version, changelog (BoundedLeastSquares stopping fix, Validate), cultlib dependency pin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:31:21 +02:00
Meta Vens
a4466fb068 Merge hands/cultmath-bls-input-field: BoundedLeastSquares.Validate names the invalid input; the stopping rule's decisions are traced and pinned
Soul holds at cultlib-gaps:verdict:cut-bls-input-field.s3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:29:02 +02:00
Meta Vens
5aca6a8902 cultcache-ts README: slot records and unions
Docs only; the code and tests are unchanged from the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:13:56 +02:00
Meta Vens
27e3f0f721 cultcache-py: pin the union and record refusal messages and the record defaults, member catalog
Tests only; the code is unchanged. All commits build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:11:36 +02:00
Meta Vens
bea5564b3a union-parity test: the one list-element probe is cast so the test project compiles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:09:57 +02:00
Meta Vens
9073d582e7 cultcache-ts codec: Number.isFinite alone decides a number
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:06:43 +02:00
Meta Vens
e72f17421e CultSlots compile-fail docs: a field without a key, a generic struct
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:05:22 +02:00
Meta Vens
344630259a cultcache-ts codec: tests for refusal messages and leaf shapes; drop a redundant key sort and regex quantifier
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:03:04 +02:00
Meta Vens
21dec1ef56 union-parity test: find the vectors by walking up, so the test runs from source as well as from dist-test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:54:30 +02:00
Meta Vens
d73249365a Union tests: valid arm shapes in the malformed-pair fixtures, the store test pulls its backing store
Not yet run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:54:09 +02:00
Meta Vens
2e5fc0f7f8 Union tests: the composition body is a boxed Node, the recursive type has finite size
Not yet run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:52:39 +02:00
Meta Vens
82aab7d83e union-parity test: the canary check counts the payloads that carry it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:51:37 +02:00
Meta Vens
f43a0d26f9 cultcache-ts codec: unit arm is a frozen object, recursive test codec annotated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:51:11 +02:00
Meta Vens
2d840d5ffb cultcache-rs: CultSlots and CultUnion derives writing the reference [key, armSlots] shape
One slot generator now serves DatabaseEntry and CultSlots. CultUnion writes
[key, slots] with unit arms as [key, []] and refuses unknown keys naming the
enum and declared keys. Tests hold the runtime to tests/vectors/union-parity.

Not yet built or run: this commit may not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:50:30 +02:00
Meta Vens
499887484c cultcache-ts: slot record and union codecs, held to the union-parity golden
Unverified first pass; not yet compiled or run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:50:23 +02:00
Meta Vens
91c8352ec5 cultcache-py: union codec and nested record codec, held to the C# union-parity golden
define_union and define_record_codec share the slot logic of define_database_entry_type
(one record codec); a field takes a codec, many, or a thunk for recursion. Unknown arm
keys are refused naming union and key, never the payload. All commits build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:49:47 +02:00
Meta Vens
b0f4057704 Merge hands/union-golden: the recursive-union and arm-refusal goldens, written reproducibly by the C# reference
Soul holds at cultlib-gaps:verdict:cut-union-golden.s2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:45:15 +02:00
Meta Vens
b941b68085 CultMath tests: pin the FreeViolated record of the real solves, column by column
The 30 trace cases' FreeViolated lines (column, gradient magnitude, threshold) are pinned by exact equality beside the base-derived decisions they feed, so a free-set test that is mis-called from SolveTraced (a shifted threshold, a short free count) fails here and not only in the hand-built FreeSetViolated tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:04:25 +02:00
Meta Vens
e63f93e4a5 Union golden: union-surface.cc regenerated with the pinned store clock
Written by the C# reference with CULTCACHE_WRITE_VECTORS=1; two regenerations on Yggdrasil were byte-identical.
The 12 payload vectors and outcomes.msgpack are unchanged. Builds and passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:59:51 +02:00
Meta Vens
b4edbb1eeb Union golden: the store's clock is an internal seam, the writer pins it, the gate test runs under any switch
CultCache mints storedAt from an internal Clock (default the wall clock); the vector writer pins it, so
union-surface.cc is regenerated in process and byte-compared on every run. The gate test sets the switch
itself and writes only a probe, so it neither skips nor touches a committed vector.

This commit does not pass: union-surface.cc still holds the old wall-clock stored-at; the next commit
regenerates it. Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:58:54 +02:00
Meta Vens
4c1a53f493 CultMath tests: the new trace tests' newline escapes were written as raw newlines; restore them
The previous commit's BoundedLeastSquaresTraceTests.cs did not compile (raw newlines inside string and char literals). The source file compiled; the CultMath.Tests project did not. This commit builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:42:25 +02:00
Meta Vens
f8253837ed CultMath BoundedLeastSquares: the free-set stationarity test is a traced decision; the stall boundary is pinned to the ulp
FreeSetViolated is the one owner of the free-set test and records each violating column with its gradient magnitude and the threshold it was judged against (SolveEvent.FreeViolated). Tests pin the column and the threshold by exact equality, one ulp each side of the tolerance at four scales, and the stall comparison f >= mark - allowance at mark == allowance and one ulp each way.

Builds are unverified until the batch's single verify job; this commit has not been compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:37:05 +02:00
Meta Vens
e5cfb10674 Union parity vectors: committed bytes written by the C# reference, outcomes as data, README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:28:20 +02:00
Meta Vens
3c75cae4f0 Union parity vectors: the recursive-union fixture, refusal and extra-slot cases, written only under CULTCACHE_WRITE_VECTORS=1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:26:15 +02:00
Meta Vens
8d3bf52ed5 Merge hands/union-refusal-cs: the C# reference refuses unknown union arms and unnamed subtypes
Held by cultlib-gaps:verdict:cut-union-refusal-cs.s2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:21:15 +02:00
Meta Vens
a0e0cceaaa Union guard tests: pin the Int32 bounds and full-width encodings of an arm key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:07:37 +02:00
Meta Vens
04c5f870d7 CultMath tests: a trace case for the cap reached right after a release
Stryker found the post-pass cap record unobserved: the only capped case ended on the step site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:07:36 +02:00
Meta Vens
3042757701 Union guard: name an out-of-range or non-integer arm key, pin interface unions, name the schema on a variant refusal, changelog the exception type
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:48:49 +02:00
Meta Vens
506ec8c540 Union guard: the store refusal is readable from its outermost message; a consumer formatter for the union is wrapped, not replaced
Manual mutants survived: the record key and schema id were only asserted through the
cause chain, and an inner built from StandardResolver alone passed every test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:56:53 +02:00
Meta Vens
62a0e9ac98 Union guard tests: pin the composite's resolver order and that a union owns its keys
Stryker survivors on the Compose line and on the inherit argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:41:04 +02:00
Meta Vens
08f05dede8 CultMath BoundedLeastSquares: the stopping rule's decisions are traced and pinned; Validate is the one owner of n < 0
The release choice and the pass judgement are internal functions (SelectRelease, JudgePass) and Solve reports each
decision, in order and with the values it compared, to an optional internal ISolveTrace. The tests assert those decisions
with exact equality against the traces the base commit dbcb2a8e produced on the same problems, and call the two functions
directly on states a solve may never reach. Solve's own n < 0 check is deleted (Validate throws the same exception), and the
NonFiniteTarget member doc names the n = 0 case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:24:02 +02:00
Meta Vens
28d79b190a Union guard tests: forge a directory-store page the way its manifest commits to it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:50:53 +02:00
Meta Vens
286e8a71e9 cultlib-gaps: slot read-only probe (any depth), section S of the union parity map
Probe facts for ruling cultlib-gaps:ruling:extra-trailing-slots: the C# skip site and why a
resolver guard sees every nested object and arm, the mark arriving at depth 0, 1, 2, in an arm
and in a list element in C#, Rust, Python and TS, the cost on Aetheria.cc against an A/A
control, a live nested case in Aetheria (WeaponData.Tracking), the C# whole-view flush that
re-encodes every held record, and Kotlin's missing library slot codec. Cut order step 4 and
the model page follow the ruling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:41:45 +02:00
Meta Vens
29b21a0f33 Union guard tests: the store refusal names the record's schema id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:35:37 +02:00
Meta Vens
ee227bb782 Contract and changelog: unions refuse an unknown arm and an unnamed subtype
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:34:57 +02:00
Meta Vens
afaae23c8a Union guard tests: helper names no longer collide with the arm classes
6254da0c does not build the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:34:56 +02:00
Meta Vens
6254da0c88 CultCache: refuse an unknown union arm on read and an unnamed subtype on write
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>
2026-10-10 21:17:56 +02:00
Meta Vens
40a30e714b cultlib-gaps: union parity map (body facts, step 0b, cut order)
Imagination pass imagination-cl-union-parity: MessagePack-CSharp's union
miss branch read and run, a CultCache-layer arm guard proved without a fork,
Aetheria's and StreamPixels' stores walked for arms and slots, and the Rust,
TS and Python slot and union behaviour measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:04:23 +02:00
Meta Vens
aaf0d7f6a7 Research: CultCache union decode tolerance prior art (Eyes, 2026-10-10)
Serves thing2❓contract-decode-tolerance and
thing2:follow_up:cultcache-union-parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 20:39:51 +02:00
Meta Vens
a42d7684fb CultMath tests: the rescaled noise-gate problems converge in a few passes, not four
A rescaled problem takes five passes on its own merits; the assertion is that the solve does not spin to the cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:59:50 +02:00
Meta Vens
300fe188d1 CultMath tests: pin the BLS release stop (bls-stop-pins)
W5 with its columns rotated so every column is the released one; the noise gate across column scales; two small problems
whose release is confirmed by a second pass. Untested at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:47:57 +02:00
Meta Vens
004d8bb78a CultMath BoundedLeastSquares: Validate names the invalid input; Solve's validation has one owner
Adds BoundedLeastSquaresInput and BoundedLeastSquares.Validate (tolerance, bounds, matrix, target; first failure).
Solve calls it and its inline tolerance, bounds and A^T b finiteness checks are deleted. Untested at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:24:59 +02:00
Meta Vens
dbcb2a8ea8 CultMath BoundedLeastSquares: release on the gradient against its rounding, never by the cost
A bound is released only when its violation exceeds both kktTol and the rounding
bound of its double gradient (Higham gamma_(n+1)); the cost-progress rule now judges
only passes that release nothing, and a release resets fMark. Fixes a cold start
stopping Converged with a violated bound (Soul W5, finding
aetheria-release:finding:cut-thrust-allocator-core.s1.solver-converged-early).
Tests: the W5 problem (cold == re-solve at 1e-12 and 1e-9) and the seed-141 noise
case. Builds; verified on Yggdrasil in the following report.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 08:51:18 +02:00
Meta Vens
50512e6e4f cultlib-gaps map: BLS early-stop diagnosis and CultMath rotation algebra
Imagination pass (session self-2026-10-10-ag): the cost-progress stop at
BoundedLeastSquares.cs:275 reports Converged with a bound still violated;
fix follows Lawson-Hanson. Quaternion algebra surface and the
mul(float4,float4) overload trap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:12:09 +02:00
Meta Vens
6bdc23c9fd Research: prior art on inverse procedural modelling from messy meshes
Eyes pass, 2026-10-10: inverse CSG and shape-program synthesis, primitive
fitting front ends, differentiable SDF/CSG fitting, measured LLM spatial
reasoning with a target, hard-surface detail, and the VibeGeometry record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 02:00:52 +02:00
Meta Vens
252e226c51 Merge hands/cultmath-release-0.4.0: org.gamecult.cultmath 0.4.0
Cut cultmath-release (campaign cultlib-gaps). Soul passes 1-4; the last docs edit is the one pass 4 prescribed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:13:10 +02:00
Meta Vens
2855963ca1 CultMath docs: state the float32 tie band in ulps, as measured (Soul s4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:12:23 +02:00
Meta Vens
299a4c5018 CultMath docs: magnitude-scaled float32 band; the frustum balls read no noise constant
Docs only; builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:04:07 +02:00
Meta Vens
104838ce10 CultMath tests: SnoiseTieScopeTests comment matches its set and the float32 band
Comment only; builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:04:07 +02:00
Meta Vens
5384d476ce CultMath smoke: fail unless Unity exited 0 and this run wrote the passing results
Stale logs are deleted before launch; the results file must be newer than the launch. All commits build (no source change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:04:06 +02:00
Meta Vens
b88fdccb75 CultMath: commit the Unity import and dispatch smoke, with its broken control
packages/cultmath/scripts/unity-smoke/ holds the good compute shader, the
deliberately broken control, the EditMode test (FindKernel, dispatch, readback),
and run-unity-smoke.ps1, which builds a scratch project outside the repo with a
file: path to the package, takes the Unity path as a parameter, requires all
three tests to pass, and deletes the project. The package README's Unity section
names it as the pre-tag step. Not -nographics: GetComputeShaderMessageCount
reports 0 for a broken shader until FindKernel or Dispatch compiles it. Ran once
on Starfire with Unity 6000.3.24f1: 3 passed, control rejected. Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:41:04 +02:00
Meta Vens
ab0eacd1c6 CultMath tests: SnoiseTieScopeTests pins each tie-rule site on its own
Asserts per output (value, and each snoise_grad component) that it differs
from 0.3.0 on the tie set, and pins the 0.4.0 values as goldens beside the
0.3.0 ones, so reverting either math.cs:614 or :720 alone fails the test. The
comment says inputs within a few float32 ulps of the set can tie too. Not yet
run when committed; the next commit-free verification run follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:37:24 +02:00
Meta Vens
d6a31416fd CultMath 0.4.0: name all three empirical constants; float32 ulp neighbours of the snoise tie set
CHANGELOG and package README: the noise enclosures rest on SNOISE_LIPSCHITZ,
SNOISE2_LIPSCHITZ and SNOISE_HESSIAN, all empirical with a margin; the README
bullet no longer says "with a proof" or "every point". Breaking: inputs within a
few float32 ulps of the tie set can also change (probe: none at 1e-5 or more,
a handful at 1e-6 near magnitude 30). Text only; builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:37:24 +02:00
Meta Vens
f9118e1a66 CultMath 0.4.0: the noise-ball enclosures are empirical, not proven
CHANGELOG and README no longer claim iv_snoise_ball and iv_fbm_ball enclose
every input unconditionally: they rest on SNOISE_LIPSCHITZ, an empirical bound
with a margin, and the tests sample. Docs only; builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:07:48 +02:00
Meta Vens
7bd34a96ce CultMath 0.4.0: state the true scope of the snoise tie change
The Breaking entry now names the whole tie set (inputs whose y-x and z-x are
whole numbers, every integer point included, change by up to 0.776), with
examples and a consumer warning; design.md and the NoiseGradTests comment drop
the narrower origin-and-(t,t,t) wording. Docs and test comment only; builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:07:48 +02:00
Meta Vens
96ffb97bbe CultMath tests: pin the snoise tie-rule scope against the 0.3.0 DLL's values
Inputs whose coordinate differences y-x and z-x are whole numbers move from the
0.3.0 value; inputs off that set keep its bits. The goldens were computed by
the cultmath-unity-v0.3.0 tag's tracked CultMath.dll. Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:07:42 +02:00
Meta Vens
476a5fa491 CultMath: tracked DLL and pdb rebuilt at 0.4.0 on Windows
Copied from artifacts/cultmath-unity-build of the release script's first run
(netstandard2.1, non-incremental, Source Link off, ContinuousIntegrationBuild).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:07:05 +02:00
Meta Vens
84c4bb14e3 CultMath 0.4.0: version, changelog, NuGet packs CultMath.Affine.hlsl
Bumps CultMath.csproj, the Unity package.json, the org.gamecult.cultlib
dependency and the semver-policy paragraph to 0.4.0. CHANGELOG states the snoise
tie rule under Breaking and the additions since 0.3.0. The tracked DLL and pdb
still read 0.3.0 at this commit, so the release script's parity check fails here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:03:43 +02:00
Meta Vens
e1b2f513f9 Merge hands/cultmath-bls-kkt-tolerance: BoundedLeastSquares takes a relative KKT tolerance
Cut bls-kkt-tolerance (campaign cultlib-gaps). Soul pass 2 holds at 9df0a82f.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:02:04 +02:00
Meta Vens
9df0a82faf CultMath tests: pin the KKT tolerance threshold, both decision sites and the forwarded default
The release flips at 1.05x and 0.95x of the computed relative gradient at four
weights either side of 1e-6; all-free conditioned solves change between the
default and 1e-12; the omitted tolerance releases at 1.1e-6 and not at 0.9e-6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:12:08 +02:00
Meta Vens
885b90c355 CultMath: BoundedLeastSquares crefs no longer name the overloaded Solve
Both crefs were ambiguous (CS0419) once Solve had two overloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:12:08 +02:00
Meta Vens
3dce41bd57 CultMath tests: the KKT tolerance argument of BoundedLeastSquares
Default and explicit-default are bit-identical; a light term under a heavy row
and an allocator-shaped problem are start-dependent at the default and solved
tight at 1e-12; invalid tolerances leave x untouched; the noise-floor fixtures
stop on progress at 1e-12 and 1e-30 (at most 6 iterations); the tolerance path
allocates nothing; the original ten-parameter signature still exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:21:10 +02:00
Meta Vens
e0ca817e82 CultMath: BoundedLeastSquares.Solve takes an optional relative KKT tolerance
New overload with kktRelativeTolerance before maxIterations; the existing
signature forwards with the new public DefaultKktRelativeTolerance (1e-6), which
replaces the private constant. A tolerance that is not finite and positive is
InvalidInput with x untouched. The default path is bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:21:10 +02:00
Meta Vens
2ad30fbfcf CultMath tests: pin default BoundedLeastSquares solves to their golden bits
Records status, iteration count and every float bit of x for the A54/B5 fixture,
three seeded random problems and a Duel allocator problem, taken at f7ab6212.
The kktRelativeTolerance cut must keep these literals unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:13:33 +02:00
Meta Vens
f7ab62126e Target: cultlib-gaps, the standing home for capabilities consumers find missing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:40:01 +02:00
Meta Vens
05e6701451 Map headers: affine forms and the tie fix are built; the tape target's remaining steps stay parked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:00:00 +02:00
Meta Vens
d4e3b837f3 Merge modeling/affine-as-built: the as-built map of the snoise tie fix and affine forms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:59:59 +02:00
Meta Vens
4b34b12b21 Map: as-built section for the affine forms and snoise tie cuts
Owners, mirror chain, derived state, verification venue, release state,
stale docs found.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:59:30 +02:00
Meta Vens
14568bf9b0 Merge imagination/affine-forms-map: Passes 5, 5b and 6 of the interval ground map (affine forms, deep well, snoise tie)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:57:26 +02:00
Meta Vens
156782da0e Merge hands/cultmath-affine-forms: affine forms for noise bounds, in C#, HLSL and GLSL
Reduced affine arithmetic (centre, one shared symbol, one error term) for CultMath:
af_* ops, af_snoise / af_fbm with gradient-plus-curvature forms, af_frustum_*, the
2D iv_snoise_ball, and the centred warp bound. Mirrored bit for bit in
CultMath.Affine.hlsl (and the Unity copy), lowered into CultMath.glsl, with fixture
families that check enclosure on device, including small-reach and C#-centre legs.
Scenario (c) is a deep well an order of magnitude past the fog band; affine beats the
centred interval there (3.57x / 3.46x vs 3.20x / 3.12x), asserted, not skipped.
Soul held cut-affine-forms (s1-s3) and cut-affine-shaders (s1-s3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:57:11 +02:00
Meta Vens
81b7cf10e3 design.md: (c) and (c-band) rows and the closing paragraph back to the gate with its detail ball [affine-gate-loses-fog-culls]
Re-measured at c01ff025 with IntervalSkipHalvesEvaluations: (c) affine 3.57x and 3.46x, c-band 3.95x
and 3.54x, efficiencies and range-ceiling ratios as printed; the (b) rows and the DeepWellCullMap
figures (4.23, 3.60, 2.02) are unchanged. The sentence that justified deleting the affine detail
ball is gone with the deletion.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:23:02 +02:00
Meta Vens
c01ff025bb Gate test: restore the affine gate's detail ball and add FogField.InsideFog to the field list [affine-gate-loses-fog-culls] [interval-gate-detail-unpinned]
Deleting the affine gate's detail ball (51a4034c) gave up 6 of 5933 provable culls inside the fog
(first: (b) inside the fog [affine]); the gate never gives up a provable cull, and a 1.4% probe
saving does not buy breaking that. The detail ball is back in the affine gate, and the gate test
now includes the fog interior, the only field where the interval gate's detail ball decides, so
saturating either gate's detail ball fails the test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 12:12:36 +02:00
Meta Vens
8dced0bf91 design.md: the deep-well cull-map run re-measured (DeepWellCullMap, explicit)
Cells per ray 4.23 interval, 3.60 affine, 2.02 oracle, the most 44, mean transmittance 0.278,
measured at the affine gate fix; the interval figure (4.51, 45) was already stale: the interval
path is unchanged by that fix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:14:18 +02:00
Meta Vens
c28cb4488b GLSL fixture: small-reach af_snoise and af_fbm families, and the centre leg of the device check [device-check-blind-to-allowance] [device-check-self-referenced]
Two families are appended (256 cases each, reach |axis| + radius of 1e-6 to 1e-3, the centre in
[-50, 50] or [-1, 1]); the earlier families are byte-identical. In the main families the
second-order remainder is at least 0.057 of e even without the allowance; here the float32
allowance is most of e, so a device whose gradient error exceeds it fails the enclosure check.
Their check text adds the centre leg for every af_snoise and af_fbm family: the fixture's own
x0 (C#'s value at the centre) must lie in the device form's [x0 - e, x0 + e], so a device whose
noise is wrong everywhere no longer passes against its own reference.

AssertSampledForm counts the cases that need the allowance (32 of 256 af_snoise, 3 of 256
af_fbm on C#'s own results) and the small families fail unless there is at least one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:09:35 +02:00
Meta Vens
b6d50be653 Mirror sweep reaches af_snoise's centred branch: realistic slices, both sides of the choice [mirror-sweep-misses-centred-branch]
EveryMirrorFunctionMatchesCSharpMath draws centre, axis and radius from [-100, 100], far past the
0.15 or so below which the centred form wins, so an HLSL edit of the choice survived it.
AffineSnoiseMirrorMatchesCSharpBitForBitOnBothSidesOfItsBranch draws 20000 slices (axis and
radius log-uniform 1e-3 to 0.5), asserts both branches are met more than 2000 times each, and
compares af_snoise (and af_fbm every eighth) bit for bit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:09:34 +02:00
Meta Vens
799029bebe af_range finds a NaN endpoint by its bit pattern, which dxc's fast-math compare cannot fold [af-range-nan-guard-folded-by-dxc]
dxc marks compares fast, so `lo == lo ? lo : -inf` was deleted under default flags and an
overflowed form ranged to NaN on HLSL targets. The guard now tests (asuint(lo) & 0x7fffffff)
> 0x7f800000, in HLSL, the Unity copy, math.Affine.cs and the regenerated CultMath.glsl.
dxc 1.9.0.5402 -T ps_6_0 default flags: before, no fcmp/select/and in the af_range body;
after, and/icmp ugt/select to +-inf for both endpoints (also under -Gis).
AffineRangeMirrorMatchesCSharpBitForBitAtTheFloatExtremes pins the mirror at zero to the
float maximum, infinities and NaN components.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:09:25 +02:00
Meta Vens
51a4034c66 Affine gate: delete the detail term (saturated at [-0.5, 0.5]); design.md (c) rows re-measured [gate-detail-term-dead]
The affine gate's best-case detail ball decided no probe on the gate test's slices and one
of the deep well's 8074 oracle-empty cells (11 of 11096 in c-band); skipping it saves the
probes: (c) affine 3.57x -> 3.62x and 3.46x -> 3.54x, c-band 3.95x -> 3.96x and 3.54x -> 3.60x.

The interval gate keeps its detail ball. Saturating it there (measured, then reverted)
takes row (b), inside the fog, from a cull fraction of 0.663 to 0.585 and a cost ratio
of 1.16x to 0.98x, so the term is live; the finding's premise holds for the affine gate only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 11:08:50 +02:00
Meta Vens
2a1f9df19a GLSL fixture: the af_* families and iv_snoise_ball(vec2), with enclosure checks for af_snoise, af_fbm and af_frustum_ball
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:47:49 +02:00
Meta Vens
e16edc0b70 Affine forms in HLSL: CultMath.Affine.hlsl mirrors math.Affine.cs bit for bit; iv_snoise_ball(float2); the GLSL lowering carries them
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:47:49 +02:00
Meta Vens
962aa45d47 design.md: rows (b), (d), (e) and the closing paragraph re-measured on the snoise tie fix (one tree)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:33:28 +02:00
Meta Vens
03359a2365 NoiseBoundTests: the gate never gives up a cull the ungated bound proves (pins the loosening direction)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 09:24:27 +02:00
Meta Vens
118e85fd3a design.md: the slice returns the final af_snoise arguments; the composition test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:37:35 +02:00
Meta Vens
aad21e4b36 design.md: the (c) rows re-measured on the snoise tie fix (the centred interval 3.20x and 3.12x, affine unchanged)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:37:25 +02:00
Meta Vens
25e0aa3fe4 NoiseBoundTests: the slice's reach is |axis| + radius
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:34:22 +02:00
Meta Vens
bdafdf3dd1 NoiseBoundTests: the affine composition must win in 35% of open slices and narrow the interval piece
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:28:03 +02:00
Meta Vens
52bf488b51 NoiseBoundTests: the affine piece alone is wider in total; measure the intersection's gain
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:26:34 +02:00
Meta Vens
922e4da458 NoiseBoundTests: FogField.Slice returns the final af_snoise arguments; AffineBound has no term left to wire
Slice builds each phase's coverage argument and the detail argument (centre,
axis, radius in noise space); AffineBound and its gate read them whole.
AffineWarpStaysEnclosed checks the rays against those arguments and their
derivation from CentredWarp; AffineCompositionIsTighterThanItsRanges sees the
shared-eps composition and the intersection.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:20:56 +02:00
Meta Vens
d8eaa110c9 AffineTests: af_range encloses on every overflowed form, the infinity on the NaN side
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:20:56 +02:00
Meta Vens
8195ac1286 iv_snoise_ball(float2): the doc states it is exact for its point and names the caller's float32 allowance
The 2D ball carries no evaluation allowance, like iv_snoise_ball(float3); the
caller that rounds the flow's point adds 2^-14 + L2 2^-20 (|centre|_1 + radius),
the convention af_snoise carries for the 3D point.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 07:20:55 +02:00
Meta Vens
05150acd2f Merge remote-tracking branch 'origin/main' into hands/cultmath-affine-forms 2026-10-04 07:17:20 +02:00
Meta Vens
48aadda0b9 Merge hands/cultmath-snoise-tie: snoise breaks simplex ties in a total order
Upstream Ashima's step compares are cyclic, so where x0's components compare equal
(the origin, every (t,t,t), all lattice diagonals) the corners stop being a simplex
and snoise jumps (about 0.77 near the origin, up to 1.56 further out). The z-x compare
is now strict, giving Gustavson's simplexnoise1234 order in C#, both HLSL copies and
the generated GLSL. Off the z=x tie set values are bit-identical. SNOISE_LIPSCHITZ
re-pinned 10.099261 -> 7.9640074. A value change: next cultmath-unity release is
Breaking (follow-up snoise-tie-release). Soul s1 held (cut-snoise-tie.h2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:26:19 +02:00
Meta Vens
e1daefb5b6 snoise tie docs: the old rule's jump reaches 1.56 away from the origin, not 0.77
Soul s1 (cut-snoise-tie) measured 1.55 to 1.56 at magnitudes 64 and above;
0.77 holds only near the origin. Comment and design text only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:25:58 +02:00
Meta Vens
3a3051b9a2 AffineTests: af_frustum_axis is (m_c, 1) h exactly and af_frustum_ball's widening is pinned
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 04:15:53 +02:00
Meta Vens
d238bf675d Document the snoise tie order in design.md and THIRD-PARTY-NOTICES
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:26:10 +02:00
Meta Vens
e7600a79c5 GLSL fixture: tie families, regenerated CultMath.glsl and glsl-parity.json
Two families appended after phacelle, cultmath_snoise(vec3) and
cultmath_snoise_grad(vec3) over TiePoint3 (seven of eight cases on lattice
diagonals, one of eight on (t, t, t)); the shared Random leaves every earlier
family's draws unchanged. Both carry the tolerance text "ulp-bounded, on the
simplex tie set" and share a glsl name with the random families, so a consumer
tells them apart by index or tolerance.

Regenerated on Yggdrasil (Linux, the fixture's platform) with
CULTMATH_WRITE_GLSL=1. CultMath.glsl: the two tie lines, the constant, one
comment sentence. Fixture: 41 cases of iv_snoise_ball(vec3) and iv_fbm_ball move
with L, everything else is byte-identical before the appended families. Full
suite on Yggdrasil: 290 of 290.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:25:43 +02:00
Meta Vens
35385268c5 design.md: the affine rounding paragraph names the underflow, overflow and pinning tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:19:06 +02:00
Meta Vens
00c97639dc NoiseBoundTests: FlowVariation's float32 allowance pinned by its rule
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:18:44 +02:00
Meta Vens
27109f1b2a af_range: the absorption is formed term by term so |x0| + e never overflows before it is scaled; AffineTests pin af_from_iv tight near the float maximum
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:18:44 +02:00
Meta Vens
30448bb45e Re-pin SNOISE_LIPSCHITZ to 7.9640074 on the tie-fixed kernel
MeasureLipschitz on e89fd687 (Yggdrasil): sampled 7.1933103, refined 7.2400064,
x 1.10 = 7.9640074, witness (229.92526, 73.37927, -66.14595). The old 9.181147
sat on the diagonal tie. C#, both Interval.hlsl copies, the pin test's witness
and design.md's provenance numbers move together; CultMath.glsl follows with the
regenerated fixture.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:16:31 +02:00
Meta Vens
e89fd68778 snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.

NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:15:32 +02:00
Meta Vens
e201f9054a Affine forms map: Pass 6, the snoise diagonal tie (cut snoise-tie r1, affine-shaders r2)
The cyclic step in Ashima's 3D simplex degenerates on the lattice diagonals,
including the origin and every (t,t,t). The fix is one strict compare, giving
simplexnoise1234's total tie order. Probes show it is bit-identical off the
z = x tie set and continuous on it. SNOISE_LIPSCHITZ re-measures to 7.9640074,
and SNOISE_HESSIAN does not move. Merge order: snoise-tie first, then the
affine branch merges main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 03:04:52 +02:00
Meta Vens
3958dca2cd Centred warp: FogField.Slice owns the affine warp terms and AffineWarpStaysEnclosed tests them; FlowVariation carries a float32 allowance; CentredWarpStaysEnclosed down to r = 1e-3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 02:55:49 +02:00
Meta Vens
e6606faff8 af_snoise: width and allowances pinned by an exact-rule test; curvature witness test; SNOISE_HESSIAN's defence stated truthfully
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 02:55:32 +02:00
Meta Vens
a5bfa63965 af_from_iv: the 2^-126 underflow term and halves before sum; af_range: never NaN; tiny, extreme and absorption tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 02:48:39 +02:00
Meta Vens
0893679235 Deep well (c): the affine-cheaper-than-interval contract fails the test; only the (a) and (e) savings skip
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 02:39:22 +02:00
Meta Vens
4ae986a52a Cull map: cells per ray on a logarithmic viridis scale
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 22:21:50 +02:00
Meta Vens
1b9cecb941 design.md: Affine forms section; (b) and (c) rows rewritten from the deep-well run, (c-band) added, contracts follow the rulings
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 22:21:50 +02:00
Meta Vens
4137285e58 Deep well (c), centred warp, affine FogField bound, range ceiling; SNOISE_HESSIAN and SNOISE2_LIPSCHITZ measured and pinned; cull map
(c) is FogField.DeepWell (ruling wells-scale-order-of-magnitude), measured under
the centred-warp interval bound and the affine bound on the same seeds; the
shallow wells are the printed (c-band) row. The warp-enlarged ball and the
warp-carrying detail ball in FogField.Bound are gone. (c) asserts affine cheaper
than the centred interval at both warps, with the 0.5 cull floor under both
(ruling affine-wells-contract-cheaper-than-interval).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:04:21 +02:00
Meta Vens
ad7e3079d4 Affine forms: math.Affine.cs (af_* ops, af_snoise, af_fbm, af_frustum_*), SNOISE2_LIPSCHITZ and iv_snoise_ball(float2), AffineTests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:41:08 +02:00
Meta Vens
c6fc478a30 Affine forms map: Pass 5b names the re-asked contract question and spec r2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:28:37 +02:00
Meta Vens
a2af405600 Affine forms map: Pass 5b, the deep well (ruling wells-scale-order-of-magnitude)
(c)'s wells are 1.1-2.5x the 60-unit noise band deep; a 10x well (depth and
half-depth radius 600) measured on the scratch probe: interval already skips
most empty space, affine gains 1.14x/1.16x over the centred interval. (c)
becomes the deep well; the mass-derived wells stay as a printed row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:26:17 +02:00
Meta Vens
32ba1bfa53 Affine forms map: Pass 5 probe results, authority map, and the tape target's pointer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:06:41 +02:00
Meta Vens
12451d3ada Release CultLib Unity 1.0.61: enable the x86_64 native QUIC plugins for Win64
gamecult_mesh_quic_native.dll.meta and msquic.dll.meta enabled the plugins
for Standalone: Win (32-bit) only, so 64-bit Windows players shipped without
the native QUIC bridge. Both now target Standalone: Win64 and are disabled
for 32-bit Windows. Managed plugin DLLs rebuilt by build-unity-package.ps1
-UpdateTemplate to carry 1.0.61; the native bridge keeps its 1.0.60 bytes
(its source is unchanged; an MSVC rebuild is not byte-reproducible).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:15:21 +02:00
Meta Vens
a796614276 Merge hands/release-guard-tidy: the guard's threat model stated, both release lines pinned against preloads, dated changelog headings accepted
npm Publish and Python build lines run the guard under env -u NODE_OPTIONS;
the shell/defaults ban is dropped (deliberate edits are review's job);
check-changelog-semver reads Keep a Changelog's dated heading; semver-policy.md
states the threat model and the first-release versions. Verdict
cut-release-guard-tidy.s1 holds with no findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:47:25 +02:00
Meta Vens
de401056e0 Tests: a refusal leaves staged what it did not refuse, with the refused record now refused again at staging
ARefusalLeavesStagedWhatItDidNotRefuse re-staged the refused record and
expected the flush to refuse it; since b636a69f lists it foreign, that
re-staging is refused at once. The partial case now stages two records before
another writer makes one foreign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:18:21 +02:00
Meta Vens
114bb4f380 Research: integration-native volumetric noise domain probe (cultmath-tapes)
A domain built so span integration holds by construction: sums of
nonnegative compact ellipsoid kernels in a mass-conserving multiplicative
cascade, per-kernel affine warp, all nonlinearity moved into placement.
Measured on Yggdrasil against the shipped void and wells, with a physical
reference (Beer-Lambert, single scattering with HG).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:17:17 +02:00
Meta Vens
a126914efc Release guard tidy: the Python build guard starts without NODE_OPTIONS too
The python-build step runs the same guard whose output is the dist PyPI
uploads, so a NODE_OPTIONS preload in the workflow or job env could stub
its checks exactly as it could on the npm Publish line. It now starts
under env -u NODE_OPTIONS; the pin's exact text and semver-policy.md say
so. The pin test's if: always() edit matched the npm line's old text and
had become a no-op; it now matches the pinned line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:16:57 +02:00
Meta Vens
84f3551ee5 Contract: a removal is judged against the store at the removal; a refusal lists the foreign record it refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:13:32 +02:00
Meta Vens
43a63b0792 Removing a foreign record is judged against the store as it is, not the last load
A removal of a key the cache does not hold writes nothing, so it never reached
the apply; it was checked against the foreign list of the last load, and a
foreign record that arrived since was reported removed. Each store now reads
its durable state for such a removal (RefuseRemovalOfUndeclared) and refuses
through the apply's own RefuseUndeclaredOnDisk, which reloads and lists the
record. The load-time check (RefuseRemovalOfForeign) is deleted: the durable
read refuses everything it refused, and it could not save that read. A store
kind that cannot read its durable state keeps the view check as its default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:13:32 +02:00
Meta Vens
b636a69f9d A refusal lists what it refused foreign, as a pull lists it
Refuse reloads a refused key as a pull loads it, and now that includes the
store's foreign list: a record another writer moved to an id this cache cannot
read is listed in ForeignRecords after the refusal, so the next write of that
key is refused at staging instead of again at the flush. Both store kinds give
Refuse the durable record's foreign entry (ForeignAt); the variant refusal too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:13:07 +02:00
Meta Vens
c0007d1ca6 Tests: a raw write replacing the only marked record at its own key keeps the file marked
A raw entry that lists the typed record's id compatible may replace it at the
original key, which is the only path left to the opaque-payload header rule.
Replaces RawPayloadWriteOverAMarkedFileKeepsItMarked, whose second-key case
duplicated the last step of the test above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:09:17 +02:00
Meta Vens
49a5526ede Release guard tidy: threat model stated, preload route pinned shut, dated changelog headings read
- The npm Publish step starts the guard under `env -u NODE_OPTIONS`, and the
  pin's exact text carries it, so a workflow or job NODE_OPTIONS preload
  cannot run inside the guard (preload-fakes-guard).
- The pin's shell/defaults ban and its test are gone: it defended only a
  deliberate edit, which is review's job (suite-gate-structural-not-parsed),
  and refused a harmless defaults.run.working-directory. A step-level shell
  still fails the exact comparison (shell-ban-deliberate-only).
- docs/semver-policy.md states that the guard and pin stop accidental edits,
  that the cleaned environment removes known variables rather than proving no
  inherited one matters (env-denylist-overclaim), recommends a
  required-reviewer environment for the npm job, and gives the first-release
  note with the valid next versions.
- check-changelog-semver accepts `## [x.y.z] - YYYY-MM-DD` and
  `## x.y.z - YYYY-MM-DD` beside the plain forms (changelog-first-entry).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:02:55 +02:00
Meta Vens
082aec7962 Merge hands/release-guard-env: the release guard runs every per-tag check in a cleaned environment
Suite, semver check and package tests run without NODE_OPTIONS, NODE_TEST_* or
the publish token; tag/version match and package tests moved into the guard,
so no step condition can skip them. Verdict cut-release-guard-env.s1 holds;
its Low findings (threat model wording, shell ban, changelog heading) follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:00:20 +02:00
Meta Vens
bb583e89d5 Merge hands/cultmath-glsl-lowering: CultMath's interval library lowered to GLSL ES 3.00 (MIT) with Phacelle in its own MPL-2.0 file
The GLSL is generated from the HLSL by one lowering, mirrored against C# by a
committed fixture evaluated on its own arguments (portable across OSes), with
the frustum ball checked by two-sided enclosure and Phacelle by a scaled
absolute bound. design.md conflict: keeps the branch's sqrt-precision text and
drops the 6e-4 sentence interval-ops r4 removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:42:28 +02:00
Meta Vens
fb567472ee Merge hands/cultmath-interval-ops: CultMath intervals r1-r4, the tile-amortized march proved on fields with empty space
iv_frustum_ball widened by its derived float32 bound, envelope-then-noise
fields, the analytic constant-density body, one shipped void (Rh 198, S 50),
LOD agreement against a converged march, and the wells contract as ruled
(cultmath-tapes:ruling:wells-overhead-plus-floor). Verdict
cut-interval-ops.s3 holds; one Low finding (LodStep footprint branch unpinned)
is recorded for the next interval cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:38:40 +02:00
Meta Vens
19b52b16c3 Release pin: keep the shell and defaults ban, which still guards what the pinned release line runs
A job or workflow defaults.run.shell can run the exact Publish line through any
command, with the token in its env. Only continue-on-error leaves the pin: a
check that fails inside the guard refuses the release whatever the step allows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:37:38 +02:00
Meta Vens
5b1f94dec7 Research: analytic noise integration feasibility probe (cultmath-tapes)
Measured on the shipped void and the Aetheria wells at warp 60: affine warp
spans, simplex cells and lattice points per span, exact-against-march cost and
error per octave, high-octave variance, clamp crossings and transmittance
error; plus Aetheria's triangle noise. Verdict: exact integration does not
beat sampling for optical depth on either field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:36:52 +02:00
Meta Vens
20fafeb2f7 CultMath design.md: the fixture's argument rule, phacelle's scaled bound, the two-sided frustum check, phacelle's evaluator
gpu-snoise-parity is Asura's FXC and Unity readback of snoise, not a GLSL run; phacelle's GLSL
evaluator is the cultmath-tapes follow-up glsl-browser-parity-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:35:48 +02:00
Meta Vens
faff7998c3 CultMath GLSL fixture: C# evaluated on the committed arguments; phacelle's platform bound scaled; frustum check two-sided
GoldenFixtureMatchesCSharp draws arguments only under CULTMATH_WRITE_GLSL=1: the draw calls MathF.Pow,
whose bits differ by OS. Checking decodes each committed case's arguments and evaluates C# on them.
phacelle is compared across platforms within 2^-20 max(|v|, 1) per component; iv_exp keeps one ulp.
The iv_frustum_ball check gains the upper side r + d + 2^-19 (r + |c|_1) that NoiseBoundTests pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:35:48 +02:00
Meta Vens
f8c01e37bf Research: prior art for analytic noise integration along warped ray segments
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:25:22 +02:00
Meta Vens
84ccf1f37a CultMath design.md: the frustum enclosure check, the phacelle family's evaluator, sqrt on GLSL
The parity section states the enclosure rule, restores that the evaluator concatenates
CultMath.Phacelle.glsl for the phacelle family, and names CultLib's browser-pane run
(follow-up gpu-snoise-parity) as that evaluator. The frustum-ball bound no longer assumes
a correctly rounded GLSL sqrt: proven for C# and HLSL, measured on GLSL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:15:33 +02:00
Meta Vens
56fe883657 CultMath GLSL fixture: iv_frustum_ball ulp-bounded with an enclosure check; phacelle family appended
iv_frustum_ball's sqrt is not correctly rounded on WebGL2, so its family is ulp-bounded and
its entry carries the enclosure rule the consumer applies in double; GoldenFixtureMatchesCSharp
applies it to C#'s results too. A phacelle family (normalization 0.5, platform exp, sin, cos)
is appended after iv_exp; every earlier family's cases are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:15:33 +02:00
Meta Vens
e877b708be CultMath GLSL tests: overloads compared by signature; the two-file header contract pinned
EveryPublicFunctionIsLowered compares (name, parameter types) after the lowering's own
step 3, so a dropped overload such as cultmath_pcg3d(vec2) fails. TwoFileHeaderContract
pins distinct guards, the MIT and MPL-2.0 provenance labels, and the MIT file's pointer
to CultMath.Phacelle.glsl. GlslLowering exposes step 3 as LowerTypeNames; output unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:15:32 +02:00
Meta Vens
2c8585bdb1 CultMath design.md: rewrap the (c) paragraph
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:12:00 +02:00
Meta Vens
7a36848b98 Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
Meta Vens
dcd4c80547 Merge hands/cultlib-release-gate: Asura cut 3-gate, the CultMath byte gate and the structural suite gate
The release script refuses a CultMath.dll that differs from the tracked bytes
(clearing every obj configuration, CultMath source pinned to CRLF, a git
checkout required). CI publishes through one guarded entrypoint that runs the
release-checks suite first, replacing the deleted if:-condition pin (ruling
asura:ruling:suite-gate-structural-not-parsed). Verdict cut-3-gate.s4: merge;
node-env-skips-suite (Medium) and always-skips-other-gates (Low) follow before
the next release tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:59:34 +02:00
Meta Vens
eb8bcc5595 CultMath design.md: the saving section from r4's run; the unprinted 6e-4 sentence goes
Every figure is from r4's IV-REPORT lines: (a)-(d) with (c)'s oracle ceiling,
efficiency, cull fraction and probe overhead; (e) at the shipped void with the
two depth errors against the converged march; the analytic body and its pin;
the [w(z1), 1] weight-bound deviation; the contracts as asserted, (c) in the
words of ruling wells-overhead-plus-floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:58:10 +02:00
Meta Vens
3b1b5d358c CultMath intervals r4: LOD agreement against a converged march at the body start, BodyDepth, (c) against an oracle
The footprint march's optical depth at the body start is measured against a
converged midpoint march (h = 0.5, every cell) and may be at most twice as
wrong as the fixed-step march; the end-of-ray assertion goes. BodyDepth owns
the body's optical depth and its pin compares depths, not transmittances.
(c) asserts probe overhead >= 0.90 and a hard cull floor of 0.5 (ruling
wells-overhead-plus-floor); the oracle ceiling, efficiency and cull fraction
are printed. WeightBoundEnclosesOneSignedNoise pins VoidField's weight bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:54:36 +02:00
Meta Vens
48784fa4d8 Unity build: the no-.git guard asks git for the checkout top, so an empty .git is refused and a worktree passes
Finding asura:finding:cut-3-gate.s3.git-guard-existence-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:44:19 +02:00
Meta Vens
bc4af4fa58 Release gate: publish-packages.yml releases through one entrypoint that runs the suite first; the if: pin is gone
scripts/release-after-suite.mjs runs the release-checks suite and then the
release action (npm publish, or the Python sdist and wheel build that the PyPI
step uploads) from one process, and releases only if the suite passed. No step
condition can release what the suite did not pass, so the pin that read if:
conditions (status functions, multi-line if:, the leading-conjunct rule) is
deleted with its tests and docs. What remains compares each job's release steps
with their expected text exactly, less their if: line, and refuses
continue-on-error, shell and defaults in the workflow.

Ruling asura:ruling:suite-gate-structural-not-parsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:43:47 +02:00
Meta Vens
0c0c254ccf Research: prior art for measuring culling efficiency against an oracle
Eyes pass for cultmath-tapes❓wells-ceiling-oracle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:40:42 +02:00
Meta Vens
6d0a0c147d CultMath design.md and README: the two GLSL files, the concatenation order, the fixture's platform tolerance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:40:16 +02:00
Meta Vens
8b970cbebf CultMath tools: compile-glsl.ps1 -Phacelle concatenates CultMath.Phacelle.glsl after the library
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:40:16 +02:00
Meta Vens
2ba0dc17f4 CultMath GLSL: Phacelle lowered into its own MPL-2.0 file; iv_frustum_ball and iv_exp in the fixture
GlslLowering.SeparateFiles is the one name-keyed rule: CultMath.Phacelle.hlsl
lowers into shaders/CultMath.Phacelle.glsl (MPL-2.0, guard
CULTMATH_PHACELLE_GLSL) and its include line leaves CultMath.glsl, which is
now MIT and says so in its provenance line. ReadShaderSource takes an
include predicate (one resolver); the C# mirror still inlines everything.
GlslMirrorTests pins both files, checks tokens in each, takes the union of
definitions and asserts cultmath_phacelle( and CultPhasor live only in the
Phacelle file. The fixture appends cultmath_iv_frustum_ball (exact) and
cultmath_iv_exp ("ulp-bounded, platform exp", platform named); the
comparison is per family, the platform family within 1 ulp with the
maximum printed. THIRD-PARTY-NOTICES lists CultMath.Phacelle.glsl, not
CultMath.glsl, as MPL-2.0.

Regenerated on Yggdrasil (Ubuntu 24.04.5, .NET 10.0.12); 286/286 green,
iv_exp within 0 ulp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:40:16 +02:00
Meta Vens
7fc91d60aa CultMath GLSL: regenerate on the merge (cultmath_iv_frustum_ball)
Regeneration alone (CULTMATH_WRITE_GLSL=1) on Yggdrasil at 8be32a2e: the
lowering of r3's widened iv_frustum_ball, 15 lines. The fixture does not
change. Full suite 286/286 green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:36:09 +02:00
Meta Vens
8be32a2e27 Merge interval-ops r3 (3ac7250a) into the GLSL lowering
README conflict resolved by keeping r3's Intervals bullet, then the GLSL
bullet. design.md and HlslSourceCompatibilityTests.cs merged clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:32:26 +02:00
Meta Vens
5fd20a0582 CultMath interval specs: interval-ops r4 and glsl-lowering r2 as admitted
interval-ops r4 fixes verdict s2's four findings (LOD agreement against a
converged march, body depth in optical depth, the wells oracle measurement,
the WeightBound pin); its wells assertion waits on question wells-ceiling-oracle.
glsl-lowering r2 merges onto 3ac7250a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:31:42 +02:00
Meta Vens
303476db6c cultlib byte gate: CultMath source checks out CRLF everywhere, and the build refuses a tree without .git
Soul s2 found the CultMath bytes follow the checkout's line endings (an LF clone
built 53084D94, not the tracked 6781BF20) and its path when no .git maps the root
to /_/. .gitattributes pins packages/cultmath/src/** to eol=crlf, which the
tracked DLL was built from, so no re-release. The build script refuses up front
without .git, and the refusal and docs name line endings beside source and
toolchain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:27:18 +02:00
Meta Vens
942e4033b2 Workflow pin: refuses success() and any if: continued past its line, the way block scalars are refused
Soul s2 found 'success() || c' and plain or quoted if: values continued on a
following line passing the pin. Any status function now refuses, and stepCondition
reports an if: whose value continues (block scalar or a deeper-indented next line)
as multi-line, which the pin refuses. Soul's four probe forms are asserted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:26:43 +02:00
Meta Vens
72a93d23be Tests: a refusal reads what it refused, so the next write into a variant store lands
Kills the surviving mutant that drops the refused key's last-read version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:23:58 +02:00
Meta Vens
463abc5c7c Contract: the apply checks ownership by declaration again where the write lands; declaring an id makes it writable under the writing type's id
The persistence contract's rule 3 now says the store refuses, under its lock, a
write or removal at a key whose durable record is under an undeclared id, for
the store's own write API and for CultMesh single-file writes, which declare
through the catalog entry they write. The sentence that a declaring type stores
the record under its own id is corrected: Declares is registry-wide, so the
record lands under the writing type's id. The late-registration test is renamed
to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:12:06 +02:00
Meta Vens
51fbe6f533 Tests: a write over a durable record under an undeclared id is refused where it lands, in both store kinds and through CultMesh
A record another writer moved to an undeclared id after the load, a staged
removal of one, and one that appeared at a written key are refused at the apply
and reloaded read-only. A direct store Push, Delete and CommitBatch of a
read-only record are refused and never relabel it. Removing a foreign record is
refused typed. A CultMesh write over a record its entry does not declare is
refused, and one whose entry lists the id lands under its own id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:11:38 +02:00
Meta Vens
28c9a03388 Removing a foreign record is refused typed, not reported as done
Remove and a committed batch removal of a key the cache does not hold ask every
store whether it carries a foreign record there (RefuseUndeclared); one that
does refuses with the typed schema conflict instead of Remove returning false
and Commit returning Committed over a record the store still carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:11:24 +02:00
Meta Vens
067df993a3 Apply refusal: a write over a durable record under an undeclared id is refused at ApplyWriteSet and reloads read-only
ForeignOnDisk and the two RefuseForeign calls in PushAll and CommitBatchCore are
gone. ApplyWriteSet refuses first, over every key it writes or removes, by
Registry.Declares for a cache and by the catalog entry it writes for a cacheless
CultMesh write. Both refusals reload through one DurableAt; the directory store
reloads through LoadPage, the per-record body of LoadRecordPages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:11:23 +02:00
Meta Vens
8ed6095a90 Workflow pin: refuses status functions and block-scalar if:, and says which sound conditions it refuses
A Publish if: calling always(), cancelled() or failure() runs after the suite failed, the
continue-on-error bypass by another name; a block-scalar if: was compared by its indicator, so
'>-' equalled '>-' whatever followed. Both are now refused. The third refusal no longer calls
sound conditions narrower: it names the three accepted shapes, and the code comment and
docs/semver-policy.md list the sound forms textual matching refuses and why.
(Soul findings cut-3-gate.s1.status-function-bypass, block-scalar-if-compared-as-text,
pin-refusal-cost-undocumented.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:02:58 +02:00
Meta Vens
1eece6beef cultlib byte gate: the refusal names toolchain drift beside a source change, and says what to check
Unchanged CultMath source built on another SDK or host gives other bytes (Linux sdk:10.0:
db2a11cc against tracked 6781bf20), so the refusal no longer sends the operator to release a
CultMath with no change (Soul finding cut-3-gate.s1.toolchain-drift-named-as-source-change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:00:54 +02:00
Meta Vens
a287b6f8a5 cultlib release gate: clear every project's intermediates, CultMath's included, before the byte gate
Directory.Build.props puts CultMath's obj at obj\packages\cultmath\src\CultMath, which the
obj\src-only clean left in place, so an old-mtime CultMath source change could reuse a stale
CultMath.dll and pass the byte gate (Soul finding cut-3-gate.s1.cultmath-obj-not-cleared).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:59:20 +02:00
Meta Vens
22d3f96a50 Tests: a write over a foreign record is refused at the write, not first at the flush
The held-only mutant of RefuseUndeclared survived the suite, because the
apply-time refusal still caught the write at flush. This pins the staging
half for foreign records in both store kinds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:44:21 +02:00
Meta Vens
0884361731 Workflow pin: a suite condition that is a textual prefix but not a whole conjunct is refused
Kills the surviving mutant that drops the ' && ' separator from the
leading-conjunct rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:39:00 +02:00
Meta Vens
e34f351b95 Contract and comments: a cache owns exactly the ids its types register or declare compatible
The persistence contract's relabel paragraph is replaced by ownership by
declaration: a record read through the catalog's schema name or a persisted
entry's list is read-only, and declaring its id claims it. CultForeignRecord
says no type can read it; the write APIs and MintElementIds name the refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:35:56 +02:00
Meta Vens
f17ef3d825 Tests: a record a type reads but no type declares is read-only on every cache write path
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>
2026-10-03 12:34:58 +02:00
Meta Vens
639782c5d6 Research: prior art for signalling a read-only record before the write is refused
Eyes pass for variants❓read-only-record-signal. ORMs, embedded
stores, serializers, file systems and object stores; signal shape and
time-of-check pitfalls for each, with evidence pointers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:34:01 +02:00
Meta Vens
e9a668769a Ownership by declaration, staging half: a write that would change a record under an id no registered type declares is refused before staging
Declares (registry) is the one answer to whether a cache may write, relabel
or remove a record stored under an id. RefuseUndeclared applies it from
Validate's Claim, which every cache write passes, to what the home store
holds at the key: a foreign record or one held under an undeclared id.
RefuseOverwrite and its two Push calls are deleted.

Red until rewritten: ForeignRecordTests.AWriteOfTheRecordItselfStoresItUnderTheTypesOwnId
and CompatibleSchemaIdTests.AnotherNewerCacheRestoringTheIdAfterAnOlderRewriteStillFailsTheCondition,
which pin the relabel rule 3 forbids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:31:53 +02:00
Meta Vens
c64c2a0e06 Workflow pin: the suite step's condition must be implied by Publish's
The suite step's if: is absent, equal to Publish's, or a leading conjunct of
a Publish condition that has no ||. A disabled or narrowed suite step fails
the pin. Closes asura:follow_up:suite-step-condition-pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:25:18 +02:00
Meta Vens
9c50297cab cultlib release gate binds CultMath by bytes: one list owns the publish roots
The declared org.gamecult.cultmath dependency must equal the CultMath package
version (-ne, not -gt), and every CultMath.dll a managed publish produced must
equal the DLL that package tracks. The type-name check and its helper,
scripts/check-cultmath-types.cs, are gone: the byte gate subsumes them.
$managedPublishes is the single owner of publish-root identity for the clean
step, the publish loop, $publishedByName, the byte gate and --api-refs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:25:18 +02:00
Meta Vens
162bd1fcce Merge hands/semver-measured: measured semver for every package, verified by Soul s7
The release check measures each package against its predecessor tag instead of
trusting caller-declared flags. First releases are proved by a managed assembly,
the suite runs in CI and the Unity scripts before publish, and the Caching Unity
prefix is its own tag family. Verdict asura:verdict:cut-semver-measured.s7 holds;
one Low finding (suite-step-condition-unpinned) is deferred.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:09:35 +02:00
Meta Vens
13401928b2 Tests: a staged record's catalog entry is the writer's encoding, not the file's slice
The store chooses carried catalog slices by the entry object the read
produced. Keyed by schema id instead, a type whose id is unchanged but
whose entry changed (a compatible id added) would have the file's old
entry written over its own. The test stages a WsItem over a file whose
ItemId entry carries an unread slot and asserts the writer's encoding
lands, in both store kinds.

Closes variants:finding:cut-c2a-content-version.s3.catalog-writer-entry-unpinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:08:21 +02:00
Meta Vens
7ec300a5aa Semver release: the suite runs first in CI and in the Unity scripts, and the Caching Unity prefix is its own tag family
publish-packages.yml runs the suite in both jobs, after .NET setup on a full-depth
checkout and before any check or publish step; the three Unity release scripts run
it before the checker. A test reads each file and fails when the step is removed,
moved, or made non-fatal. The Caching Unity pin now needs a prefix declared once
whose exact tags hold the package's own manifest at the tag's version. No NUL byte
in the checker is a test. The policy records that releases run from a full clone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 11:43:43 +02:00
Meta Vens
c84a1fcc06 Semver check: the NUL and TAB in the ls-tree reader are escapes, so the file is text
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 11:43:42 +02:00
Meta Vens
3ac7250aa9 CultMath intervals r3: the analytic body, LOD-far, one shipped void, (c) against its ceiling
MarchTile reports a body start where the gated bound over the rest of the
grid is a positive point and appends the rest unprobed; MarchLod takes one
closed-form Beer-Lambert step there. AnalyticBodyMatchesFineMarch pins it
against the ungated field and the fixed-step product, and pins that no
(a)-(d) tile reports a body. VoidField takes a focal length; LOD-far
(Rh 4000, S 50, f 467) joins the enclosure test, which now asserts it
sampled weights in (0, 1) and at 0. The void grid ends at Rc + 6/K. The
doc derives the one shipped void (198, 50) and records the [w(z1), 1]
deviation. (c) is held to 0.90 of its probe-free ceiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:42:57 +02:00
Meta Vens
ebb12cbc93 CultMath intervals r3: the fog grid's far plane is Aetheria's Main Camera's (2048), cited by line
The Tint camera's 1000 was the wrong camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:38:12 +02:00
Meta Vens
5f6bbfd4b5 CultMath intervals r3: iv_frustum_ball widened by its derived float32 rounding bound; extreme-family enclosure
The radius gains (|c|_1 + radius) 2^-20 in C# and both HLSL copies; the
doc's strictness claim leaves. TileBallEnclosesEveryRaySegment measures in
double over the r2 domain and seven extreme families with outward corner
flows, pins the radius two-sided, and counts misses per family. The noise
ball enclosure tests draw radii to 1e3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:37:30 +02:00
Meta Vens
4d3cabec5a Imagination pass 4: interval-ops r3. The frustum ball widened by its derived float32 bound, the analytic constant-density body, one shipped void (Q9), (c) held to 90% of its ceiling, LOD weights exercised, Stryker kills demonstrated
Resolves Soul's five findings on cut-interval-ops.h2 and carries the rulings
operator-wells-contract-vs-ceiling and operator-analytic-wall-body. glsl-lowering
r2 and site-ground r3 now depend on interval-ops r3; the site-ground spec names
the one shipped void with (Rc, e) derived from (Rh, S) and the body-start word.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 11:14:39 +02:00
Meta Vens
a935d15cc2 Interop: the edited variant overrides its name, as a variant must
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 11:03:17 +02:00
Meta Vens
211ee3dbaa Interop: the override edit opens its cache with a codec
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 10:58:06 +02:00
Meta Vens
830f62ab94 Tests: a catalog entry with a slot this runtime does not read survives a neighbour's write
Pins that a carried catalog entry keeps the store's slice, in the single-file and the directory store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 10:48:57 +02:00
Meta Vens
49f8abd1b6 Interop: a C# in-place edit is written, and Rust and Python read the new value
The C# peer gains edit-in-place (payload and override through the public snapshot
API; collection and nested object through the held document), and the four-peer
test has Rust and Python read each edit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 10:47:31 +02:00
Meta Vens
2a3773a463 CultCache: the store owns a file's bytes, not the record object
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>
2026-10-03 10:47:30 +02:00
Meta Vens
92efc6f428 Tests: WriteSetTests reads the byte string through System.Buffers' ToArray, which the second fix lacked the using for
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 09:55:11 +02:00
Meta Vens
8c9faf06c6 Semver check tests: the first-release probe's cwd and blob size are pinned, and assembly-less packages read no tags' trees
Also defines the PE fixture before the tests that use it, so a filtered run no
longer reaches it before initialization.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 09:53:17 +02:00
Meta Vens
58066918c4 Tests: the version-definition pins compile and compare against the canonical encoding, not the file's own wide bytes
The previous commit does not build: the widening helper read a byte string with an overload the test project cannot bind.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 09:52:48 +02:00
Meta Vens
7d7ea233cb Tests: pin the version's byte definition, an Expect over a removed record, the cache's own written version, the no-condition hash cost, unstored uniqueness and the staleness check over a writer's own upserts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 09:48:25 +02:00
Meta Vens
6001ebf932 CultCache: a single-file version is the hash of the record's exact stored bytes, and a copied record keeps them
A record read from a store file carries the bytes it was read from (CultPersistedRecord.RawBytes, dropped when any field is
assigned) and is written as those bytes, the way a schema catalog entry already is. A version is therefore the SHA-256 of the
slice the file holds, whatever legal encoding its writer chose, not of C#'s re-encoding, and a write that copies a record leaves
its bytes and so its version alone. The contract says exactly that, so the parity cuts port it literally.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 09:48:24 +02:00
Meta Vens
98eb42af60 Semver policy: first release is proved by a managed assembly; assembly-less prefixes are pinned by the suite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:57:06 +02:00
Meta Vens
8c3e834967 Semver check: a first release is proved only by a managed assembly in the declared folder
Monorepo snapshot tags track every package's directory, so tracking a file
proved nothing about a package's own releases. A package with an assemblies
directory is a first release only when no tag under any prefix tracks a managed
assembly there (decided by PE bytes, once per blob). Assembly-less packages are
a first release when no tag of their own prefix exists; their prefix is pinned
by the suite test tying each declared prefix to a workflow trigger or an
existing tag, which now fails on a typo for every assembly-less package.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:57:06 +02:00
Meta Vens
e940a37316 Semver check: the folder pathspec has no root special case and the tree listing is compared as git prints it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:50:30 +02:00
Meta Vens
49a0bcb001 CultCache: spell a version with APIs the netstandard Caching target has
Convert.ToHexString and SHA256.HashData do not exist on the target.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:26:49 +02:00
Meta Vens
df4b22b482 Contracts: record identity for conditions is the version, the SHA-256 of the stored bytes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:23:31 +02:00
Meta Vens
f2b5b83345 Tests: version identity (rewrite at the same storedAt, pull arrival, ExpectUnchanged, staleness check, own-write version, in-memory, SHA-256 definition)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:23:31 +02:00
Meta Vens
b195525d64 CultCache: a record's version is the SHA-256 of its stored bytes; conditions, pulls, the staleness check and a refusal's reload compare it, never storedAt
Adds CultStoredDocument.StoredVersion, CacheBackingStore.VersionOf and the
SingleFileBackingStore.SerializeRecord hook, and swaps every identity comparison
in the same commit: SameStoredIdentity stays the one comparison, over versions.
Deletes the Identity helper, the (schemaId, storedAt) Expected tuple, the
in-memory fake-record projection and the directory's storedAt arrival.
Breaking public API for 2.0.0: CultCommitRequest.Expected, ConditionsHold and
the abstract SingleFileBackingStore.SerializeRecord.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:23:31 +02:00
Meta Vens
a8a6d1bf85 Semver policy: first release is derived from the folder's tracking by tags
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:01:30 +02:00
Meta Vens
2610434917 Semver check: a first release is one whose package folder no tag has tracked
The changelog-heading guard missed 12 of 17 heading shapes and a first changelog
entry. It and its parsing are deleted. A release with no same-prefix baseline is
a first release only when no tag in the repository, under any prefix, tracks a
file under the package's folder (declared assemblies dir, else the changelog's
directory); otherwise it refuses naming the tag. A tag whose tree cannot be read
refuses. Tests pin the mistyped prefix for CultMath 0.3.1, cultlib 1.0.61 and a
Python package, a genuine first release, the moved-folder choice, and an
unreadable ancestry and tree.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:01:30 +02:00
Meta Vens
ea8ca2e98b C# store header tests: a marked header stays marked over a copied record; the directory skew test holds the record ahead in the cache
The foreign-record header test expected a marked header to drop while a record was copied
forward, which the header rule no longer does. A directory store loads a record from its
page, not the manifest, so its skew test makes both writers hold the record ahead instead of
editing the manifest.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 07:09:28 +02:00
Meta Vens
25d6cb2951 C# store header: the durable header owns "holds element ids"; StoredHoldsIds and KnownIdFree are deleted
A record's bytes cannot say whether it holds an element id (an id is an ordinary keyed
string), so the per-record fact decoded by whichever type loaded the record was a split
authority: a narrower type unmarked a v3 store over bytes holding ids. HeaderForWrite is now
a pure function of the durable header, the HoldsIds of the records the write encodes, and
whether it copies any record forward: a write that copies keeps a marked header, a write
that encodes every record decides by content. The directory store takes the same rule.

Also pins the three Low test findings deferred from c2a-write-set s3: the reload cause is
observed by type and message, the author's own Expect after a commit, and the directory
store's re-mint branches.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 07:04:48 +02:00
Meta Vens
bd71385d5c C# write set tests: pin the re-mint and staleness branches, the foreign refusal's cause, and Expect on the stored delta
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>
2026-10-03 06:09:21 +02:00
Meta Vens
2e3312635d C# write set: one re-mint for every store kind, and a foreign refusal carries its reload failure
The directory store wrote an earlier storedAt over a later record it replaced, against the contract.
CacheBackingStore.StoredAtOver is now the one re-mint; the single-file apply and the directory
generation write both call it. A foreign refusal passes the reload's cause to
CultSchemaConflictException as its inner exception. Per-commit build not verified.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 06:05:41 +02:00
Meta Vens
a9225e8786 Semver policy: ancestor baseline, first-release changelog guard, declaration validation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:55:13 +02:00
Meta Vens
3a25a9d3aa Semver check: baseline is the newest older tag that is an ancestor of the commit; first release refused when the changelog lists an earlier version; declaration checked whole
The previous release is chosen by version order among same-prefix tags strictly older than the version, and must be an ancestor of HEAD, so a later backport on another branch never moves a rebuild's predecessor. A release with no previous tag is refused when the changelog already lists an earlier version, so a mistyped prefix cannot make an unmeasured first release. The declaration is validated as a whole: two packages sharing a prefix, or an entry without a string tagPrefix and changelog, refuse.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:55:12 +02:00
Meta Vens
56743c61a3 C# write set tests: the second writer in the Expect-on-a-variant test reads what the first wrote
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:18:06 +02:00
Meta Vens
2649dd1f6b C# write set tests: a readable vector holding a variant refuses a write the cache never read, and Expect on a variant is retried after the reload
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:15:42 +02:00
Meta Vens
45d6866d31 C# write set: qualify MintStoredAt, and pin the variant write into a plain store another writer changed
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>
2026-10-03 05:12:38 +02:00
Meta Vens
cc8604ae94 C# write set: tests and contract text for the refusal that reloads and the header-only staleness rule
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>
2026-10-03 05:11:17 +02:00
Meta Vens
ebd66e6f5b C# write set: a record written over another is stored later than the one it replaces
ApplyWriteSet re-mints an upsert's storedAt when the durable record it replaces is not
earlier, so A-B-A on the same wall clock cannot read as unchanged. Two stale whole-view
comments are corrected.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:04:37 +02:00
Meta Vens
ce07277c0b C# write set: one refusal path that reloads, and header-only staleness in a variant store
CacheBackingStore.Refuse reloads the refused and moved keys through Loaded and only
then unstages them, or changes nothing; RefuseForeign calls it. A write into a store
that holds or would hold a variant lands only while the durable record headers equal
what this store last read (_lastRead), else CultWriteConflictException. SameStoredIdentity
is the one identity predicate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:04:20 +02:00
Meta Vens
c4d450e82b C# write set: delete the merge judgement (JudgeMerge, MergeInvolvesVariants, Judging, HoldsVariants)
A write decoded other writers' records on a write path. The decode is removed
here; the merge tests that depended on it go red until the refusal path and the
header-only staleness check arrive in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:02:50 +02:00
Meta Vens
2774a7fe8b C# write set tests: the real-store check, and tests for the five mutants that survived
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>
2026-10-03 03:38:29 +02:00
Meta Vens
88934bcb76 C# write set tests: fix the five tests the first run named, and the Mesh test's catalog comparison
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>
2026-10-03 03:20:17 +02:00
Meta Vens
a7114a1f9f C# write set: tests for copy-forward, raw catalog carry, header rule and the inverted lay-back rules
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>
2026-10-03 03:16:49 +02:00
Meta Vens
aa186513a5 C# stores: every flush, commit and CultMesh single-file write applies a write set to the durable store
SingleFileBackingStore.ApplyWriteSet replaces WholeView and ReplaceDurable: under the lock, from the
durable snapshot read there, it removes staged removals, writes staged upserts and copies every other
record and the catalog entries they reference unchanged. PushAll, CommitBatchCore (conditional and
unconditional, one apply) and CultMesh's write call only it. Catalog entries read from a store keep
their exact bytes and are written verbatim while their decoded fields are unchanged. Derive no longer
lets a registered entry publish an id nothing staged sits under. HeaderForWrite is the one header rule
for the single-file and directory stores. A write is judged against variants another writer left in the
file. Tests of the deleted rules are rewritten in the next commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 03:05:08 +02:00
Meta Vens
60735753dd C# store: delete lay-back, WholeView, SameStoredBytes and the whole-view lay-back tests and contract text (does not build)
_unowned with its rebuild, Owns, Publisher, WholeViewWrite, WholeView, SameStoredBytes and
WroteWholeView are deleted before the write-set apply that replaces them; PushAll and
CommitBatchCore still call them, so this commit does not build. The contract states the
write-set rule that the next commit implements.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 03:01:20 +02:00
Meta Vens
780d8cfa83 Variants map: store authorities after merge-r1 and the Mesh reader cut
One owner per store decision per runtime (header, existence and links, zero-byte,
directory at path, lock, replace, soft write), with pinning tests and open gaps by
follow-up id. Written by a Modeling pass against 705ab669.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 03:00:19 +02:00
Meta Vens
38ead8c4b0 Semver policy: the declaration file owns prefix, changelog and assemblies; first release and measured set are derived
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 02:36:58 +02:00
Meta Vens
23d7beac20 Semver check derives what callers used to declare: first release, tag prefix, changelog and measured set
The checker reads each package's tag prefix, changelog and assemblies directory
from scripts/release-packages.mjs and derives the release record from git tags.
A first release is the absence of any older tag in a checkout that holds other
tags; a malformed same-prefix tag, a checkout without tags and an unreadable
repository refuse. The measured set is every managed assembly (CLI header in
the PE bytes) tracked at the previous tag at any depth; an unbuilt one is a
break, a built assembly with no namesake is an addition.

Deleted with their branches, refusals and tests: --first-release, --api-new,
--tag-prefix, --changelog, --api-baseline-path and the workflow first-release
decision line. Any unknown option is now a usage error.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 02:36:54 +02:00
Meta Vens
3d17254bbf Research: Windows store replace primitives (ReplaceFile vs MoveFileExW vs POSIX rename), with probe counts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:31:11 +02:00
Meta Vens
705ab669b5 Mesh single-file store tests: junction target is one level; the empty-store race test skips Windows
A Starfire run showed the junction helper left its parent directory behind, which the "write went
through the link" assertion mistook for a write. The race test sees nothing-at-the-path windows on
Windows because File.Replace is not atomic to a lock-free reader; that is the store's replace and
is reported as a fork, so the test skips there with that reason.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 02:17:18 +02:00
Meta Vens
b1ff2dcbce Mesh single-file store: tests for dangling link, directory, zero byte, lock, concurrent replace and the older layout; contract sentence
Behavioural tests through Mesh and CultCache for each rule the cut owns, and the persistence
contract's single-readability paragraph states that one runtime has one reader, lock and replace
and that only nothing at the path is missing. Not yet run: verification follows in one job.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 02:02:02 +02:00
Meta Vens
9852fa9367 Mesh single-file helpers read and replace through the CultCache single-file store; a directory at the store path is an error
SingleFileBackingStore gains ReadDurable and ReplaceDurable (internal): CultMesh's read and write
helpers reach the one reader, <file>.lock and the unique-temp atomic replace through them. ReadDisk
decides absence from the path's attributes, so a directory at the path is an IOException, not an
empty store. Includes the directory clause that the spec lists as a separate step.
Builds with the previous commit's deletions; the tree before this commit does not build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:59:42 +02:00
Meta Vens
2e12e9eebb Mesh single-file helpers: delete the second reader, the older catalog layout and the lock-free replace
Deletes CultMesh's ReadSingleFileSnapshot, HasOlderCatalogLayout, ReadLegacySchemaCatalogEntry,
WriteFileAtomically and the File.Exists gate, ReadStore's entry-reader delegate, and the three tests
of the older catalog layout. This commit does not build: the Mesh read and write sites still name
the deleted reader; the next commit routes them through the CultCache single-file store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:59:13 +02:00
Meta Vens
245b0619b4 Declared-range check: find npm's package root on Unix as well as Windows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:35:45 +02:00
Meta Vens
172759fd30 Declared-range check: a clone without release tags is refused rather than passing for want of releases to build
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:30:15 +02:00
Meta Vens
4f76bb9391 cultcache-ts 0.15.0; cultnet-ts and cultmesh-ts declare ^0.15.0; a check builds the tagged releases a declared range admits
cultnet-ts imports readSingleFileStore and cultmesh-ts imports schemaIdentityOf and SchemaConflictError, none of which cultcache-ts 0.14.0 exports. The new check builds each tagged release a declared range admits and requires every runtime name the dependent imports to be an export of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:28:48 +02:00
Meta Vens
2d26c8100e CultCache TS: nothing at the path needs both looks to agree; the second look is a test seam
readStoreBytes reports an absent store only when the read and the lstat both find nothing. Any other lstat failure is thrown, and an entry the lstat finds that is not a link is reported as appearing during the read. The manifest walk also runs a soft pushAll over every vector.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:28:47 +02:00
Meta Vens
12805eb32f CultCache Rust: a soft push_all asks the one reader, as TypeScript's does
Absent writes, a store it reads is left byte for byte, and anything the reader refuses (an unreadable file, a dangling link) is refused. The readability manifest walk now runs a soft flush over every vector, so TypeScript and Rust are pinned by the same files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:28:47 +02:00
Meta Vens
133dd9f760 Semver policy: a rebuild is measured against its predecessor, a first release is cross-checked, every tracked assembly counts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 00:58:34 +02:00
Meta Vens
4c7ae3adc6 Publish workflow: a package's first release passes --first-release, which the checker verifies
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 00:58:33 +02:00
Meta Vens
652b0837d2 Semver check: one rule for an unbuilt tracked assembly, comparisons must run, git failures are refusals, a first release is cross-checked
Every assembly the previous tag tracks that the build does not hand over is a break, whatever its name; a run that compared nothing is refused (the --api-new rename of a lone assembly). The cultlib build now hands over every shipped assembly. --first-release is believed only with no other same-prefix tag and the prefix its changelog directory names. git ls-tree/show failures and an unreadable changelog become refusals. Tests pin the CultMath-shaped rename, the left-reference scenario (a dependency's netstandard base chain changes), the first-release probes and the workflow's own first-release line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 00:58:33 +02:00
Meta Vens
43183276d3 CultMath design.md and README: iv_frustum_ball, the envelope composition, the scenario saving
design.md "Intervals" gains the frustum-ball paragraph and the envelope-then-noise
paragraph: how a consumer composes the bound, the short-circuit, and that the warp
lives on the noise only. r1's slab saving paragraph is replaced by r2's printed
numbers per scenario. README names iv_frustum_ball and the envelope composition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:08:35 +02:00
Meta Vens
1111537b71 CultMath intervals r2: the tile march on cell ranges and the scenario measurement
MarchTile runs the pre-pass once per tile on cell-index ranges. It starts from
the whole grid, skips and doubles on proof, halves on maybe, and runs to the
grid's end. Every ray of the tile integrates exactly the dense cells with the
dense march's Integrate. MarchLod is the void's footprint-aware march inside the
unmasked runs. IntervalSkipHalvesEvaluations prints (a)-(d) at N in {1, 4, 8, 16}
and warp in {0, D}, the void grid (Rh x camera offset x S), and the shipped point
with three marches. It asserts transmittance agreement and skips naming
saving-2x-scenarios when (a), (c) or (e) misses its 2x contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:04:54 +02:00
Meta Vens
6f2f3a19a3 CultMath intervals r2: cut r1's per-ray march; envelope-then-noise fields and their composed bound
Deleted: SavingField's MarchIntervals (per-ray probes, the StepMax growth cap,
probes attempted where the ball could never prove emptiness), its unconditional
three-snoise sample, and the saving test that skipped naming r1's fork.

Added, test-side only: the scenario model. FogField is Aetheria's nebula over its
gravity-well height map (Volumetric.cginc, Zone.cs, Settings.asset cited by
line): (a) height fog, (b) inside the fog, (c) the zone bowl and four wells.
SlabField is r1's slab, kept as the worst case. VoidField is the shipped cavity
with footprint octave weights. Each composes one bound from existing iv_* ops:
the envelope over the tile slice's box, its fade, and the noise ball from
iv_frustum_ball enlarged by the warp. The pointwise density is envelope-gated.
EnvelopeBoundEnclosesDensity checks 2,000 slices x 64 warped points with no
tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:04:54 +02:00
Meta Vens
9b9a8b3e05 CultMath intervals r2: iv_frustum_ball, the ball of a screen tile's rays over a depth segment
One new function in C# and both tracked HLSL copies (one blob): the centre on the
tile's central ray at mid-depth, radius ((z1 - z0) / 2) |(m_c, 1)| + z1 N / (sqrt(2) f)
+ warp. TileBallEnclosesEveryRaySegment checks 2,000 seeded tiles x 64 points
(sub-pixel corners at both depth ends with full-length flows) with no tolerance,
and pins the ball to the derivation so a grown ball fails too. The mirror's
compared-function count moves from 52 to 53.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:04:44 +02:00
Meta Vens
42f05f6681 CultCache C#: a dangling Windows junction at the store path is an IOException, and the tests reach it without a privilege
Opening a junction whose target is gone fails with UnauthorizedAccessException, not the
IOException the dangling-link rule promises. ReadDisk now turns that into the same
IOException. The dangling-link tests build a directory junction (mklink /J), which needs
no privilege, and the file-symlink cases skip where the account cannot create one.
The fixture removes links before its recursive delete.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 23:56:53 +02:00
Meta Vens
db4ecb1f8b Readability vectors: a store followed by one stray byte is refused by every runtime
The manifest held only three-byte and whole-store trailers, so a Rust reader that
tolerated a single trailing byte passed every suite while C#, TypeScript and Python
refused the file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 23:53:36 +02:00
Meta Vens
c642bba752 Imagination pass 3d: footprint LOD. The cavity cost as a function of hollow radius, camera offset and ramp width; Q8 retired for a steps-per-pixel contract
The march is footprint-aware: step = max(k z theta_px, min(S/4, lambda_fine/2)),
one smooth weight per octave, the bound enclosing the truncated field. The
cavity cost model is a grid, not a number; the shipped point needs about 6
steps and 12 snoise per pixel against about 35 fixed steps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:36:17 +02:00
Meta Vens
f218c9e8f3 Tests: pin the authority record shape checks and the dangling-link message
Mutation survivors of the authority reader (record count, schema id, type) and of the
reader's dangling-link error text each get a test that fails when the rule breaks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 23:28:53 +02:00
Meta Vens
210a4f50cd Imagination pass 3c: the true cavity. Unbounded fill carved by one pseudo-Gaussian sphere, the camera re-derived inside the hollow, (e) honestly under 2x, Q8 cavity-contract
Ruling operator-void-true-cavity: the void is a 3D hollow in a cloud volume,
not the 2021 bowl. The outer volume is an unbounded fill so every ray ends in
a wall; Rc grows to 640 so the hollow (198) holds the camera at 186. With no
sky the evaluation ratio for the shipped scene is predicted 1.0-1.2x; the
pre-pass's gain there is sample placement (2.0-2.6x), raised as Q8.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:22:36 +02:00
Meta Vens
9f449fb104 Imagination pass 3b: the void. The old main menu found in Aetheria e6181c11, site-ground r3, the void scenario in interval-ops r2
Ruling operator-ground-void-brush answers Q6 outside the options. The
original main menu's brush, floor, sun, orbit, camera follow, authored light
and stardust rule are read from Aetheria at e6181c11 and carried in their
units; the lighting is the original's analytic pulse made 3D; the TAA
reprojects by camera and flow; the saving test gains the void scenario and
asserts 2x on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 23:04:30 +02:00
Meta Vens
070a58d2aa Imagination pass 3: envelope-then-noise fields, scenarios with empty space, the tile pre-pass, the split Phacelle
Revision 2 specs for the two CultLib cuts of cultmath-tapes, as follow-ups on
hands/cultmath-interval-ops (c9bd003b) and hands/cultmath-glsl-lowering
(5ae207e8), under the rulings operator-tile-amortized-march,
operator-split-phacelle, operator-compiler-downloads and
self-exp-measure-in-site, and the operator's challenge to the march.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:39:33 +02:00
Meta Vens
a23eb8219d TS test: the forged-header case spells its newline as an escape
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:19:15 +02:00
Meta Vens
8a833730c9 cultnet-ts: Idunn authority files are read by cultcache-ts's one store reader
readAuthorityRecord decoded the store itself and accepted only cultcache.store.v1. It now calls
readSingleFileStore, so v3 reads, a zero-byte file is a StoreUnreadableError, and a refused
header is never echoed. Tests derive their bundles from the Rust-written fixtures.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:12:15 +02:00
Meta Vens
0122761a74 TS: the one store reader is exported and decides soft pushAll; absent-read and forged-header tests
pushAll({soft}) asked readStore, so it skipped every file the reader refuses. It now asks
#readDisk: absent writes, readable skips, unreadable refuses. The reader is readSingleFileStore,
synchronous and exported, so a consumer reads a store through it instead of decoding one itself.
New tests: soft pushAll over absent, readable and five unreadable files; reading an absent store
writes nothing; a forged header that does not start at the prefix is never echoed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:12:15 +02:00
Meta Vens
68c1296b7a Semver check: keep git's stderr out of the refusal (the fatal-message test fails without it)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:44:03 +02:00
Meta Vens
c358ef9739 Semver check: drop the unobservable stdio option; the empty-package test really creates the directory
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:38:55 +02:00
Meta Vens
df13b502b9 Semver check: built and reference paths are resolved against the process, not the tool's directory
The ApiCompat tool runs from scripts/api-gate, so a relative --api-built or --api-refs
failed as an unreadable assembly. The release scripts pass absolute paths; any other
caller does not have to.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 21:30:37 +02:00
Meta Vens
0ac870f132 Semver check tests: pin git's silence, the gate manifest file, netstandard.dll, the first-release hint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:28:26 +02:00
Meta Vens
e7786ffac2 Semver check: a malformed version is refused by name, and refusals are pinned never to repeat input contents
A malformed --version fell into the tag-reading catch and was reported as unreadable
tags. It is now validated first and refused without repeating the value. A canary test
runs seven refusals (measured break, unreadable built DLL, unreadable baseline blob,
missing entry, skipped version, breaking under too small a bump) with a canary in the
changelog and the assemblies and asserts it never appears in the output.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 21:27:03 +02:00
Meta Vens
da2b9482e5 Rust: the rename fault fires inside replace_file_atomically, at the call a swallowed error would hide
The seam sat before the call, so a mutant that swallowed the real rename's result was invisible to it. It now makes the rename itself fail, on both platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:11:07 +02:00
Meta Vens
7b815e1ca8 TS and Python: pin the refusal text (v3 named, ASCII digits only); TS comments say a zero-byte file is refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:05:38 +02:00
Meta Vens
9a251fe60c Rust: one store-header reader; bin-encoded headers refused; rename fault seam and the tests the survivors asked for
store_header is the only reader of a store file's leading bytes. It returns the header slot (a MessagePack str only, never bin, as in C#, TS and Python) and separately the proof that the bytes are one whole array, so a newer store cut short after its header is still named unsupported. leading_header, read_array_header and read_string are deleted. decode_store_file takes the header the reader already found.

Vectors bin-header-v1/v3/v9 join the readability manifest as refuses x4. Tests: a rename fault step (WriteStep::Rename) shows a failed rename is NotReplaced, the store intact and no staging file left; every merging writer refuses through a dangling link; the unsupported-format refusal names v1 and v3 and echoes only ASCII-digit headers.

Not yet compiled or run at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:05:37 +02:00
Meta Vens
c4826a39a3 C# tests: the directory refusal shows a header only as store version digits, so the suffixed legacy names are no longer echoed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:00:51 +02:00
Meta Vens
eb79b21e51 C# tests: read "echoed" from how the refusal introduces the header, since it also names the readable formats
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:51:55 +02:00
Meta Vens
3f33445d84 C# tests: assert the header refusal text with plain checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:51:00 +02:00
Meta Vens
9e481f59b7 C#: detect a dangling store link by its reparse-point attribute (Caching has no FileInfo.LinkTarget)
Commit 425531d8 does not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:49:43 +02:00
Meta Vens
425531d88e C#: a dangling link is an I/O error on read; the directory refusal echoes a header only as store version digits; contract names Rust's real type
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:49:12 +02:00
Meta Vens
75ba6c0f5c Mesh: a zero-byte store file is refused, not read as empty; the alias test is retargeted to one type per schema
CultMesh's single-file reader had its own zero-length-means-empty branch, so a Mesh write
replaced a zero-byte file the cache refuses (merge-r1-precedence). It now asks the cache's
snapshot reader and gets its verdict. The auto-merged test that read a same-schema instance
through MeshNoteAliasDocument cannot be built under retire-schema-alias; it now states the
ownership rule it protected (a database serves only what a shard owns) with the one type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:49:12 +02:00
Meta Vens
5ae207e82b CultMath design.md, README, notices: the GLSL target
design.md gains "GLSL Target: Source Transformations" (the list, the common-subset rule, the
regeneration command, the derived-file rule, the two parity legs). README says how a WebGL
consumer includes CultMath.glsl. THIRD-PARTY-NOTICES lists CultMath.glsl as MPL-2.0, because
it inlines the Phacelle include.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:45 +02:00
Meta Vens
34a9329db5 CultMath tools: get-glslang.ps1 (pinned 16.6.0) and compile-glsl.ps1
compile-glsl.ps1 writes the wrapper a WebGL2 consumer writes (#version 300 es, highp precision,
the library by concatenation, one call of every public function) and compiles it with glslang;
-Body replaces the calls, for the negative control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:45 +02:00
Meta Vens
d2ff33085f CultMath GLSL: shaders/CultMath.glsl generated from the HLSL by one enumerated lowering, pinned
GlslLowering (test project) is the HLSL -> GLSL ES 3.00 transformation as named steps: includes
inlined by the C# mirror's own resolver, guards and texture functions dropped, type names,
static const, casts by a balanced scanner, intrinsic renames, literals passed through, the
CULTMATH_GLSL guard and a provenance line. GlslMirrorTests pins the committed file to the
lowering (CULTMATH_WRITE_GLSL=1 regenerates it), finds no HLSL-only token, finds every public
function but the texture samplers, and pins fixtures/glsl-parity.json (256 seeded cases per
family as float32 bit patterns) to C#.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:44 +02:00
Meta Vens
0eeb619b35 CultMath HLSL: write the shader in the subset GLSL ES 3.00 shares, bit for bit
Each edit is legal HLSL that computes the same bits, and closes one dialect gap the GLSL lowering
would otherwise have to special-case:
- snoise and snoise_grad: step(h, 0.0) -> step(h, float4(0.0, 0.0, 0.0, 0.0)); GLSL has no
  step(genType, float).
- pcg: (state >> 28) + 4 -> + 4u; GLSL has no implicit int -> uint.
- cellular and phacelle jitter, cellular id: (float)((uint)hash.x >> 8) before the float
  multiply; GLSL has no implicit uint -> float.
- damp (float2/3/4): the scalar amount is splatted explicitly; GLSL has no lerp(vecN, vecN, float)
  promotion.
- pcg3d(float3) is defined before pcg3d(float2), which calls it; GLSL needs a declaration before use.
Both tracked copies; the C# mirror stays green, FXC cs_5_0 compiles both copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:44 +02:00
Meta Vens
0f13c7d368 C#: the unreadable-format refusal names every format the runtime reads, v3 included
A mutation survivor on the merge's refusal text: dropping v3 from the list passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:17 +02:00
Meta Vens
e5f06a2a49 Python: fix the payload byte literal in the refusal-text test (the previous commit did not parse)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:04:16 +02:00
Meta Vens
0bd1bac963 Python: pin what the unreadable-format and extra-slot refusals say and what the runtime reads
Mutation survivors on the merge's refusal text (the two readable formats, the
unrecognised-header description, the record-slot refusal) had no test pinning them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:02:15 +02:00
Meta Vens
c9bd003bb3 CultMath intervals: comments name the gradient without the snoise_grad/fbm_grad tokens
The cut's negative check is a pattern over math.Interval.cs and CultMath.Interval.hlsl: the
bounds use values only, so neither file names the gradient functions. Comments only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:57:01 +02:00
Meta Vens
b8dddc90d9 CultMath design.md and README: the Intervals section
Representation, the op list, the one-ulp widening rule and its two cases, the Lipschitz
provenance and pin, what the ball bounds guarantee and what they do not, tightness, the
saving and its open fork, and the consumers. Every number is one a committed test prints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:56:30 +02:00
Meta Vens
cd7913bfcc CultMath intervals: enclosure harness, one-ulp shrink mutants, noise bounds, Lipschitz pin, saving
IntervalTests runs every op over 10,000 seeded intervals x 16 points with no tolerance, and
kills each op's one-ulp-shrunk lo and hi; exp's widening is an equivalent mutant on this CPU,
and smoothstep's is killed by a constructed non-monotone step. NoiseBoundTests encloses snoise
and fbm over 2,000 balls x 64 points and the bounded domain warp, pins SNOISE_LIPSCHITZ against
1e5 points and the ascent from MeasureLipschitz's witness, prints tightness, and prints the
march saving. The saving is 0.93x, not 2x: that test skips with the fork named.
MeasureLipschitz and MeasureMonotonicity are explicit, slow tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:56:30 +02:00
Meta Vens
bbf5269751 CultMath intervals: cultmath_iv_* over float2(lo, hi) in C# and HLSL, with snoise and fbm ball bounds
math.Interval.cs and shaders/CultMath.Interval.hlsl (included from CultMath.hlsl after Phacelle;
both tracked copies) add iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale, iv_abs, iv_min,
iv_max, iv_sqr, iv_sqrt, iv_exp, iv_clamp, iv_saturate, iv_smoothstep, iv_lerp, iv_snoise_ball
and iv_fbm_ball, and SNOISE_LIPSCHITZ = 10.099261 (1.10 x the refined max |snoise_grad| that
NoiseBoundTests.MeasureLipschitz prints). iv_exp and iv_smoothstep widen by one ulp through
asfloat(uint), added beside asuint as the HLSL intrinsic. The mirror test now compares 52
functions; the Unity package and the NuGet content files carry the new include.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:52:33 +02:00
Meta Vens
c5d9728b05 Semver policy: refusals, first-release declaration, unmeasured changes list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:52:17 +02:00
Meta Vens
83e3f81263 Semver check: measuring nothing refuses; reference assemblies; tool restored alone
- A missing or moved baseline path, a previous tag that does not track the DLLs, and a built DLL
  with no namesake at the tag (unless declared with --api-new) refuse, naming what is missing.
- First release is declared with --first-release, never inferred: no previous tag, or tags that
  cannot be read, refuse without it.
- The "already published" skip is removed; a local tag of the released version exempts nothing.
- ApiCompat and the pinned NETStandard.Library.Ref 2.1.0 live in scripts/api-gate and are restored
  there, so the root manifest (dotnet-stryker) is no longer needed by a release. Both sides resolve
  netstandard through the reference assemblies.
- Tests: refusals, netstandard false breaks, cross-assembly references, restore diagnostics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:52:14 +02:00
Meta Vens
d350e150b0 Rust: bring back R1's leading-bytes readers for leading_header
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:49:01 +02:00
Meta Vens
f35867a232 Rust: a newer store cut short is still named unsupported; tests follow the single reader
R1's rule that the header alone decides an unreadable-format store (a newer store
cut short is not damaged) is kept as leading_header, asked before the whole-file
proof. A directory at the store path now stops a write at the read as an I/O
error, so the two tests that used it to reach a failed rename assert that instead,
and the two c2a header tests assert R1's UnsupportedFormat kind, not c2a's old text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:48:34 +02:00
Meta Vens
ff0251bbf8 Fix the R1 merge: close the Rust store_header visitor; a whole-store replace through a dangling link keeps replacing it
The merge left store_header's visitor unclosed (does not build). R3's pin that a
whole-store flush replaces a dangling link survives the single reader: the reader
still refuses the link as an I/O error, and the flush, which has no content to
carry from a link that holds none, writes v1 over it (Rust write_all_unlocked, TS
pushAll).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:46:39 +02:00
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
d0ea37f8e4 Research: prior art on cross-process file locking across runtimes and OSes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:41:10 +02:00
Meta Vens
dbc2a6295b Imagination pass 2: the ground moves, the CSS wash is sampled from it, stardust and DoF mapped
Folds in the operator's rulings: Q1 cultmath-tapes, Q2 samples then
resolution, Q3 the shader owns a dynamic ground and the CSS wash is sampled
from its result. The nebula is flow-warped (two-phase flow map) with one
flow texture owning motion for the warp, the TAA motion vectors, the hue and
the stardust. Interval culling encloses the warp by enlarging the ball;
interval-ops gains that test. New fourth cut site-stardust: stateless
instanced particles plus an opacity-driven gather on the stardust layer, the
web form of the raymarch-driven DoF. Budget re-checked: the full stack lands
a mid-range phone at S = 4-6, not 8. Docs only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 19:31:13 +02:00
Meta Vens
49e0fefb69 Imagination: map the gamecult.org ground shader as the proving ground for CultMath intervals
Three cuts under a proposed cultmath-tapes campaign: a textual HLSL -> GLSL
lowering pinned by test, interval ops with enclosure tests and a committed
Lipschitz constant for snoise, and the site's per-pixel interval-culled,
TAA-accumulated cloud ground driven by body tokens with the CSS wash as the
fallback. Docs only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 19:16:19 +02:00
Meta Vens
32f5700b2a Semver check tests: double trailing slash, and a process started outside the target repo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:03:00 +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
c49c76ee50 Research: prior art on records a writer does not own (formats, embedded stores, literature)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:51:58 +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
84a34b8735 Semver check tests: pin the broken-manifest failure text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:35:27 +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
f40069dd88 Semver check tests: scope the cleanup assertion to the checker's own temp dirs, cover a broken manifest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:34:05 +02:00
Meta Vens
6a3dae8954 Semver check: run tests from outside the repo, pin cleanup, usage and restore failure
The checker now runs with the target repo as its working directory in tests,
so the tool manifest is found from the script's own location as it is in the
release scripts. Adds tests for usage errors, a missing changelog, backslash
and trailing-slash baseline paths, temp-file cleanup, a restore failure and a
missing dotnet failing closed, the five-line failure excerpt and the
five-diagnostic cap. Drops two unreachable defaults and an unreachable
spawn-error branch from the checker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:32:38 +02:00
Meta Vens
ed31a97689 Semver check tests: cache built fixture assemblies by content
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:01:37 +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
65bc144da0 Semver policy: describe the measured input and its failure rule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:08:36 +02:00
Meta Vens
b8e1f7e0f6 Unity release scripts: run the semver check after the build, with the built DLLs
The check moves from before the build to once the assemblies under judgement
exist, and receives every shipped GameCult.* DLL (cultlib) or the fresh
CultMath.dll with the baseline path of the previous tag's tracked plugins.
The early calls are deleted; each script keeps exactly one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:08:36 +02:00
Meta Vens
8f2eab7651 Semver check: behavioural tests for the measured public API
Real assemblies built in the test and compared through the CLI: removal
without ### Breaking, removal under too small a bump (post-1.0 and 0.y.z),
additions alone, a declared break with nothing measured, a removed GameCult.*
assembly, tool failure failing closed, inherited-member removals, an
already-published version, the baseline coming from the tag's blob, and a
first release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:08:36 +02:00
Meta Vens
716ef9341c Semver check: measure the public API against the previous tag's DLLs
check-changelog-semver.mjs takes --api-baseline-path, --api-built and
--api-refs, extracts the previous tag's tracked DLLs as git blobs, and runs
the pinned Microsoft.DotNet.ApiCompat.Tool on each built assembly. Measured
breaks make the release breaking for the lane check and must be declared
under ### Breaking; a tool failure without a diagnostic refuses the release;
a GameCult.* assembly the tag tracked and the build lacks is a break.
Behavioural tests follow in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:03:09 +02:00
Meta Vens
614fd44584 Merge CultMath 0.3.0 release (Asura 2a-ii-c)
snoise moves to the upstream kernel (breaking values, minor bump for 0.y),
smin_grad, cellular, snoise/fbm/ridged gradients, Phacelle, BoundedLeastSquares.
Unity package licence is MIT AND MPL-2.0. Soul holds: asura:verdict:cut-2a-ii-c.s3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:58:28 +02:00
Meta Vens
9e0f0ca36d CultMath 0.3.0: notices say the Unity package is MIT AND MPL-2.0, not MPL-2.0 as a whole
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:56:55 +02:00
Meta Vens
603d972f57 Single-file store: an unowned variant has no resolved document at load, so its in-place check is skipped
The previous commit serialized every unowned record's document at load, which a variant cannot give
until the cache resolves it (five tests failed on it). A variant persists its delta, never its
resolved document, so there is no in-place change to detect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:54:41 +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
8ab384448c CultMath 0.3.0: scope snoise constants to 3D in the changelog; Unity package is MIT AND MPL-2.0
2D snoise keeps r^2 0.5 and scale 130; only the permutation (+10) changed.
package.json license and LICENSE.md now match CultMath.csproj: MIT overall,
Phacelle files MPL-2.0 (operator ruling cultmath-license-mit-and-mpl).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:48:16 +02:00
Meta Vens
98e3b91389 CultMath 0.3.0: rebuilt tracked DLL and pdb (Windows, Starfire)
Output of packages/cultmath/scripts/build-unity-package.ps1; reruns of the
script exit 0 with a clean tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:05:23 +02:00
Meta Vens
32ff786aed CultMath 0.3.0: version bump and changelog (snoise values break, minor bump)
Does not carry the rebuilt DLL yet; the Unity package script's version
parity check fails until the binary follows in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:03:56 +02:00
Meta Vens
c2cd4202d2 Tape campaign: meshing defaults to surface nets plus the tile pass, not dual contouring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:40:05 +02:00
Meta Vens
f7d4b63940 Park the CultMath tape campaign: interval-pruned field evaluation, option c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:30:54 +02:00
Meta Vens
35ff3fbe42 Variants map: Soul holds C2a F2 (entry bytes, stale lay-back, stuck flush; main merge is its own cut)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:53:45 +02:00
Meta Vens
7007a17059 Variants map: Soul holds the cultcache-ts push identity fix (label match survives; Python and Rust share the bug)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:29:44 +02:00
Meta Vens
0a26f96ede Variants map: F2 fork A ruled (forbid slot reuse); fork B deferred as the next parity cut
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:08:21 +02:00
Meta Vens
9dcb3aa154 Merge put-serve bound: a put the server could never serve is refused, and refusals say why
CultMesh document servers (Rust, TS, Python) refuse a put whose served snapshot reply
could never fit the response bound, before storing it. Refusals carry fixed text per
variant, acknowledge nothing, go unreliable and unordered, and never echo peer input or
OS text; publishers and the snapshot client report a refusal instead of timing out. Send
failures follow Cut D; a reply the queue cannot take is answered, not dropped; a failed
send's goodbye names a fixed error code in every runtime. Python validates options as
Rust does and survives any unstorable put or malformed TCP message.

Soul passes 1-4 plus merge verdict; the final batch closes it. Verified on Yggdrasil at
8b8f8436 (main merged): every CultNet/CultMesh suite in Rust, TS, Python and C#, interop 27/27.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:40:50 +02:00
Meta Vens
8b8f8436cc cultmesh-ts: a snapshot refusal carries fixed text and acknowledges nothing
The snapshot fallback sent the caught error's message to the peer, and
errors from getCache or the registry went the same way, so a message the
application or a peer supplied could reach the wire. Its ack fields also
covered the refused request. The snapshot path now refuses per case with
fixed text (the snapshot source failed / the snapshot response is too
large / the snapshot response could not be queued) through refuse(),
which sends unreliable and unordered, so it needs no room in a full
queue, with ack fields that acknowledge nothing, as every runtime's
refusal does. onError keeps the detail.

Tests, with a raw peer that acknowledges nothing: a queue of 8 filled
exactly by an unacknowledged 8-fragment reply, then a refusal that still
arrives and acknowledges nothing; and a getCache failure carrying a
canary, refused with "the snapshot source failed".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:06:03 +02:00
Meta Vens
e1bdf92321 C#: an unsendable goodbye names the failure by a fixed code
EndUnsendable put the SocketException's message on the wire, the
platform's wording. SendErrorCode names a permanent failure EMSGSIZE,
EINVAL or EAFNOSUPPORT, anything else UNKNOWN, so every runtime's
unsendable goodbye reads "packet could not be sent: CODE" with the same
codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:04:40 +02:00
Meta Vens
d541e9eb35 cultmesh-py: a refusal acknowledges nothing and names its own send failure; TCP handling refuses instead of dying; the daemon checks its port
Queue-full refusal (findings 2 and 6). The refusal was built in
_send_rudp_schema_frame before _end_refused_peer reset the session, so
its ack fields covered the refused request (ack=102 mask=0x3). A reply
the queue cannot take now raises _ReplyUnqueueable; the loop ends the
peer through _end_refused_peer, which builds the refusal before the
reset, gives it ack fields built after it (acknowledging nothing, like
the goodbye and like Rust's refusal), sends it unreliable and unordered,
then the goodbye. A refusal fragment that can never be sent is not
dropped: the goodbye names its failure ("packet could not be sent:
EMSGSIZE"), where it used to say the session refused a packet.

Queue full to the brim (item 1). The tests fill a queue of 8 exactly with
a reply the peer never acknowledges, then show the next reply is answered
with response_unqueueable, acknowledging nothing, followed by the
goodbye; and that a refusal whose fragment cannot be sent ends the
session with a goodbye naming EMSGSIZE.

TCP (finding 8). A message whose handling raised (snapshot recordKeys as
an int, a shard-log afterSequence that is not a number, subscribe,
schema-catalog and verse requests with int fields) killed the
connection's thread with a traceback that could quote the peer. The TCP
handler now refuses such a message with fixed text (message_unhandled),
logs the error type only, and serves the connection's next message.

Daemon (finding 10). --port 99999 is an argparse error (exit 2), not a
traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:04:07 +02:00
Meta Vens
c3cc2b49c4 cultnet-py: an unsendable goodbye names the failure by a fixed code
end_unsendable_session put the OSError's text on the wire, which is the
platform's wording and can quote an address. send_error_code names a
permanent failure EMSGSIZE, EINVAL or EAFNOSUPPORT (Winsock equivalents
included), anything else UNKNOWN, as the Rust and TypeScript goodbyes
do; is_permanent_send_error is now "has a known code".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:04:07 +02:00
Meta Vens
a71f62e997 cultmesh-rs: pin the refusal at a queue full to the brim, and a real EMSGSIZE
The queue-full test used an empty queue, so a reliable refusal fitted and
the reliable-refusal mutant lived. The new test fills a queue of 8
exactly with a reply the peer never acknowledges; the next reply cannot
be queued, and its refusal must still arrive, acknowledging nothing the
peer sent. A RawPeer helper drives the peer packet by packet; the gap
test uses it too.

A response too large for any datagram fails with the kernel's EMSGSIZE:
the rejection is ResponseSendFailed("EMSGSIZE"), the goodbye is exactly
"packet could not be sent: EMSGSIZE", and no refusal is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:01:33 +02:00
Meta Vens
b8f754c898 cultmesh-rs: the snapshot client quotes only a refusal it recognises; the publisher doc says what holds
request_raw_snapshot_from_rudp_catalog put a catalog's error text into
its own error unfiltered. It now reports "refused the snapshot", naming
the refusal only when it is one of the fixed CultMesh refusal texts, as
the publisher does; both share recognised_refusal.

The publisher's doc claimed any refusing catalog is reported as a
refusal. That holds against a Rust catalog, which never acknowledges a
message it refuses; a catalog that acknowledges on receipt and refuses
afterwards (the TypeScript document server) is reported as having
admitted it. The doc now says so.

A fake catalog refusing with a canary is shared by the publisher test
and a new snapshot-client test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:01:33 +02:00
Meta Vens
5ef988b1d0 cultnet-rs, cultmesh-rs: an unsendable goodbye names the failure by a fixed code
end_unsendable_session put the io::Error's Display on the wire: the
platform's wording, which differs by OS and by which packet failed.
cultnet-rs gains send_error_code, the fixed name of a failed send's
cause (EMSGSIZE, EINVAL, EAFNOSUPPORT, or UNKNOWN), matching the codes
the TypeScript goodbye already carries; is_permanent_send_error is now
"has a known code". The goodbye reads "packet could not be sent: CODE".

ResponseSendFailed carried an ErrorKind, which lost the cause (EMSGSIZE
has no kind and read as Uncategorized). It now carries the same fixed
code, and the session ends where the send failed, over the real error,
so the goodbye and the caller's rejection name the same failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:01:33 +02:00
Meta Vens
a771e71bd6 Rust test: pin the cargo-mutants survivors in the catalog lay-back
- The fixture gains an older entry listing the id ahead of the one that
  owns it, and the whole-store write carries the record under the
  registered type a Rust cache's pull gives it, not the entry's name.
  Kills the own-id lookup flip and the ||-to-&& flip in the entry filter.
- A store path that cannot be read (a directory) is an error, not an empty
  store (the NotFound guard had no test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:59:52 +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
6275b175e6 Merge origin/main (b248b0a1) into hands/cultmesh-put-serve-bound
No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:53:51 +02:00
Meta Vens
1d6e136440 Error-contract map: put-serve Soul merge-verdict notes (ENOSPC label, detail keys, new code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:53:42 +02:00
Meta Vens
b248b0a148 Variants map: C1 questions ruled (override deleted; declared compatible ids resolve); defaults stand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:52:25 +02:00
Meta Vens
ebb1eb2528 Variants map: C1 cut map (one schema id on the wire); two operator questions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:50:08 +02:00
Meta Vens
4a7fa1d63f Variants map: schema identity ruled C1 (<name>.vN on the wire); AetheriaEve not a consumer; C2 deferred
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:34:09 +02:00
Meta Vens
976b57f10c Test: each runtime's writes leave another runtime's records and catalog entries unchanged
C# writes a store; Rust, TypeScript and Python each write their own note
into it; the C# record and catalog entry are unchanged and C# reads the
store. C# writes again, and the other runtime's record and entry are
unchanged too. Compared on decoded MessagePack values, every slot of the
entry and its members included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:20:11 +02:00
Meta Vens
eae56189c6 Rust: a write lays back the catalog entry the file holds for an id
C2a F2, widened by Self. The schema-identity probe showed a Rust write
replacing the C# catalog entry it read with a fabricated one: version
"sha256:...v1", members [], canonical JSON naming the hash.

- The store's one reader returns the file's catalog beside its envelopes.
- The encoder writes back the entry the file holds for each id it writes
  (by own id, else a lister) and describes an id itself only when the file
  has none, or when a registered type owns the id under another name (a
  rename, whose rule is unchanged).
- Test: a push and a whole-store push both lay back a C#-shaped entry byte
  for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:20:11 +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
daedfdd658 Merge CultCache R1: typed read failures, no empty-file stores, no echoed values, typed write outcome
Only "nothing at the path" reads as an empty store; a zero-byte file is Undecodable in every
runtime; a dangling link is an I/O error. Errors name keys and schema ids only, never stored
values, and echo a header only in the cultcache.store.v<digits> shape. Single-file writes
report NotReplaced / ReplacedNotDurable / Rejected; an unlock failure after a successful write
no longer replaces the write's result.

Soul passes 1-3; batch 4 closes pass 3. Merged tree 9d9e76d4 verified on Yggdrasil: Rust
(cultcache, cultnet, cultmesh), cultcache-ts 44 including the four-runtime interop test,
cultnet-ts 198, cultcache-py 47, GameCult.Caching.Tests 346.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:13:24 +02:00
Meta Vens
fa34b68593 cultmesh-rs: drop the unreachable earlier-failure arm; rustfmt the resolution
receive() returns a reply only for a Ping, answered with a Pong, and a
Ping delivers no frame, so no rejection can follow an earlier send in the
same poll. The arm that carried such a failure into the rejection's end
was dead code; its mutant survived for that reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:05:45 +02:00
Meta Vens
3d285f4923 Variants map: cross-runtime schema identity fork (C1 recommended); F-ID.1-3 open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:04:55 +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
2ee707d05e cultmesh-rs: no permanent send failure is dropped when a rejection ends the session
A reply sent earlier in the same poll could fail permanently and then be
dropped when a rejection returned first; and a refusal fragment that can
never be sent must end the session naming its error. Both now reach
end_session as the unsendable end. The test makes the refusal itself
unsendable: the peer's goodbye names the error, nothing is counted as a
lost datagram, and the other peer is served.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:50:43 +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
62b34a7139 Merge origin/main (ack Cut D) into hands/cultmesh-put-serve-bound
Send-failure semantics are Cut D's: a transient failure is counted and
never reported; a permanent one ends that peer's session.

Conflicts:
- cultmesh-ts index.ts: the two sendPackets. Cut D's
  sendPacket(record, packet) is the one; the branch's reporting
  sendPacket is deleted, and so is the options doc's "Every failed
  datagram send reaches onError".
- cultmesh-py server.py: the rudp_send_failures doc is Cut D's. The
  receive loop is Cut D's (handling guarded, permanent send errors
  carried) with the branch's over_rudp=True. _send_rudp_schema_frame
  sends in rudp_max_fragment_bytes fragments and returns Cut D's
  permanent error from any fragment; a reply the queue cannot take no
  longer vanishes into a count (below). _send_rudp_packet and the two
  peer-ending helpers are Cut D's.
- cultmesh-py test imports: both create_rudp_schema_transport and
  decode_rudp_packet.

Semantic resolution:
- cultmesh-rs: end_rejected_session and Cut D's end_refused_session and
  end_unsendable_session collapse into one end_session(key, SessionEnd).
  A refusal fragment that can never be sent is not dropped: the goodbye
  names its error. The refusal stays unreliable, unordered, and
  acknowledges nothing (its ack fields are built after the reset).
- cultmesh-rs: Cut D's "snapshot response could not be sent: {error}"
  becomes CultMeshRudpRejectionReason::ResponseSendFailed(ErrorKind),
  with a fixed REFUSAL_TEXTS entry. Its session ends as unsendable: the
  peer gets the goodbye naming the error and no refusal.
- P3: a snapshot the reliable queue cannot take is answered.
  cultmesh-py sends a small cultnet.error.v0 (response_unqueueable,
  fixed text) unreliable and unordered, then the session ends as Cut D
  ends any message the handling cannot serve. cultmesh-ts sends its
  snapshot fallback error unreliable and unordered. cultmesh-rs already
  answers through the refusal (ResponseQueueFailed); a test pins it.
- Tests follow: the branch's EMSGSIZE tests now expect Cut D's permanent
  handling (the TS one is deleted, Cut D's own tests cover it), and the
  Python queue test expects the answer instead of a count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:49:55 +02:00
Meta Vens
b04d03b6f1 Ack map: Cut D landed at 2f06ef84
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:44:13 +02:00
Meta Vens
2f06ef8400 Merge ack Cut D: a failed send to one peer stays that peer's loss; C# schema server survives peer input
A peer's undecodable or unhandleable message ends only that peer's session and never
escapes Poll (the C# RUDP schema server's live crash). A send failure's goodbye belongs to
the generation that sent it. TS transient send failures are counted, not reported. The
interop harness finds the Rust peer through cargo metadata.

Soul passes 1-4; batch 5 closes pass 4's findings. Merged tree a892e117 verified on
Yggdrasil: GameCult.Networking 481, Mesh 264, cultnet-ts 198, interop 27/27 with
CARGO_TARGET_DIR set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:44:01 +02:00
Meta Vens
f789c8d133 cultcache-rs: the document canary uses a value serde would quote
The first canary put a string where a u32 goes, and rmp-serde refuses that by
marker without quoting it, so the canary passed with the serde text back.
A string where a unit enum goes is quoted as an unknown variant. The canary
now covers get, get_all, get_all_with_keys, put_envelope and prepare_entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:35:47 +02:00
Meta Vens
92d9e13868 Test: a string-named store read is typed as a note in the assertion
The handle named by a bare schema id yields unknown; 3c9 test code read .body
off it and did not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:20:54 +02:00
Meta Vens
108071e8b3 Interop harness: ask cargo where the Rust peer is, and name missing prerequisites
The harness assumed that the Rust interop peer builds under
packages/cultnet-rs/target. When CARGO_TARGET_DIR or a cargo config's
build.target-dir is set, cargo builds elsewhere, and every Rust lane
failed with a spawn ENOENT. The path now comes from `cargo metadata`'s
target_directory, read once on first use.

The TypeScript peer imports @gamecult/cultcache-ts, and the Python peers
import msgpack. When either was missing, the lanes died with import
errors in a child's stderr or with timeouts. The first use of each peer
now checks its prerequisite and fails with a message that names it and
the command that provides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:44 +02:00
Meta Vens
1f04d5eda3 cultnet-ts: pin the one-fragment size check with a large fragment too
With only 1 KiB fragments, a pre-check measuring a small multiple of a
fragment (Soul's min(payload, frag*2)) still passed a 70,000-byte send.
The fixture now also runs with 40,000-byte fragments. Any such multiple
of that is over the datagram limit, so the mutant dies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:36 +02:00
Meta Vens
ce63ff88dc C#: a handler's fault ends the session without blaming the peer
The schema server's backstop ended every session with "session refused
a packet", including when the fault was the server's own: for example a
handler's response too large to send, or a refusal the handler did not
type. Decoding and handling are now two catches:
- A message that cannot be decoded is the peer's. The session ends with
  the refusal reason.
- A handler fault ends the session with "server could not handle a
  message".
The transport's EndRefusedPeer becomes EndPeerSession(peer, reason). It
reports whether it ended the session, so the log says when a response
that failed after its first fragment had already ended it over the send.
Before, the log reported that it ended a session that was already gone.

The comment said that ending the session matched cultmesh-py. It does
not: Python silently skips a malformed frame. The honest anchor is the
C# TCP schema server, whose ServePeerAsync ends the one peer whose
message fails to decode or handle. The comment now says so.

Pinned, each by a test that kills its mutant:
- a handler fault's goodbye carries the server's reason, not the refusal
  (mutant: the refusal reason);
- the post-fragment-0 path logs that the session had already ended
  (mutant: a constant outcome);
- EndPeerSession says no goodbye to a replaced peer's endpoint (mutant:
  the Ended check dropped);
- a refused peer's frames still queued are never handled (mutant: the
  queue purge dropped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:29 +02:00
Meta Vens
b37f898465 C#: a send failure on an already-ended session says no second goodbye
3b9ffac2 claimed that the generation check subsumed the UnsendableReason
once-guard. It did not. A thread that reads the generation only after
the session has ended carries the ended session's own generation, and the
check lets it through. For example, the polling thread answers a Ping on a
session that a caller's send has just ended but that has not yet been
retired. When that Pong fails permanently, the thread says a second
goodbye and overwrites the first failure's reason (Soul pass 4, PR4).
EndUnsendableSession now also returns when the session has already ended.

Two tests:
- The polling thread answers a Ping on a session already ended, and the
  Pong can never be sent. Exactly one goodbye goes out, and the first
  failure's reason stands. The hook admits the Ping after the session
  ended and before the peer is queued for retirement. The test kills the
  mutant that drops the Ended check.
- A failed send's goodbye is sent under the gate. A polling thread
  admitting a new Connect from the endpoint waits on the gate, so the old
  generation's goodbye never follows the new session's Accept. The test
  kills the mutant that sends the goodbye outside the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:19 +02:00
Meta Vens
9bdf387382 Test: identity defaults, lister dedup, filters by a listed id, store id order
Stryker survivors on 426c956a..fa266e58, each now a behaviour test:

- schemaIdentityOf's defaults (version "<name>.v1", compatible ids holding
  only the own id) are asserted for a name-only definition.
- A lister bound twice is one lister: an unowned id it lists still
  resolves to it rather than being refused as ambiguous.
- A snapshot filtered by an id a binding lists returns that binding's
  records, on the typed and raw paths.
- The ambiguity refusal's record key and message tail are asserted, and a
  raw snapshot record's payload encoding.
- A string-named store document reads the record under its id, and a
  definition's own id wins over an id it lists wherever each record sits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:15:53 +02:00
Meta Vens
ddf24ad1ec cultmesh-rs: rustfmt the canary assertions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:01:21 +02:00
Meta Vens
b810810cf3 cultcache-ts: a record refusal names only a string key; pin link writes for R3
The five-slot refusal ran a non-string key or schema id through String(),
quoting stored bytes. A string is named; anything else is described by its
type.

A push through a live link, and a pushAll through a dangling one, replace
the link with a regular file. Tests named for R3 pin that until R3 decides
whether writes resolve or refuse links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:54:51 +02:00
Meta Vens
549b745ee6 cultcache-py: a record refusal names only a string key; pin link writes for R3
The five-slot refusal echoed a non-string key or schema id with repr, quoting
stored bytes. A string is named; anything else is described by its type.

A push through a live link replaces the link with a regular file. A test
named for R3 pins that until R3 decides whether writes resolve or refuse
links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:54:51 +02:00
Meta Vens
cd93e4d112 cultcache-rs: keyed and document refusals keep no stored value; encode refusals are Rejected
- The redb envelope decode (get by identity and pull_all) and every typed
  document decode (get, get_all, get_all_with_keys, put_envelope, prepare_entry)
  kept serde's text, which quotes stored values. They now keep CultCache's
  context, which names the entry by type and key, and a fixed description of
  the decoder failure's kind.
- A snapshot the store cannot hold (two types claiming one schema id) was
  reported as NotReplaced, so a caller retrying on that kind looped forever.
  It is CultCacheStoreWriteFailedKind::Rejected now, raised before any I/O.
- release_lock and the read-only snapshot state one unlock policy: an unlock
  failure never replaces the result. Only the read-only path, whose API has an
  observer, reports it. release_lock also names the case where closing does not
  release the lock: a duplicate of the open file description, such as a child
  forked without exec.
- README: reads refuse a dangling link, but writes do not follow links yet. A
  write through a live link, and push_all through a dangling one, replace the
  link. Two tests pin that behaviour, named for R3 to decide between resolving
  and refusing. The write example classifies Rejected apart from NotReplaced.
- The shared-gate test waits 30 s and 60 s for the writer instead of 1 s, and
  checks that the file on disk is untouched while the gate is held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:54:05 +02:00
Meta Vens
fa266e5829 CultNet C#: a second type for one schema id is refused, typed
Soul F5. F7 (operator, 2026-09-30): "Schema aliasing sounds like a really
bad idea, I support retiring it." CultNetDocumentRegistry.Register wrote
both maps unconditionally, so the last binding for a schema id won.

- Register refuses a binding whose type already holds another schema id,
  or whose schema id another type holds, with the reference runtime's
  CultSchemaConflictException: the new binding's schema id, both document
  types, an empty record key. Nothing is bound on refusal. Binding a type
  to the schema id it holds again replaces the binding. This matches Rust
  (fc3f3ee5) and Kotlin.
- Tests: both claimant shapes in both orders with two bindings ahead of
  the holder (F6/M11), the first binding intact afterwards, and a same-type
  rebind replacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:58 +02:00
Meta Vens
852ea0fe1a CultMesh TS: a node's returned documents belong to its cache
Soul F8 (M6, M7 survived): node.get, getRequired and put return the
object the cache holds, as C# and Python do (Rust copies). That is the
behaviour that ships.

- The README says the returned object belongs to the cache and must not be
  mutated, and shows the put-a-new-value pattern.
- A test pins it: two reads return the same object, put resolves to that
  object, and the caller's own value is never the one held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:57 +02:00
Meta Vens
091477d066 CultMesh TS: a catalog addresses each document by its id
Soul F7 (P9): a catalog holding two documents of one type answered a typed
lookup with the last one added, and nothing else could name the other. Two
documents of one type under different keys stay legal.

- CultMeshDocumentCatalog.tryDocumentById and documentById look a handle up
  by its document id, the key the catalog already holds handles under
  (it defaults to the record key).
- The publication-catalog test addresses both synced documents by id
  instead of documents[0] and documents[1].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:57 +02:00
Meta Vens
9118e24976 CultMesh TS: a document is read only under an id its type declares
Soul F3 and F4, and F1 in CultMesh's schema descriptor.

- The schema descriptor built from a definition uses schemaIdentityOf, so
  a handle's schema id is the id the cache stamps.
- Store documents (F3): a handle reads the record under its schema id, or,
  for a definition, under an id the definition lists as compatible, as the
  cache resolves it. The catalog-entry name/version match and both parse
  fallbacks are deleted: a record under another id is not this document,
  whatever its payload parses as.
- Peer snapshots (F4): syncDocumentFromPeerSnapshot and definition-named
  peer-snapshot handles take the peer's record at the key under the type's
  schema id only. The shape filter is deleted and no match is not-found.
  P8a (syncing A at k while the peer holds only B at k overwrote the local
  B and then threw) and P8b (a peer's C stored as A) write nothing now.
- HELD: a peer-snapshot handle named by a bare schema-id string still takes
  the first record at the key. Eve Electron reads the C# Aetheria daemon
  that way (EveElectron src/cultmesh-provider-client.mjs:129-142): the
  daemon stamps SHA-256 schema ids and the client asks by
  "gamecult.eve.provider_advertisement.v1", so only the fallback finds its
  records. How a TS reader names a C#-hosted schema is raised as a fork;
  the fallback goes when it is ruled.

Tests: the store- and peer-snapshot tests that asserted shape routing are
replaced by tests of the refusal, including P8a and P8b, and one that pins
the held fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:57 +02:00
Meta Vens
0ee98e8c2e CultNet TS: the registry resolves only ids a definition declares
Soul F1, F2 and F3 on the registry. The F2 principle (operator,
2026-09-30): "CultCache never destroys or relabels a record it doesn't
understand; keeps it byte-for-byte, refuses writes that would overwrite it."

- F1: the registry derives every binding's identity with cultcache-ts's
  schemaIdentityOf, so it stamps, refuses and resolves exactly as the cache
  does. P1 (A with schema name "p1.shared", B with schema id "p1.shared")
  is now refused by both.
- F2a: an id nobody owns resolves to the one binding that lists it as
  compatible. Several listers and no owner is a SchemaConflictError naming
  the id and the listers, whatever order they were registered in; an owner
  beats every lister. This is refused at resolution, as the cache refuses
  it at load, not at registration: refusing at registration would make the
  outcome depend on whether the owner was registered before the listers.
- F2b: version inference is deleted. "p3.note.v7" no longer resolves to
  the type named "p3.note" and gets relabelled "p3.note.v1".
- F2c: a schema version string, and a schema name, are not ids. Snapshot
  filters, typed puts and deletes, and raw puts match only declared ids.
- F3: a raw record under an id no binding declares is refused. The
  registry no longer tries each binding's parser and stores the record
  under the first that accepts it (P5: an identity parser registered first
  took everything).
- The dead resolvePayloadSchemaVersion helper is gone.

Tests: the three shape- and alias-routing tests are replaced by P1, P2,
P3/P4 and P5 as behaviour tests. The one-type-per-schema test registers two
bindings ahead of the holder (F6/M11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:41 +02:00
Meta Vens
426c956a40 CultCache TS: one function derives a definition's schema identity
Soul F1 (hold on 98999997..6e47710e): the cache stamped records with
schemaId ?? schemaName ?? type, while the CultNet registry and CultMesh's
schema descriptor used schemaId ?? type. A definition that set only a
schema name was stamped one id by the cache and bound under another by the
registry, so its records could land on a peer as a different type.

- schemaIdentityOf(definition) returns the schema id, schema name, schema
  version and every id the definition answers to (its own first). It is
  exported, and it is the only place that derivation lives.
- The cache's catalog entry is built from it; nothing else changes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:41 +02:00
Meta Vens
abc847337d cultmesh-rs: the refusal is sent unreliable and unordered
The refusal went reliable and ordered, but the session ends with it, so
it is never resent: a peer missing any earlier reliable packet from the
server held it behind that gap forever. It now goes unreliable and
unordered, so it is delivered on arrival whatever the peer is missing.

The test drops the server's snapshot reply before the refused put: the
refusal still arrives, followed by the goodbye.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:41:35 +02:00
Meta Vens
b3176de1f8 cultmesh-rs: a refusal carries fixed text, never a sink's or source's error
The refusal sent to a rejected peer carried the rejection's Display
text, which for SinkRefused and SnapshotSourceFailed is the sink's or
source's own error. Odin's sink formats the peer-supplied document type
into its error, so a peer's input came back on the wire.

The refusal now carries fixed text per rejection variant
(REFUSAL_TEXTS). The rejection returned from poll_once still carries the
detail for the caller; the server itself logs nothing.

publish_cultnet_message_to_rudp_catalog no longer quotes a catalog's text
unfiltered: it reports "refused the put", quoting the text only when it
is one of the fixed refusals.

The test fixtures' sink and source errors carry a canary; the refused
peer's text is the fixed one and never contains it. A fake catalog that
refuses with a canary of its own shows the publisher's error stays
fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:41:35 +02:00
Meta Vens
ead1a25aff cultmesh-py: RUDP option rules apply only to a RUDP server; the daemon reports a refused limit as a usage error
The RUDP option rules refused a TCP-only server (enable_rudp=False) for
RUDP limits it never uses. Rust checks those options only for the RUDP
server they configure, so Python now checks rudp_resend_delay_ms,
rudp_max_pending_reliable_packets, rudp_max_fragment_bytes and the empty
snapshot's fragment rule only when enable_rudp is true. The snapshot
limits still apply to every server.

The daemon turned --max-snapshot-documents 0 into a traceback. The server
still owns the rule; the daemon reports its refusal through
parser.error: exit status 2 and the rule named, no traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:38:20 +02:00
Meta Vens
699432128b cultmesh-py: a put the store cannot take is refused to its peer, never raised
A well-formed put still killed the RUDP thread: _handle_raw_put called
apply_raw_put_message unguarded, and cache.put_envelope raises for a
blank recordKey, a payload that is not MessagePack or does not decode to
its schema, a global document under another key, or anything a
document's own decoder or validator raises. Over TCP it ended the
connection.

The apply site now refuses any put the store cannot take to the peer
that sent it, as malformed_document_put, over RUDP and TCP alike, and
logs the error's type. Neither the refusal nor the log quotes the error,
which can quote the put. No new shape checks: the store decides what it
can take.

The test sends each shape over RUDP and over TCP and shows the RUDP
thread alive, the TCP connection serving its next message, and other
peers served.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:38:20 +02:00
Meta Vens
d655f7585b cultmesh-ts: the served-bound test comments stop saying admission sizes receipts
Admission has sized only the served reply since a1065cab; three comments
and one fixture assertion still described the receipt bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:36:24 +02:00
Meta Vens
3c9e195b36 cultcache-rs: pin the text a failed write reports
cargo-mutants found no test of what CultCacheStoreWriteFailed says. Both
outcomes now assert the path and the outcome in the error text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:17:40 +02:00
Meta Vens
7472ede480 cultcache-rs: an unlock failure never turns a landed write into a failure
with_shared_lock, with_exclusive_lock, try_with_exclusive_lock and the redb
store's with_database returned the unlock's error in place of the action's
result, so a write that had replaced the store could be reported as failed
and read by Odin as not replaced. Closing the lock file releases the lock
whether or not the explicit unlock succeeds, so all four now release through
one helper that returns the action's result. The unlock failure is not
surfaced anywhere: there is no diagnostic channel on these paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:14:27 +02:00
Meta Vens
9963e43510 Ack map: S-5 still open after ack Cuts 1-1c (probed); a data-loss fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:03:33 +02:00
Meta Vens
1d65755a3b Test: catalog write arguments and the catalog refusal's names and message
Stryker on 43ed9396..3763a53c left 20 survivors, in two places:
- cultMeshCatalogWriteArguments. No test wrote through a catalog's
  replace or submitPrediction, so which argument was the context went
  unobserved. A test now writes through both, in every calling form,
  and checks the context and value that reached the document.
- The catalog refusal's schema names and message. The refusal test now
  checks both names and that the message names both types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:56:46 +02:00
Meta Vens
b25deed129 Error-contract map: record put-serve batch 3 discrepancies (codes, map shape, session end)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:39:48 +02:00
Meta Vens
93ac594513 cultcache-rs: a failed store write says whether it replaced the file
Every single-file write now carries CultCacheStoreWriteFailed in its error
chain, above the cause. NotReplaced: creating, writing, syncing or renaming
the staging file failed, and the store file holds what it held before.
ReplacedNotDurable: the rename landed and the directory sync failed. Odin
could not tell these apart, so it kept every failed snapshot as a candidate.
The text names the path and outcome only.

fsync does not fail on a healthy filesystem, so the staging and directory
sync steps take a fault armed per thread by tests; builds outside tests never
fail there. The staging write (RLIMIT_FSIZE child) and the rename (a
directory at the store path) fail for real.

README: the write outcome, the dangling-link rule and the no-echo rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:37:59 +02:00
Meta Vens
e4e2a0b9d7 cultnet-ts: drop decodeRawDocumentPut, which only receipt admission used
The registry method existed on this branch only to size a receipt at put
admission in cultmesh-ts, which no longer does. It never reached main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:37:41 +02:00
Meta Vens
a1065cabe0 cultmesh-ts: admission sizes only the served reply, never a receipt
Admission sized the larger of the snapshot reply and a receipt echoing
the put, so a put servable at the bound was refused, within 89-125 bytes
of it, for a receipt an application that sends none never sends. Whether
a receipt is sent is onDocumentPutRaw's to decide after it stores the
put, so admission cannot know it (ruling: Self, fork A option a).

Admission now sizes only the snapshot reply serving the document alone.
A receipt that cannot be sent takes the existing path: onError hears why
and the peer is told with a cultnet.error.v0 that it is not coming. This
removes the receipt bound of 9757ed54 and its test.

The new test pins the band at its boundary with puts whose echoed receipt
is over the limit: exactly at the limit is admitted and served, with no
receipt and with an echoed one, and one byte over is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:37:40 +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
853e8db5e5 cultcache-ts: refusals echo a header only in its known shape; a dangling link is not absent
An unsupported header is echoed only as cultcache.store.v<digits>; any other
is described by its byte length (a 2,914-character header was echoed whole).

pullAll's ENOENT catch wrapped the whole decode, including the legacy-repair
write, so a failed repair read as an empty store. It now covers only the read,
and only nothing at the path is absent: a dangling symbolic link is refused
instead of being replaced by the writer. A soft pushAll uses the same read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:35:19 +02:00
Meta Vens
53f6b54e15 cultcache-py: refusals echo a header only in its known shape; a dangling link is not absent
An unsupported header is echoed only as cultcache.store.v<digits>; any other
is described by its byte length, since the bytes are the store's.

A read that found nothing through a dangling symbolic link returned an empty
store, and the writer then replaced the link. Both stores now share one read
rule: only nothing at the path is absent. JsonLinesBackingStore also drops
`Path.exists()`, which reported a symlink loop or a file-as-parent as absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:35:19 +02:00
Meta Vens
c70dbd58be cultcache-rs: the header canary checks for the quoted header, not a prefix
"cultcache.store.v" is a prefix of the "cultcache.store.v1" the refusal names,
so the unquoted check failed on a correct refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:35:19 +02:00
Meta Vens
b7e70bd58c Ack map: record Odin Soul S-5 (pipelined-put ACK before delivery) as a data-loss check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:34:53 +02:00
Meta Vens
0e6e6321c2 CultMesh Kotlin: the reconciliation self-test note gets its own schema
4002a996's refusal caught the self-test itself: the reconciliation block
registered kotlin.reactive_note beside kotlin.reactive_note.ui under one
schema id in one cache. The block tests reconciliation, not two types for one
schema, so its note is now kotlin.reconcile_note under its own schema id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:34:37 +02:00
Meta Vens
53cff052b8 cultcache-rs: refusals never echo store bytes; a dangling link is not absent
A decode refusal kept serde's text, which quotes values read from the file
(`invalid type: string "..."`). The detail is now CultCache's own context and
record refusals, which name only a record's key and schema id, plus a fixed
description of the decoder failure's kind. rmp-serde reports no position, so
none is given. An unsupported header is echoed only in the shape
`cultcache.store.v<digits>`; any other is described by its length.

`is_absent` used `fs::metadata`, which follows links, and a read that found
nothing through a dangling link returned an empty store, so a writer then
renamed a regular file over the link and the store left its volume. Only
nothing at the path is absent now (`symlink_metadata`), and a read through a
dangling link is an io::Error.

Tests: canaries for a catalog and a legacy value and for unshaped headers,
the dangling link on every read with nothing written, and a
`cultcache.storeX` header that claims no format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:31:59 +02:00
Meta Vens
03a344da20 CultMesh TS: a catalog's value type is never inferred from a value
43ed9396's type test caught it: TypeScript inferred the catalog value type
from a written value or an expected result through the conditional's false
branch, so replace and submitPrediction accepted any value and an annotated
collection read took the annotation as its type. The false branch is now
NoInfer, so only a definition or an explicit type argument decides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:08:38 +02:00
Meta Vens
38000efab2 cultmesh-rs: rustfmt the refusal tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:55:30 +02:00
Meta Vens
25b075a473 cultmesh-rs: the rejection test reads the refused peer's acknowledgement before going on
A refused peer now acknowledges its refusal, and that datagram reaches
the server after the session has ended. The test took the next poll to
be the second client's request and failed on the dropped acknowledgement
instead. It drains the server until idle after each refusal is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:52:27 +02:00
Meta Vens
835bee3ae0 cultmesh-py: the daemon refuses a snapshot document limit of zero
The daemon test ran with --max-snapshot-documents 0 to make every
snapshot a peer error. A limit of zero is now refused at startup, as the
Rust document server refuses it, so the test pins what the daemon does
with it: it exits non-zero naming the rule and never writes its ready
file. The limit's enforcement as a peer error stays pinned in process by
test_cultmesh_local_server_rejects_oversized_snapshot_responses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:48:40 +02:00
Meta Vens
9f66852d22 CultNet TS: the registry's refusal message names the schema id
Stryker left one survivor on 9adea366: emptying the refusal message. For a
same-type claimant the message did not name the schema id at all. It now
names the claimant's schema id in every case, and the refusal test checks
that it does. The message is built from document types and the schema id
only; registration carries no record, so no record value can reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:47:55 +02:00
Meta Vens
4002a996b8 CultMesh Kotlin: a cache refuses a second type for one schema id, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." CultCache.register wrote its type and schema maps
unconditionally, and put, get, getAll and delete register implicitly, so one
Kotlin cache could hold two types for one schema and codecForSchema answered
with the last.

- register refuses a codec whose type already holds another schema id, or
  whose schema id another type holds, with the new
  CultSchemaConflictException (schema id, both document types, empty record
  key), the Kotlin counterpart of the other runtimes' typed conflict. The
  implicit registration in put, get, getAll and delete goes through the same
  check. Registering the same type for the same schema again stays allowed.
- Self-test: other-type same-schema and same-type other-schema claimants,
  through register, put, get, getAll and delete, each in a fresh cache; the
  first codec still answers for its schema afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:47:48 +02:00
Meta Vens
fc3f3ee5ff CultNet Rust: a second type for one schema id is refused, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." CultNetDocumentRegistry::register inserted into its
type and schema-id maps unconditionally, so the last binding for a schema id
won, as the TypeScript registry did before 9adea366.

- register returns Result and refuses a binding whose type already holds
  another schema id, or whose schema id another type holds, with
  cultcache_rs::SchemaConflictError: the new binding's schema id, both
  document types (a binding carries no schema name), an empty record key.
  Nothing is bound on refusal. Binding a type to the schema id it holds
  again replaces the binding.
- Call sites propagate the refusal: the cultmesh-rs registry macro, the
  interop peer example, and the tests.
- Tests renamed to what they test: raw snapshot replication hydrates the
  receiver's own type for the schema; a reactive document coalesces direct
  member writes.
- New test: both claimant shapes in both orders, the first binding intact
  afterwards, and a same-type rebind replacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:47:39 +02:00
Meta Vens
d465845516 cultmesh-rs: a rejected peer is told why, and nothing it sent is acknowledged
Every application rejection, DocumentUnservable included, ended with
self.sessions.remove(&key): no refusal and no goodbye. The peer could not
tell a refusal from packet loss, and a publisher timed out and retried
the same put.

The session still ends, but the peer is now sent the refusal as a
cultnet.error.v0 carrying the reason's text (no code or details, as the
TypeScript server's refusals carry none), then the session's goodbye.
The refusal is built before the reset and carries the goodbye's emptied
acknowledgement fields, so it never acknowledges the refused message:
publish_cultnet_message_to_rudp_catalog reads that acknowledgement as
admission. A refusal that cannot be encoded or queued is not sent; the
goodbye still is.

publish_cultnet_message_to_rudp_catalog now returns a catalog's refusal
as an error naming it, instead of waiting out its flush deadline.

Tests read the refusal from the client side: the refusal text, the put
still pending after it, and the goodbye invalidating it; and a publisher
refused by a sink hears the sink's reason well inside its deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:42:44 +02:00
Meta Vens
1a173f6000 cultmesh-py: a put that could never be stored is refused, not raised
_unservable_put sized a put by building the envelope it would store, and
that read document["recordKey"] and document["payload"] directly: a put
over RUDP with no recordKey raised KeyError out of the RUDP loop and
ended the thread for every peer. A string payload raised TypeError the
same way.

_unservable_put now refuses a put whose document lacks a string recordKey
or a binary payload as malformed_document_put, the code the server
already uses for a put with no document map, before any sizing reads it
and before its early return, so the TCP path refuses it too instead of
raising in apply_raw_put_message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:42:43 +02:00
Meta Vens
d04d71e898 cultmesh-py: refuse options that could answer nothing, as Rust does
With one-byte fragments and a reliable queue of 16, even an empty snapshot
can never be queued: every reply vanished while rudp_send_failures
counted, and the peer timed out. Rust refuses such a configuration at
startup in validate_options; Python's __post_init__ accepted it.

__post_init__ now applies Rust's rules to every option the two servers
share, in Rust's order: max_snapshot_bytes, max_snapshot_documents,
rudp_resend_delay_ms, rudp_max_pending_reliable_packets and
rudp_max_fragment_bytes must be greater than zero (the first two when
set; zero was accepted before), max_snapshot_bytes must hold an empty
snapshot response, and an empty snapshot response in fragments of
rudp_max_fragment_bytes, counted rounding up, must fit the lesser of
65535 and the reliable queue. Rust's session, lifetime and payload-budget
rules have no Python option to apply to.

The test measures the empty snapshot the server actually sends, pins each
rule at its boundary, and shows the configuration at the bound does
answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:41:19 +02:00
Meta Vens
3763a53cef CultMesh TS: a catalog refuses a second type for a schema it holds, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." A catalog indexed handles by schema id and by schema
name and version with plain map writes, so a handle of another type for a
held schema silently replaced it for every lookup by schema alone.

- Document and collection catalogs refuse, with the SchemaConflictError the
  registries use, a typed handle whose type differs from the one holding its
  schema id or schema name and version, or whose type already holds another
  schema id. Nothing is indexed when a handle is refused.
- Allowed: handles of one type for one schema (one per record), the same
  handle again, and a handle without a type, which claims none.
- Test: other-type same-id, other-type same-name-and-version and same-type
  other-id claimants, both orders, via add and via construction, for both
  catalogs; the first handle still answers afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:37:32 +02:00
Meta Vens
43ed9396d3 CultMesh TS: a catalog lookup infers its value type from the definition
The deleted parse option was the only thing that typed a catalog lookup, so
every caller had to name the value type. Catalog methods now take the schema
argument as a type parameter: a CultCache document definition yields its own
value type, an explicit type argument still wins, and a bare schema
descriptor yields unknown. CultMeshCatalogValue names that rule.

- Document catalog: tryDocument, document, latest, replace,
  submitPrediction, authoritativeWriter, predictionWriter, watch.
  Collection catalog: tryCollection, collection, latest, watchChanges.
- The two copies of the (value, options) / (context, value) argument split
  collapse into cultMeshCatalogWriteArguments, and the typed lookups share one
  private #require per catalog.
- Tests: the three reads that named their type infer it again. A type-level
  test pins inference for reads, rows, watches, writes and writers with
  @ts-expect-error on wrong assignments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:36:18 +02:00
Meta Vens
e9d5ae2cad cultnet-ts: pin that a fragmented send over one datagram's size is sent
The EMSGSIZE pre-check measures a send's largest datagram, which is one
fragment when maxFragmentBytes is set. Nothing pinned that: a check that
compared the whole payload (Soul's S-TM2) would refuse every large
fragmented send, and large sends are live. A 70,000-byte send with
1,024-byte fragments must not throw and must arrive whole. The test
kills the mutant whose size check ignores maxFragmentBytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:47 +02:00
Meta Vens
a44e189e0c C#: a peer's undecodable message ends that peer's session, never the poll
RudpCultNetSchemaServer decoded each schema frame outside its per-message
backstop, so a malformed payload (0xc1) or a well-formed message with an
unknown schemaVersion threw MessagePackSerializationException out of
PollAvailableAsync and PollOnceAsync. The drain aborted: a good peer's
request in the same drain went unserved, a consumer loop that did not
catch died, and the bad frame was already acknowledged while its peer
stayed connected.

Decoding now runs inside the backstop. A peer whose message cannot be
decoded or handled has its session ended: the transport's new
EndRefusedPeer sends the refusal goodbye under the gate, counts the drop
in PacketsDropped, drops that peer's frames still queued, forgets the
peer and raises PeerDisconnected. A session that already ended (replaced
by a new Connect, or ended by a send failure) gets no goodbye, since the
endpoint may belong to a new session. Other peers in the drain are still
served. Ending the session matches cultmesh-py's server, where one
peer's bad message ends that peer; runtime parity is the reason.

The log names the fault's type only and never its message, because the
decoder's message quotes the peer's input (the unknown schemaVersion).
The dispatch-backstop test now asserts that rule instead of the handler
message it used to require.

Tests, each failing on the pre-fix code: a malformed payload and an
unknown schemaVersion each leave the good peer served without Poll
throwing (also through PollOnceAsync); the bad peer's session ends,
counted and told; the log carries the type and not the canary value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:40 +02:00
Meta Vens
3b9ffac2fe C#: a send failure's goodbye belongs to the generation that sent
Send ended an unsendable session after releasing SessionGate, and the
goodbye was built under the gate but sent outside it. If the polling
thread admitted a new Connect from the same endpoint in that window, the
ended generation's goodbye reached the new client: it saw Data, Accept,
Disconnect and believed itself disconnected while the listener held it
connected.

Every path that finds a permanent send failure now reads the session's
generation under the gate at the send, and EndUnsendableSession builds
and sends the goodbye under the gate only while that generation is
unchanged. The generation check replaces the UnsendableReason once-guard:
a second failure in the same generation finds it already ended.

The test orders the new Connect inside the send, before the fragment
that can never be sent, and asserts the new client hears no Disconnect
and stays connected on both sides. It fails on the pre-fix code
(Data, Accept, Disconnect) and kills the mutant that drops the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:30 +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
6e47710e81 Test: catalog lookups by definition name their value type
The catalogs' parse option went with asSchemaAlias, and with it the only
thing that inferred a catalog lookup's value type. Three test reads now pass
the note type explicitly, as a caller does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:05:11 +02:00
Meta Vens
ff8d89d512 cultcache-ts: an empty store file is refused, not read as empty
An existing zero-byte file is decoded and refused, like any other
undecodable store, matching the Rust and C# runtimes. A missing file stays
empty (ENOENT only), and tests pin that a symlink loop and a file-as-parent
surface as ELOOP and ENOTDIR. `[0x90]` stays an empty legacy store.

Refusal text no longer points at a variant-aware runtime: an unsupported
header names the format found, and a v1 record with a fifth slot says the
file is not a valid store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:04:39 +02:00
Meta Vens
202c4bbd63 cultcache-py: only a missing store reads as empty; an empty file is refused
`Path.exists()` returns False for ELOOP and ENOTDIR, so a symlink loop or a
file where the parent directory should be read as an empty store. The read
now catches FileNotFoundError only. An existing zero-byte file is refused
with ValueError, like any other undecodable store, matching the Rust and C#
runtimes. `[0x90]` stays an empty legacy store.

Refusal text no longer points at a variant-aware runtime: an unsupported
header names the format found, and a v1 record with a fifth slot says the
file is not a valid store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:04:39 +02:00
Meta Vens
271801106b cultcache-rs README: only a missing store is empty; an empty file is Undecodable
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:04:39 +02:00
Meta Vens
6acc414639 cultcache-rs: only a missing store reads as empty; an empty file is not a store
The single-file reads tested `path.exists()` and treated false as an empty
store. `exists()` is also false when the path cannot be inspected, so a
symlink loop, a file where the parent directory should be, or a parent
without permission read as an empty store, and a writer would then replace a
store it never saw. The read now maps only ErrorKind::NotFound to empty;
every other failure stays an io::Error. `pull_all`'s lock-free absence check
gets the same rule.

A store file that exists with zero bytes is Undecodable: no CultCache writer
produces one. `[0x90]` stays an empty store: it is the legacy envelope array
with no envelopes and carries no header, so it claims no format.

Refusal text: an unsupported header names the format found and the one this
runtime reads, and a v1 record with a fifth slot says the file is not a valid
store. Neither suggests a variant-aware runtime; the C# reference refuses the
same v1 bytes.

Tests pin a symlink loop and a file-as-parent on every read, the empty file,
the newer-header truncation boundary (1-19 bytes Undecodable, 20 and up
UnsupportedFormat), the refusal text, and staging cleanup when the rename
fails rather than the write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:02:27 +02:00
Meta Vens
b36b4706fa Parity scope: Rust and Kotlin snapshot hydration is by schema id, not aliasing
F7 retires schema aliasing (one type presented as another for the same
schema). The Rust and Kotlin rows called their snapshot hydration "same-schema
alias" hydration. Both decode a raw record by its schema id into the one type
the receiving runtime registered for that schema; neither has a handle
aliasing API. The rows now say that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:32 +02:00
Meta Vens
2c959b228f CultMesh TS: asSchemaAlias is gone; a catalog finds a typed handle by its type
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." One type owns a schema, so a handle never needs
presenting as another type for the same schema. Parity with C# 1e045502.

- Delete asSchemaAlias from the document, bound document, collection and
  bound collection handles, and cultMeshSchemasAreCompatible, which only it
  and the catalogs' alias step used.
- Document and collection catalogs answer a typed lookup from that type's
  handle or not at all. A descriptor without a type is still looked up by
  schema id, then schema name and version. The parse option on every catalog
  method existed only to convert an aliased value, and is deleted.
- CultMeshNode passes the caller's definition to its cache. The node no
  longer resolves another type's definition by schema id, and no longer
  re-parses values into the requested type, so a type the node did not
  register is refused by the cache. node.get and node.put now return the
  cache's own value, as CultCache.get and put do.
- Tests: the noteAliasDocument fixture is deleted. Tests that used it for
  publication, sync, snapshot, store and prediction paths now use
  noteDocument. otherTypeForNoteSchema stands for another type claiming the
  note schema: catalogs do not find it, and a node refuses it on every path
  and refuses to register it beside noteDocument.
- README stops documenting node aliasing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:17 +02:00
Meta Vens
9adea36680 CultNet TS: 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." CultNetDocumentRegistry.register silently replaced
the binding for a schema id, so two types could claim one schema and the
last one registered won.

- register refuses a binding whose definition claims a type, schema id or
  schema name another bound definition already holds, with the
  SchemaConflictError CultCache uses for the same refusal: the new
  definition's schema id, both schema names, an empty record key.
- Binding the same definition again still replaces its binding, and a
  definition listing another's id as compatible is not a second owner.
- Test: identical-id, same-name and same-type claimants refused in both
  registration orders, leaving the first binding in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:08 +02:00
Meta Vens
016df4626c Merge FEC: own the GF(2^8) Reed-Solomon math; drop reed-solomon-erasure
Operator: "FEC: Please own the math." rs-gf256-v1 implemented in
media_fec.rs, wire-identical to the crate (1,795 geometries, independent
derivation, 6,520 cross-version recoveries, 60k hostile blocks identical).
lru and instant advisories gone; 13 fewer dependencies. combine uses one
product row per coefficient: encode ~1.0-1.4x, decode ~1.2-1.4x the crate.
Follow-up: k^3 inversion makes 255+1 recovery ~124 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:48:18 +02:00
Meta Vens
dc5f43a17a Ack map: correct the Node send-failure claim
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:29:48 +02:00
Meta Vens
71932dbad2 Ack map: pivot to QUIC; remaining ack cuts parked, Cut D lands
Operator: "Yeah, pivot to QUIC adoption, I ain't gonna let sunk costs take the wheel."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:28:32 +02:00
Meta Vens
b7908668f4 cultcache-rs README: how a consumer tells store read failures apart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:21:06 +02:00
Meta Vens
d9d4b32ded cultmesh-rs: connect() takes a server over any snapshot source
The previous commit's tests did not compile: connect() was typed to the
default Source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:20:22 +02:00
Meta Vens
82f976b933 Merge portability onboarding fix: pack the whole public closure; time the headless run from its command
NU1101 since 69bd45ff: pack-nuget.ps1 packed a hand list that missed
GameCult.Math; it now follows ProjectReferences from the public roots.
ubuntu timeout since 2dc7c122: the headless deadline started at connect.
Green on both OSes: run 36727267703 (ubuntu passed twice).
Follow-up (Eve): pack-dotnet-surface.ps1 ignores the version parameters
the verify script passes, so the local feed reuses published versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:19:31 +02:00
Meta Vens
c0ab617e61 cultcache-rs: pin that only a cultcache.store header claims a format
A file that opens like a store but carries some other string where the
header goes is undecodable, not a store in a format this runtime does not
read. cargo-mutants left the guard's `true` mutant alive without this case.

The size-limited child's stdout and stderr now travel with the parent's
failure message, so a child that fails for another reason says which.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:17:45 +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
1e0455028c CultMesh: AsSchemaAlias is gone; a catalog finds a handle by its type
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." One CLR type owns a schema, so a handle never needs
converting to another type for the same schema.

- Delete AsSchemaAlias from the document and collection handles and their
  interfaces, with the MessagePack round-trip converters only it used.
- The document and collection catalogs look a typed handle up by its type.
  The schema-name/version fallback existed only to find a handle of another
  type and alias it.
- Tests: the MeshNoteAliasDocument fixture is deleted. Tests that used it
  for publication, sync, snapshot and prediction paths now use
  MeshNoteDocument; the one test that only checked alias refusal is deleted.
  Catalog tests check a type with no handle is not found.
- public-api.md stops documenting aliasing; a stale comment in
  ReadDocumentFromSnapshotResponse that cited the fixture is trimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:15:48 +02:00
Meta Vens
d54110fe24 cultnet-ts: the send-failure tests send requests without response-only fields
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:14:20 +02:00
Meta Vens
04c0171bcc eve-browser-network: the headless deadline starts at its command
The headless C# client armed a 45 s deadline as soon as its lease was up,
but the orchestrator then runs the whole browser scenario (Chromium launch,
two clicks, provider and Odin replacement, page reconnect) before it sends
INVOKE. On ubuntu runners that scenario alone takes most of the budget: the
last green run finished with little to spare, and since 2dc7c122 the ubuntu
onboarding job has timed out in the headless process either before the
receipt or during the post-receipt benchmark.

The orchestrator already bounds each step it waits on, and closing stdin
ends the headless wait for its command. The headless process now bounds only
its own work: the 45 s deadline covers the invoke, convergence and benchmark
after INVOKE arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:11:41 +02:00
Meta Vens
5ab0f9d337 cultmesh-rs: the at-bound fixture exercises the default served_record
The test Source overrode served_record in both modes, so no test ran the
trait's default and a default that stripped provenance survived. Source
now keeps the default, which the at-bound fixture pins (the record as
received); a separate StrippingSource overrides it for the stripping and
failing-source tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:11:15 +02:00
Meta Vens
31b6c09d64 cultnet-ts: the send-failure tests build operation requests the contract accepts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:08:51 +02:00
Meta Vens
9757ed54a5 cultmesh-ts: a put is refused unless its receipt could be sent
Admission now sizes two replies built from the record the registry
would store: the snapshot reply serving it alone and the receipt
echoing its value under the put's message id with the server's default
provenance, through the one receipt builder the send path uses. A put
whose larger reply exceeds maxPayloadBytes, 65535 fragments or the
reliable queue is refused before onDocumentPutRaw, so a stored put
always gets its receipt when the application stores through the
registry and echoes that value. The existing fallback (a receipt that
cannot be sent all the same reaches onError, and the peer a
cultnet.error.v0) stays for handlers that acknowledge something larger.

The served-bound fixtures carry a long storedAt, which the served record
keeps and the receipt replaces, so they still pin the served bound; a
new test pins the receipt bound: one byte over is refused, exactly at
the limit is stored and receipted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:04:01 +02:00
Meta Vens
6861b55db3 cultnet-ts: the registry decodes the value a raw put would store
CultNetDocumentRegistry.decodeRawDocumentPut returns the binding and
value applyRawDocumentPutMessage would store for a raw put, so a server
can build the receipt that would acknowledge a put before storing it.
It throws, as the put would, when no binding accepts the document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:04:01 +02:00
Meta Vens
73fbb264d6 cultnet-ts: a caller's send larger than a UDP datagram throws EMSGSIZE and takes nothing
Node reports an oversized datagram only to the send's callback, so the
caller's send used to succeed and the session ended later. The transport now
checks the largest datagram of the send against the socket family's UDP
limit (65,507 bytes for IPv4, 65,527 for IPv6) before the session builds
anything: the send throws, nothing is queued, and no sequence is taken, as
the Rust and C# listeners withdraw it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:28 +02:00
Meta Vens
2d76b8d1b6 Pin the reset before a refusal's goodbye (cultmesh-ts, cultmesh-py) and C#'s once-guard
The document servers' refusal tests sent an unreliable fragment, whose
sequence the session never records, so dropping the reset before the goodbye
survived. They now refuse a reliable fragment and check the goodbye does not
acknowledge it.

C#: a BeforeSend seam orders a caller's failing send between the polling
thread's ack and its send, so both threads end the same session; the peer
hears one goodbye.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:27 +02:00
Meta Vens
de8e2e1b88 cultnet-ts: an operation server's send failure belongs to the generation that sent it
A permanent failure reported after that generation ended (the client
connected again from the same endpoint) ended the generation that replaced
it and said goodbye to the new client. The callback now acts only while the
generation that sent the datagram is live, which is the one in `sessions`.

That makes two guards unreachable, so they are deleted: `peer.unsendable`
before the goodbye (the first permanent failure ends the generation, so a
second callback from it is already ignored) and the conditional delete (a
live generation is the entry at its key). A refusal's goodbye is sent
best-effort, as the transport and the document server send theirs, instead
of through the failure handling meant for datagrams a session owes.

Tests: late failure after a reconnect, for the transport's generation check
and for the operation server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:27 +02:00
Meta Vens
ee593f392e cultmesh-rs: the snapshot source says what it would serve for a put
CultMeshRudpSnapshotSource gains served_record: the record the source
would serve for a received put's document. Admission sizes that record,
so a source that serves another shape than it receives (dropping
provenance, or adding its own, as Odin's presence records do) keeps
admission exact by overriding it. The default serves the record as
received, so existing sources, closures included, are unchanged. A
source that fails to answer refuses the put as SnapshotSourceFailed.

Tests: a stripping source admits a put that is over the bound only as
received and serves it at exactly the bound; the existing at-bound
fixture stays as the default-method test; a failing served_record
refuses the put.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:16 +02:00
Meta Vens
9b4167d9ab pack-nuget: pack the ProjectReference closure, not a hand list
Merge 69bd45ff made GameCult.Caching.MessagePack reference CultMath
(PackageId GameCult.Math), so its nuspec depends on GameCult.Math 0.2.4.
The packer's hand-written project list never packed it, and GameCult.Math
is not on nuget.org, so every clean consumer of the local feed failed
restore with NU1101. That is the Eve surface consumer smoke in the
"one-command multiplayer onboarding" jobs, red on both OSes since then.

The packer now walks ProjectReference from the public roots (GameCult.Mesh,
GameCult.Networking.WebSockets) and packs the whole closure. The expected
package names come from MSBuild's PackageId and Version for each project,
since GameCult.Math carries its own identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:08 +02:00
Meta Vens
a288bb037a cultmesh-py: a peer's message that handling cannot serve ends that peer's session, never the RUDP thread
The RUDP loop caught only ValueError, so a subscribe frame with
`schemaIds: 5` raised TypeError in _handle_connection_message and ended the
thread for every peer. Any exception from handling one peer's messages now
ends that peer's session with a goodbye, as a refused packet does, and is
logged by its type alone: the error's message can quote what the peer sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:56:17 +02:00
Meta Vens
25ea551bd5 cultnet-ts: an operation handler's result belongs to the generation its request arrived on
When that generation ends while the handler runs (a goodbye, a refusal, a
timeout, a new Connect from the same endpoint), the response is not sent
and a failure sends no second goodbye: a client that connected again owns
the endpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:54:33 +02:00
Meta Vens
e8e8e5bac1 GameCult.Networking: a Connect that replaces a peer ends the old peer's session under its gate
A caller still holding the replaced peer (a schema handler awaiting work for
it) could send into the endpoint the new session owns: a goodbye after a
partial failure disconnected the new client, a fixed initial sequence
delivered the stale frame in place of the real one, and a drawn one stalled
the new session. The replaced session now ends under the SessionGate every
Send takes, so a stale Send throws and nothing of it leaves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:54:33 +02:00
Meta Vens
1f25195afe cultnet-ts, cultmesh-ts: a send the network refuses is a counted lost datagram, never an "error"
Node reports a failed send only to the send's callback, never on the socket's
"error" event. Emitting it as "error" from the transport ended any process
with no listener (invokeCultNetOperation among them) once per resend tick.
The transport and the document server now count it in sendFailures, as the
operation server, Rust and Python already do. The tests send to
255.255.255.255 with no error listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:51:47 +02:00
Meta Vens
3cc081db0f cultmesh-py: keep the generation test's replies whole; probe the small queue with an empty snapshot
The generation test reads each server packet as one message, so its
server now fragments only above 60000 bytes. The queue test's liveness
probe is an empty snapshot: a hello reply needs more fragments than its
eight-packet queue holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:51:06 +02:00
Meta Vens
2e94dc3aa3 Merge origin/main into hands/ack-cutD: Cut 3 flow window, RUDP client bind
Import lists in cultnet-ts index.ts and operation-service.ts and the
cultnet-py test block conflicted; both sides are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:49:04 +02:00
Meta Vens
36ea08d38d Merge interop Cut 0: the interop lane runs, and fails honestly
The consolidation guard accepts cultnet-rs vendored schema copies (the lane
was red for 40 main runs). Every CI step now fails on any failed command
(job default shell bash -eo pipefail); proven red on a throwaway branch.
The cultnet-py explicit-bind test stays on loopback; the Rust drift guard
lists the vendored directory. CI run 36723314611 green at 59d85557.
Follow-up: CI never runs schema_discovery_contracts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:48:50 +02:00
Meta Vens
36cd169cd6 cultmesh-py: pin the 65535-fragment bound and a reply the queue cannot take
A put over RUDP whose served reply needs 65536 one-byte fragments is
refused and one that needs 65535 is stored. A snapshot of two documents
each admitted at the queue's bound needs twice the queue: it is counted in
rudp_send_failures, not sent, and the thread serves the next request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:48:09 +02:00
Meta Vens
c5de65b875 cultmesh-ts: observe the peer's refusal as the frame it cannot yet parse
A TypeScript peer refuses the two-key cultnet.error.v0 this server sends
(missing routingHint, code and details; docs/cultnet-error-contract-cut.md),
so it arrives as an invalidMessage, never a message. The served-bound rig
records those, the refusal tests count them, and the refusal's text is
asserted through onError, which receives the same error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:47:10 +02:00
Meta Vens
66e439f4c8 cultmesh-rs: the fragment-bound tests pump the client while they wait
A put of 55 fragments leaves the client in flow-window steps, so the
server hears it only while the client reads acknowledgements and resends.
The helper that waits for a refusal or a stored put now pumps the client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:47:09 +02:00
Meta Vens
045f9a21b6 Ack map: Cut D Soul pass 2 result; K1 gains the Kotlin resend loop
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:46:59 +02:00
Meta Vens
dc2a0a7eaa cultcache-rs: a failed single-file write removes its staging file
Any failure after the staging file is created (a short write under a
file-size limit, a full disk, a failed sync or rename) now removes it before
returning the error. A write that failed for lack of space no longer keeps
holding the space it failed on until the next write sweeps it. C# parity with
WriteSnapshotAtomically's finally.

The test runs the write in a child process under RLIMIT_FSIZE with SIGXFSZ
ignored, so the failure is a real EFBIG mid-write, reached without
permissions.

Odin store-recovery map, cut R1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:39:52 +02:00
Meta Vens
c0b56e0674 cultcache-rs: type single-file read failures, read the header only
A single-file store whose bytes were read and not accepted now carries
CultCacheStoreUnreadable { path, kind } in its error chain, with kind
Undecodable or UnsupportedFormat, so a consumer can downcast_ref instead of
matching message text. A failure to read the bytes stays an io::Error and
never carries the type. The flattening map_err that turned every decode
error into a string is gone.

store_header reads only the array header and the first string instead of
decoding the whole array, so a store cut short after its header still names
its format: a truncated v1 store is an undecodable v1 store, not a legacy
decode mismatch, and a truncated newer-format store is UnsupportedFormat.

Odin store-recovery map, cut R1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:39:51 +02:00
Meta Vens
59d8555719 cultnet-rs: the vendored-schema guard lists the directory
The equality test compared only the 22 names in a hand-kept list, so a schema
copied into packages/cultnet-rs/contracts/cultnet/ without joining the list
escaped it. Every *.schema.json under the vendored tree is now found by listing
it and compared against its canonical copy. The presence test is gone: include_str!
already fails the build for an embedded schema that is not vendored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:39:46 +02:00
Meta Vens
83be3bd90f cultmesh-py: RUDP replies go in fragments, and a send failure ends no thread
The local server sent every RUDP reply as one datagram, so a snapshot of a
100 KB put raised EMSGSIZE inside _rudp_loop and ended the RUDP thread for
every peer. Replies are now fragmented by rudp_max_fragment_bytes (default
CULTMESH_RUDP_DEFAULT_MAX_FRAGMENT_BYTES, 1200, as in the Rust and
TypeScript servers), and each peer's reliable queue is bounded by
rudp_max_pending_reliable_packets; serve_node takes both.

A put arriving over RUDP whose served-alone reply would take more
fragments than one message (65535) or the peer's reliable queue can hold
is refused as document_unservable, with fragmentCount and
maxFragmentCount in its details. A datagram that cannot leave, or a reply
the peer's queue cannot take, is counted in rudp_send_failures and never
raised out of the loop.

This overlaps hands/ack-cutD (not yet on main), which classifies send
failures and ends only the affected peer's session; its counter has the
same name, and its classifier should replace the plain count here when it
merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:37:55 +02:00
Meta Vens
6cf19b136a cultmesh-ts: judge a put by what serving it sends, and tell the peer every refusal
The document server refused any delivered frame larger than
maxPayloadBytes, reporting it only to onError: the peer's put, already
acknowledged by the transport, vanished. The frame had been reassembled by
then, so the check protected no memory; it only refused puts whose frame
was over the limit because of provenance or a long message id although
their served reply fit. It is gone. maxPayloadBytes bounds what the server
sends; a put is judged by its served reply, and every refusal reaches the
peer as a cultnet.error.v0.

A stored put whose receipt cannot be sent (a receipt from onDocumentPutRaw
larger than maxPayloadBytes) now tells the peer so with a cultnet.error.v0,
as well as onError. The unservable refusal names sizes only, never the
record key, so a long key cannot make the refusal itself too large to send.

The options document states that the served-size judgement is exact only
for an application that stores through the registry it was given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:37:54 +02:00
Meta Vens
b1d7d31f59 cultmesh-rs: the snapshot source keeps owning the served record's provenance
The previous commit cleared source provenance from every served record.
Odin's snapshot source serves runtime-presence records with provenance it
authors itself (source_runtime_id, source_agent_id, source_role and an
odin-observed tag), and a crate test pins that a raw response is served as
the source returned it. The server does not own that shape, so it no
longer rewrites it, and admission again sizes the record as received.
Which rule a Rust server should size by is reported as a fork.

The fragment-count admission, the construction cross-check and the
exact payload-budget fixture stand. The budget fixture's put carries a
longer message id so its frame, the exact budget, is at least its
served-alone response. Polling for a refusal sleeps when the server is
idle, so a put of many fragments has time to arrive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:37:34 +02:00
Meta Vens
cc624e934d Ack map: Cut 3 merged (1210fdc7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:36:26 +02:00
Meta Vens
1210fdc7ae Merge ack Cut 3: the 4 MiB send bound counts every byte above the lowest unacknowledged packet
F1: acknowledged sequences above a gap still count until the lowest
advances, in Rust, TS, Python and C#. Rust direct send purges expired
packets before the window check. Soul passes: F1 held; batch 2 held with
13 own mutants killed and interop 27/27 (ack1d-interop:2). Only docs
differ from the verified tree. Follow-up: cultmesh-rs Cargo.lock stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:36:10 +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
b62bfc7c8c cultnet-py: the explicit-bind test stays on loopback
The test sent from an explicit 127.0.0.1 bind to a non-loopback address, which
Windows refuses outright (WinError 10051), so it only held on Linux. It now
binds 127.0.0.2 for a 127.0.0.1 endpoint, whose derived bind is 127.0.0.1, and
checks the source address the listener sees. That still fails if the explicit
host is ignored, on every platform, with no dependence on a non-loopback route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:32:32 +02:00
Meta Vens
52abce9191 Interop workflow: every command in a step counts
The Windows runner's default pwsh does not stop when a native command fails, so
a multi-command step reported only its last command: a failing cultnet-py suite
went green inside "Verify Python RUDP reliability". The job now defaults to Git
Bash, which the runner starts with -e -o pipefail. The pwsh consolidation guard
checks git ls-files' exit code instead of treating a failed listing as clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:32:28 +02:00
Meta Vens
2204ce23e9 TypeScript: a definition listing an id registers beside the one owning it; registration refuses typed
Owner beats lister, as in C# and Python. The cache kept one definition per id,
own or compatible, and refused a second with a plain Error, so a v1 definition
could not stay registered beside a v2 declaring v1's id (Soul F8, cult-cache.ts
registration). Now a definition owns its schema id and any number may list it:
a record resolves to the owner, else to the one lister. Several listers and no
owner name no single definition, and the load is refused with
SchemaConflictError naming the id, the record key and the listers.

Every registration refusal is now SchemaConflictError (Soul F10): a second owner
of one id, a second definition with one schema name, and a different definition
for one type. Each names the new definition's id and both schema names.
SchemaConflictError takes an optional message for these uses.

Tests: an owner and a lister register in both orders and each reads its own
records, and a lister alone reads the owner's id; two listers register in both
orders, a record under their id is refused typed without its owner and is the
owner's with it, registered before or after them; each of the three refusals is
a SchemaConflictError in both registration orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:31:31 +02:00
Meta Vens
84246b685a Python: a document listing an id registers beside the one owning it; registration refuses typed
Owner beats lister, as in C#. A document owns its schema id; others may list it
as compatible beside its owner, in either registration order (batch 9 refused
any overlap, which forbade keeping v1 beside a v2 that declares v1's id; Soul
F8). A record resolves to the document owning its id, else to the one document
listing it. Several listers and no owner name no single document, and the load
is refused with SchemaConflictError naming the id, the record key and the
listers: refusing at registration would depend on whether the owner had
registered yet.

Every registration refusal is now SchemaConflictError (Soul F10): a second owner
of one schema id, a second document with one schema name, and a second
definition of one document type. Each names the new document's id and both
schema names and document types. Before, the last two raised CultCacheError.

Tests: an owner and a lister register in both orders and each reads its own
records, and a lister alone reads the owner's id; two listers register in both
orders, a record under their id is refused typed without its owner and is the
owner's with it, registered before or after them; two owners, one schema name
and one document type twice are each refused as a schema conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:31:25 +02:00
Meta Vens
9118be879d cultmesh-rs: admission sizes the record as served, in the fragments it is sent in
A served record carries no source provenance: the server now clears
source_runtime_id, source_agent_id, source_role and tags from every record
it sends in a snapshot response, as the TypeScript and Python servers
already serve only what their stores keep. Put admission sizes that served
record, so provenance that makes a put larger only in its received form no
longer refuses it; the sink still receives the record as it was sent.

Admission also counts the response's fragments: a put whose served-alone
response would take more fragments of max_fragment_bytes than
max_pending_reliable_packets_per_session or 65535 is refused with
DocumentUnservable, which now names the fragment count and its limit.
Construction refuses options that could not carry an empty snapshot
response, in bytes or in fragments.

The payload-budget fixture sets both budgets to exactly the put's frame, so
a charge equal to a limit must fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:28:55 +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
6d0cc96316 Variants map: F7 ruled, retire AsSchemaAlias
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:25:19 +02:00
Meta Vens
c8e3ef917e Error contract map: Q1 B, Q2 B, Q3 A, Q8 A ruled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:20:03 +02:00
Meta Vens
beb7c46162 Interop guard: accept cultnet-rs's vendored schema copies
The consolidation guard rejected every *.schema.json outside contracts/cultnet/,
including the R-AB byte copies vendored into packages/cultnet-rs/contracts/cultnet/.
The interop workflow has been red at that step on main ever since, so no later
cross-runtime step ran. The guard now accepts that one path; drift of those copies
is judged by the Rust equality test in tests/schema_discovery_contracts.rs.
The retired-namespace check is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:17:58 +02:00
Meta Vens
50f834e457 Map the CultNet error contract cut
The cultnet.error.v0 contract is not decodable across runtimes, and the
interop CI lane has been red for 40 main runs. Cut 0 unblocks the lane;
Q1, Q2, Q3 and Q8 must be ruled before Cut 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:15:20 +02:00
Meta Vens
d7a98a2763 Variants map: C2a F2 ruled B (carry unclaimed records untouched); batch 9 Soul result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:12:27 +02:00
Meta Vens
cce4d391b8 Ack Cut D D2 (C#): only the polling thread changes the peer set
A caller-directed Send that failed after a fragment left called
EndUnsendablePeer on the caller's thread, removing from _peers and raising
PeerDisconnected while the polling thread could be changing _peers too.
Send now ends the session and sends the goodbye under the peer's
SessionGate, and queues the peer; TryReceiveOnce and PollResends retire
queued peers before anything else. A peer a new Connect already replaced
is skipped, so a stale peer object cannot remove the peer that replaced it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:22 +02:00
Meta Vens
b96420b606 Ack Cut D (Rust): pin the receive reset, the ack after an unsendable reply, and a refusal at every fragment
- Document server: a reset reported by a receive is idle, not an error
  (Windows only, where the system reports it; run on Starfire).
- Document server: no ack follows a reply that can never be sent. The
  guard is not decoration: without it the ack's success overwrote the
  failure and the session that owed the Pong was kept.
- Hub: a send refused at any fragment index delivers nothing of it; at
  index 0 the next send arrives whole (Soul's probe, committed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:16 +02:00
Meta Vens
06f8a31cb5 Ack Cut D (Python): a packet one peer's session refuses ends that session, never the RUDP thread
session.receive raises ValueError on bad fragment metadata, and nothing
in _rudp_loop caught it, so one admitted peer could end the thread for
every peer. The server now ends that peer's session with "session refused
a packet", as the socket transport and the Rust servers do.

Tests pin that, and two D2 rules that were unpinned: a resend loop sends
nothing more to a peer it just ended (P1), and responses after one that
can never be sent are not sent (P3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:11 +02:00
Meta Vens
cc750d4e4c Ack Cut D D2 (cultmesh-ts): the document server's sends are guarded, classified, and a refused packet ends only its peer
Every socket send in the RUDP document server goes through
sendRudpDatagram. A Connect from UDP port 0 used to store a session whose
pending Accept threw from the resend interval on every tick, an
uncaughtException that ends the process. Now:

- a Connect whose Accept can never be sent starts no session and leaves a
  session already at that endpoint as it was;
- a datagram that can never be sent as built ends only that peer's session
  (goodbye, onSessionClosed); any other failure is a lost datagram,
  reported through onError, and ends nothing;
- a packet the session refuses (bad fragment metadata) ends that peer's
  session with "session refused a packet" instead of leaving a session
  that has recorded a sequence it never delivered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:05 +02:00
Meta Vens
a44e0199ac Ack Cut D D2 (cultnet-ts): a send Node refuses never escapes a timer or a receive handler
Node reports a failed UDP send two ways: it throws for an argument it
refuses (port 0, which any spoofer can claim as a source) and passes a
system failure to the callback later. sendRudpDatagram routes both to one
failure handler, and isPermanentSendError classifies with the permanent set
the other runtimes use (EMSGSIZE, EINVAL, EAFNOSUPPORT, and Node's own
ERR_SOCKET_BAD_PORT for port 0).

- The socket transport's receive and resend paths hand a failure to that
  handler: a datagram that can never be sent ends the session (goodbye
  names the error code, never its message); any other failure is emitted
  on "error" as the socket always reported it. Caller-directed sends still
  throw to the caller. A resend loop stops at the generation it ended.
- The operation server starts no session for a Connect whose Accept can
  never be sent (counted as dropped), ends a stored peer whose datagram
  can never be sent, and counts other failures in sendFailures.
- CultNetRudpSession.endUnsendableSession, as in the other runtimes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:01 +02:00
Meta Vens
7c1a792a28 Media FEC: one table lookup per shard byte in combine
Soul measured the owned math at 1.7-2.6x the crate's time at realistic sizes
(16+4 x 1200 B encode 64k -> 122k ns), because combine called gf_mul per byte:
two zero checks and three lookups. combine now tabulates each coefficient's
256 products once and spends one lookup and one XOR per byte; coefficient 0
is skipped and 1 is a plain XOR. The construction and every wire byte are
unchanged: the known-answer fixture passes untouched.

tests/media_fec_bench.rs is an ignored, dependency-free timing test over the
public surface (4+2 at 160 and 1300 B, 16+4 at 1200 B, 255+1 at 1300 B):

    cargo test --release --test media_fec_bench -- --ignored --nocapture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:53:53 +02:00
Meta Vens
b4cee00af2 Ack Cut 3: a server's new generation does not inherit acknowledged bytes
A server accepting a fresh Connect can send before its Accept is
acknowledged, so no ack of the new generation prunes the acknowledged
sizes; only `end_session` clearing them keeps the ended generation's bytes
from counting against the new window. Rust, TypeScript, Python and C# each
pin it: 4 MiB acknowledged above a lost packet, a fresh Connect, and the
server's first 1 MiB send of the new generation goes out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:46:38 +02:00
Meta Vens
1833df58d4 cultnet-rs: a direct send measures the window after reclaiming expired sends
`send` checked `window_admits` before `send_many` purged deadline-passed
packets, so the first direct send after the lowest unacked packet expired
was refused for acknowledged bytes that `send_many` would already have
released. `send` now purges first, as `send_many` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:46:20 +02:00
Meta Vens
93f1a50895 Merge origin/main into hands/ack-cut3: Cut 3 and D Soul rulings, RUDP client bind
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:44:10 +02:00
Meta Vens
a2e0748ab5 cultmesh-ts: the second at-bound put fits the inbound limit
Its ten-character message id made its own frame one byte over
maxPayloadBytes, so the inbound cap refused it before admission was
asked. The id is now as short as the first put's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:29:30 +02:00
Meta Vens
8fc74c7094 Merge RUDP client bind: a client for a remote endpoint binds the unspecified address
A client with no bind host binds loopback for a loopback endpoint and the
unspecified address of the endpoint family otherwise, in Rust, TS, Python,
Kotlin and C#. On Windows a loopback-bound socket could not send to a remote
address (os error 10051), which kept eureka-state from reaching Huginn.
Soul: all suites green at e079ba1a; Windows send reproduced on Starfire.
Follow-ups recorded: Python/TS IPv6 peer filter, localhost resolution
parity, 127.0.0.2 loopback tests in Rust/C#/Kotlin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:26:37 +02:00
Meta Vens
4b668002aa cultmesh-ts: pin each served-bound limit with a fixture that can tell the rules apart
The queue-bound puts carry a source runtime id, which the registry does not
store, so sizing the record as received instead of as served is caught.
The test states the served record's shape itself rather than asking the
registry, so a registry that served a different record is caught too. A
two-document snapshot over maxPayloadBytes proves a reply that cannot be
sent reaches onError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:15:49 +02:00
Meta Vens
0eb7434ab1 cultmesh-ts: the RUDP document server fragments its replies and refuses unservable puts
The server reassembled fragmented puts but sent every reply as one UDP
datagram, ignoring its own maxFragmentBytes and maxPayloadBytes, and a
datagram that failed to leave vanished. An 80 KB note put in 1 KiB
fragments was admitted and its snapshot never arrived, with no error.

Replies now fragment by maxFragmentBytes (default
CULTMESH_RUDP_DOCUMENT_SERVER_DEFAULT_MAX_FRAGMENT_BYTES, 1200), and
maxPayloadBytes bounds both inbound frames and replies. A put whose
served-alone reply (sized by the registry, under the shortest message id,
with the send path's encoder) would exceed maxPayloadBytes, 65535
fragments or maxPendingReliablePackets is refused before onDocumentPutRaw:
onError receives a CultMeshRudpUnservableDocumentError naming the sizes,
and the peer a cultnet.error.v0. Every datagram send reports failure to
onError, as does a snapshot reply that cannot be sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:09:50 +02:00
Meta Vens
2cf1e07bac cultnet-ts: the registry sizes the reply that would serve a raw put
CultNetDocumentRegistry.createRawSnapshotResponseForPut(put, messageId)
returns the snapshot response that would serve the put's document alone:
the envelope applyRawDocumentPutMessage stores, as createRawSnapshotResponse
serves it. A server can judge a put by the size it would be served at.

applyRawDocumentPutMessage and the new method share one envelope builder,
and the served record comes from the snapshot path's own record builder,
so the size and the stored record cannot drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:09:50 +02:00
Meta Vens
e079ba1ad3 cultmesh-kotlin: the bind self-test observes a wildcard, not its family
A JVM wildcard socket is dual-stack, so a client bound to 0.0.0.0 reports the
IPv6 wildcard as its local address and the self-test's first check failed on
Linux. Assert that a client for a remote endpoint binds a wildcard, and keep the
exact loopback checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:52:26 +02:00
Meta Vens
e6ddcdd6e1 GameCult.Mesh: an RUDP client binds by its endpoint, as the other runtimes do
CultMeshRudpSocketOptions.BindHost defaulted to 0.0.0.0 for servers and
clients alike (a58daa54, so clients could reach remote routes). That bound an
IPv4 socket for every client, so a client for an IPv6 endpoint could not send,
and a client for a loopback endpoint listened on every interface.

BindHost is now nullable and unset by default. Unset, a client binds loopback
for a loopback endpoint and the unspecified address of the endpoint's family
(0.0.0.0 or ::) otherwise, matching cultnet-rs, cultnet-ts, cultnet-py and
cultmesh-kotlin. A server keeps binding 0.0.0.0. An explicit BindHost wins for
both. The test that pinned the old default's value is replaced by tests of
what each endpoint binds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:50:58 +02:00
Meta Vens
cf62461643 cultmesh-kotlin: an RUDP client for a remote endpoint binds the unspecified address
The CultMesh RUDP client entry points defaulted bindHost to 127.0.0.1. On
Windows a socket bound to loopback cannot send off the host (os error 10051),
so a default client could not reach a remote endpoint.

cultNetRudpClientBindAddress owns the rule: loopback for a loopback endpoint,
otherwise the unspecified address of the endpoint's family (0.0.0.0 or ::).
bindHost defaults to null on every client entry point, which uses the rule. An
explicit bindHost still wins; a CultMesh server still binds 127.0.0.1 unless
told otherwise. The self-test pins all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:49:31 +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
9954652266 cultnet-py, cultmesh-py: an RUDP client for a remote endpoint binds the unspecified address
create_rudp_schema_transport and CultMesh.create_rudp_client (and create_client
and connect_client above them) defaulted bind_host to 127.0.0.1 on an AF_INET
socket. On Windows a socket bound to loopback cannot send off the host (os
error 10051), so a default client could not reach a remote endpoint.

cultnet-py now owns the rule in rudp_client_bind_host: loopback for a loopback
endpoint, otherwise the unspecified address of the endpoint's family (0.0.0.0
or ::). bind_host defaults to None on the client paths, which use the rule; the
socket family follows the bind host. An explicit bind_host still wins; a
CultMesh server still binds 127.0.0.1 unless told otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:44:06 +02:00
Meta Vens
b68ddfe357 cultnet-ts, cultmesh-ts: an RUDP client for a remote endpoint binds the unspecified address
CultMesh.createRudpClient defaulted bindHost to 127.0.0.1 on a udp4 socket, and
invokeCultNetOperation bound 127.0.0.1 unconditionally. On Windows a socket
bound to loopback cannot send off the host (os error 10051), so neither could
reach a remote endpoint.

cultnet-ts now owns the rule in rudpClientBindHost: loopback for a loopback
endpoint, otherwise the unspecified address of the endpoint's family (0.0.0.0
or ::). The operation client and the CultMesh client both use it, and the
CultMesh socket family follows the bind host. An explicit bindHost still wins;
a CultMesh server still binds 127.0.0.1 unless told otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:41:44 +02:00
Meta Vens
fd394d54db cultmesh-py: refuse a put the local server could never serve
With max_snapshot_bytes set, CultMeshLocalServer stored any raw put while
refusing every snapshot response over that size, so it could hold a
document no request would ever return.

_handle_raw_put now asks the database for the snapshot response that would
serve the document alone, built from the envelope the put would store (the
resolved local schema id, not the put's alias) under an empty message id,
the shortest this server answers. It is measured with the snapshot path's
own encoder. Over the bound the put is refused before it is stored, as
cultnet.error.v0 code document_unservable with responseBytes and
maxSnapshotBytes. At the bound it is stored and served at exactly that size.

The database's put and snapshot paths share one envelope builder and one
served-record builder, so the preview and the stored record cannot drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:41:34 +02:00
Meta Vens
88d2d72405 cultnet-rs: an RUDP client for a remote endpoint binds the unspecified address
CultMeshRudpSocketOptions defaulted bind_host to 127.0.0.1 for servers and
clients alike. On Windows a socket bound to loopback cannot send off the host
(os error 10051), so a default client could never reach a remote endpoint such
as rudp://10.77.0.1:17872.

bind_host is now Option<String>. Unset, a server still binds 127.0.0.1; a client
binds loopback for a loopback endpoint and the unspecified address of the
endpoint's family (0.0.0.0 or ::) otherwise. An explicit bind_host wins for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:37:21 +02:00
Meta Vens
027bd8aa47 cultmesh-rs: size an admitted put under the shortest message id CultNet encodes
The served-size check built its response under an empty message id, which
the CultNet encoder refuses, so every put was rejected as
ResponseEncodingFailed. No snapshot request can carry an id shorter than
one character, so a one-character id gives the smallest response that could
ever serve the document; that is the size a put is now judged by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:33:16 +02:00
Meta Vens
85288a5a47 cultmesh-rs: refuse a put the document server could never serve
The server admitted a put up to the 4 MiB per-session budget but serves at
most max_snapshot_response_bytes (1 MiB by default), so it could accept a
document no snapshot request would ever return.

DocumentPutRaw now sizes the snapshot response that carries the document
alone under an empty message id, the smallest response that could serve
it, with the snapshot path's own encoder. Over max_snapshot_response_bytes
it is refused as DocumentUnservable, naming both sizes, before the sink
sees it. At the bound it is admitted and served at exactly that size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:20:55 +02:00
Meta Vens
c9cdad127e cultmesh-rs: an application rejection carries a typed reason
CultMeshRudpApplicationRejection.reason was a String, so a consumer could
only log a refusal, never act on its cause. It is now
CultMeshRudpRejectionReason, one variant per refusal the document server
makes, with sizes and limits as fields. Display keeps the existing
sentences, so `{}` logging reads the same.

The snapshot path encodes its response through one helper, which put
admission will share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:20:41 +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
aab72f3d47 Ack Cut D D2: a refused send withdraws the fragments the flow window queued behind the failed one
Rust hub and C# listener: 1,100 one-byte fragments, the first can never be
sent, and nothing of the send stays queued.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:49:46 +02:00
Meta Vens
abb922cd57 Python: one registered document carries each schema id; a second claimant is refused typed
register_document_type refuses a document whose own or compatible ids overlap
another registered document's, raising SchemaConflictError (empty record key,
both schema names, a message naming both document types), as TypeScript does.
Before, the record resolved to whichever document the dict held first (Soul F4).
With one carrier per id, resolution by id is a single pass. SchemaConflictError
takes an optional message for this use.

The test that pinned owner-over-lister resolution inside one cache becomes a
refusal test in both orders; shared compatible ids and a shared own id (neither
declaring its own) are refused in both orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:52 +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
590744f6f1 Ack Cut D D2 (C#): the oversized-send test compares the outstanding count with the one the listener's own unacknowledged Accept holds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:30:25 +02:00
Meta Vens
69acee38e7 Ack Cut D D2 (Python): a refused Accept leaves the peer with the goodbye only
The peer is told its session ended, and no Accept resend follows it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:27:33 +02:00
Meta Vens
d391770abb Media FEC: own the rs-gf256-v1 math, drop reed-solomon-erasure
Operator ruling, 2026-09-30: "FEC: Please own the math."

reed-solomon-erasure 6.0.0 pulled lru 0.7.8 (RUSTSEC-2026-0253, unsound) and
instant (RUSTSEC-2024-0384, unmaintained) into every cultnet-rs consumer.
media_fec.rs now implements the construction its docs already state, read
against the crate's source rather than its names: GF(2^8) reduced by 0x11D
with 2 as generator, the n x k Vandermonde matrix over the evaluation points
0..n-1 with 0^0 = 1, E = V * inverse(top k rows), parity = rows k..k+m of E,
and recovery through the inverse of E's rows for the first k present shards,
which is the row choice the crate made.

About 130 lines: const log/exp tables, a Gauss-Jordan inverse, the encoding
rows, and one combine loop. It allocates only the block's shards and three
k x k matrices, so the decoder's bound is still set by the wire validators
(<= 256 shards of <= 64 KiB). The shard functions no longer return errors
that only the crate's own checks could raise; every caller already bounds k,
m and the shard length.

The generator test is deleted with the crate; the fixture keeps its output and
names the commit that ran it. New: a seeded property test over 300 random
(k <= 48, m <= 24) geometries, any e <= m erasures recover exactly and m + 1
are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:26:40 +02:00
Meta Vens
d866cd5916 Media FEC: pin rs-gf256-v1 to 35 known-answer cases from the reference crate
tests/fixtures/media_fec_rs_gf256_v1.kat holds what reed-solomon-erasure 6.0.0
produced at 73bf4421: the parity bytes of every standard video geometry
(k = 1..=16), standard audio 4+2 at 1, 37 and 1300 bytes, a 4000-byte shard,
all-zero and all-0xff data, and the policy's edges (1+1, 2+1, 255+1, 1+255,
2+254, 128+128, 200+56, 250+6), plus 120 erasure patterns mixing data and
parity. Its header defines the data, so another runtime can meet it.

Two tests drive the codec's public audio path, which takes any (k, m): every
case's parity matches byte for byte, and every pattern recovers the erased data
from the fixture's own parity bytes. Both pass against the crate here, before
it is replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:25:30 +02:00
Meta Vens
ac5c86033b Ack Cut D D2: pin the disconnect, the resend loop, the Python resend, ack and accept paths
The injected permanent failure now takes precedence over the injected transient
one, so a test can end a session on a peer that is also losing datagrams and
count exactly which datagrams were attempted afterwards. New tests: a hub
disconnect that can never be sent returns the error; a peer or session ended
in the resend loop is sent nothing more (Rust hub, document server, C#); the
goodbye reaches the peer; the Python server ends the session on a permanent
failure in a resend, an ack and an accept. The oversized-send test compares the
outstanding count with the one the hub's own unacknowledged Accept already
holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:25:04 +02:00
Meta Vens
73bf44212e Media FEC: a generator for rs-gf256-v1 known answers from the reference crate
Before reed-solomon-erasure is replaced by an owned GF(2^8) construction, it
produces the vectors the owned code must meet. The ignored test encodes every
standard video geometry (k = 1..=16), standard audio 4+2 at 1, 37 and 1300
bytes, a 4000-byte shard, zero and 0xff data, and the policy's edges (1+1,
255+1, 1+255, 2+254, 128+128, 200+56, 250+6), and reconstructs four erasure
patterns per case, asserting the crate returns the original data.

It is run once and deleted with the crate; git keeps its provenance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:22:33 +02:00
Meta Vens
1c71d98c71 Ack Cut D D1 and D2 (Python): a vanished peer is idle, and a send that can never succeed ends only that peer's session
D1: on Windows the closed port of a vanished peer is reported by the next
recvfrom as ConnectionResetError (WinError 10054), and the RUDP thread returned
on it. It is now idle, as in the Rust document server, the Rust hub and the C#
listener. Both tests fail on Windows without the fix and pass with it.

D2: is_permanent_send_error (EMSGSIZE, EINVAL, EAFNOSUPPORT and their Winsock
equivalents) separates a datagram that can never be sent as built from a lost
one. The server ends that peer's session with a goodbye whose reason names the
error and never the thread; transient failures stay counted losses.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:19:47 +02:00
Meta Vens
6cf92329b6 Ack Cut D D2 (C#): a send that can never succeed throws from a caller-directed send and ends only that peer's session inside a poll
The listener swallowed every send failure as a counted loss. A send now splits
into permanent (MessageSize, InvalidArgument, AddressFamilyNotSupported: the
datagram can never be sent as built) and transient (everything else, still a
counted loss). SendSchema throws the SocketException and queues nothing, with
the packets it created withdrawn and their sequences reissued; a permanent
failure after an earlier fragment left ends the session. Inside a poll the
failure ends that peer's session, PeerDisconnected carries a reason naming the
error, and the poll is never failed. Tests pin each path, the classification,
BytesSent after a healthy send and a zero failure count.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:19:46 +02:00
Meta Vens
64ed64a894 Ack Cut D D2 (Rust): a send that can never succeed is the caller's error inside a send and ends only that peer's session inside a poll
The hub and the document server swallowed every send failure as a counted
loss, so a 70,000-byte reliable frame returned Ok and was resent forever.
Failures now split in two. Permanent means the datagram can never be sent as
built (EMSGSIZE, EINVAL, EAFNOSUPPORT and their Winsock equivalents,
is_permanent_send_error in cultnet-rs); everything else stays a counted loss.
A caller-directed hub send returns the error and queues nothing: the packets
it created are withdrawn and their sequences reissued, so the peer never sees
a gap (a permanent failure after an earlier fragment left ends the session).
Inside a poll the failure ends that peer's session with a goodbye whose reason
names the error, reported to the hub caller as Disconnected, and never fails
the poll. The document server ends the session the same way and reports a
snapshot response it can never send as an application rejection. Tests pin
each path, the classification per OS, BytesSent after a healthy send and a zero
failure count for healthy sends.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:19:46 +02:00
Meta Vens
436b423762 Ack Cut 3 F1: the acknowledged-bytes tests pin pruning on the ack that closes the gap
The bytes counted above a lost packet stop counting once it is acknowledged;
every runtime now asserts that another MiB is admitted after the gap closes.
Rust prunes on expiry only when something expired, so the acknowledgement path
owns the pruning it can observe.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:12:23 +02:00
Meta Vens
0daf6f4cc0 Ack Cut 3 F1: Rust pins that an expired lowest packet releases the acknowledged bytes above it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:07:28 +02:00
Meta Vens
d7eacd1011 Ack Cut 3 F1: the 4 MiB flow bound counts acknowledged bytes above the lowest unacknowledged sequence
The bound summed only packets still awaiting an ack. Once frames above a lost
packet were acknowledged they stopped counting, so a receiver could be made to
hold more than 4 MiB behind one loss, and its hold buffer ended the session.
The sender now keeps the payload size of each acknowledged sequence above the
lowest pending one until the lowest advances past it, in Rust, TypeScript,
Python and C#. Tests per runtime: one lost packet, four acknowledged 1 MiB
frames, the fifth refused until the gap closes; and the floor moving to a new
lowest sequence.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:04:19 +02:00
Meta Vens
42ba0f082a Ack map: Cuts 3 and D Soul rulings; record the stalled-sender liveness gap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:02:32 +02:00
Meta Vens
c99f29c309 Test: the selection guard probe passes the descriptor's new compatible-ids argument
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:48:38 +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
da0315e69e Test: TypeScript writes a record under an older schema id, C# declares it and rewrites it
The C# interop note declares a compatible id and the peer gains a rewrite mode; the TypeScript
cross-runtime test has TS write a record under that id and another schema name, C# open and
rewrite it, and C#, TypeScript, Rust and Python open the result.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:32:47 +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
2125635dff Ack Cut D: the document-server send-failure tests take initial_sequence as an Option
Ack Cut 1c made CultNetRudpSocketTransportOptions.initial_sequence optional; the
new client helper still passed a bare u32.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:26:51 +02:00
Meta Vens
5926804e2b Python test: a document that owns an id is resolved before one registered earlier that lists it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:14:38 +02:00
Meta Vens
1a04b0d788 Rust: a registered type owns its schema id, so only it may name an id over records the file holds
The previous cut let any supplied type name a schema id over the arrived one, which let a raw
envelope of another type retype the records under an id (the dynamic-payload test refused it and
failed). A rename under a stable id is a write of the registered type that owns the id; a Rust
registered type's id is its entry type, so a supplied envelope whose type is its schema id names
the id and any other type under an occupied id is refused, typed, with the file left as it was.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:10:06 +02:00
Meta Vens
0996fc96f5 Ack Cut 3: the hold-buffer test acknowledges the gap by hand
Cut 1's the_hold_buffer_takes_exactly_1024_frames test held 1,024 frames behind a
sequence the receiver never sees, by sending them all above one unacked packet.
Cut 3's flow window stops a sender 1,023 sequences above its lowest unacked packet,
so the test's sender was refused at frame 1,023. The missing packet is now
acknowledged by hand (the receiver never saw it), so the window slides and what
the test probes is the receiver's hold buffer, not the sender.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:07:30 +02:00
Meta Vens
86dc2e33d0 Contract: the cache owns the schema-id stamp, records resolve by schema id, writes may not retype
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:16 +02:00
Meta Vens
e6f9b2f8c1 TypeScript: the type check runs across tiers; a renamed stable id reads across runtimes
The writer refuses a write that would change which type an existing record resolves to, unless
the entries of both the existing records and the written ones own the id (a rename; the registered
descriptor wins). Tests pin the arrived-overwrites-registered guard in both id orders, the
content-hash tie-break against the compatible-list order, sorted id processing and the cross-tier
refusal; a cross-runtime test has TS write a schema renamed under a stable id and TS, Rust and
Python open it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:14 +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
6f219bd919 Rust: a record resolves by schema id, and the registered type names a renamed schema id
pull_all_backing_stores resolves a record to a registered type by its schema id (a registered
type's id is its entry type) before the catalog name is tried. The writer tells the entries the
file already holds from the ones its caller supplies, and a supplied type names the schema id over
an arrived one, so a stable-id rename is not a conflict; two types of one tier under one id still
refuse the write.

Tests: a stable-id rename write, a renamed schema opening by id, and an id not overruled by a name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:11 +02:00
Meta Vens
ebe15ab8c9 Python: a record resolves to its document by schema id, and the type check runs across tiers
A record now resolves to the document that owns its schema id, else one that lists it as
compatible, before the catalog name is tried; a schema renamed under a stable id opens, and an id
that names one document is not overruled by a name that names another. The name resolves a record
only when no local document has the id.

The writer refuses a write that would change which type an existing record resolves to, unless the
entries of both the existing records and the written ones own the id (a rename, the registered
descriptor wins).

Tests pin the arrived-overwrites-registered guard in both id orders, the content-hash tie-break
against the compatible-list order, sorted id processing, the cross-tier refusal and the rename, and
a cache whose document does not list the id a loaded record sits under.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:07 +02:00
Meta Vens
b8c00117b3 Merge origin/main into hands/ack-cutD 2026-09-30 10:58:23 +02:00
Meta Vens
ede87f7423 Ack Cut 3: the flow-window tests shake hands the way Cut 1 requires
An Accept now counts only while the session's own Connect awaits it, and reliable
data is refused until the handshake seeds the watermark, so the tests' bare Accept
no longer connected the session. The helpers now pair a sender and a receiver
through CreateConnect/AcceptConnect in all four runtimes; the sender's Connect
is acknowledged, so nothing is pending on it when the tests begin. Rust also
takes initial_sequence as an Option.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:57:53 +02:00
Meta Vens
8c71b5c6dd Merge origin/main into hands/ack-cut3
# Conflicts:
#	packages/cultnet-ts/src/rudp.ts
2026-09-30 10:52:49 +02:00
Meta Vens
29385586bd Ack map: Cuts 1, 1b and 1c merged (05b5705d); record the residuals
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:52:07 +02:00
Meta Vens
05b5705d69 Merge ack Cuts 1, 1b and 1c: one watermark, the write belongs to its session, a new generation is recognised
Cut 1: one watermark owns ordered delivery, seeded only by the handshake.
Cut 1b: every ending drops what the session still owed; flush is bound
to its generation. Cut 1c: a Connect repeats only by its sequence;
stale data and stale Connects are handled as TCP does (challenge Ack);
an unanswered attempt retries at +window-1; the session draws its
initial sequence. Wire bytes unchanged in all four runtimes. Five Soul
passes. Public option types changed: initial_sequence is optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:28:05 +02:00
Meta Vens
acbf031c5b Tests: the arrived entries' hashes sort before the registered one's, so a writer that ignores the tiers picks the arrived one
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:16:53 +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
7a5b64ad88 TypeScript and Python writers: records are compared for type within a tier only
A read-back record's type is its schema's name and a supplied one's is the document type, so records of different tiers are never
compared; two supplied (or two arrived) records of different types under one id still refuse the write.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:13:44 +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
e77246b657 Writers derive the catalog from the records they write; the merging is deleted
Every writer (C# single-file and directory, TypeScript, Python; Rust already did) chooses, for each schema id a record carries, one
entry that publishes it: the entry that owns the id (a registered descriptor, else an arrived entry), else one that lists it as
compatible. A registered descriptor wins over an arrived entry with the same own id. Entries are written as chosen, with no union of
compatible ids and no dependence on arrival order; an entry no record needs is not written. Two entries of one tier that share an own id
and disagree on the schema name, records of different types under one id, or a record no chosen entry publishes refuse the write with a
typed error (CultSchemaConflictException, SchemaConflictError) naming the id, the names and a record key, and the store is left as it was.
CultSchemaCatalogEntry.MergeById and the Python and TypeScript merge paths are gone. A cache-held record unchanged since the file was read
is rewritten under its registered id (C# single-file and directory), so an entry that lists an older id is not needed to publish it.
Changelogs for cultnet-py (removed export) and cultcache-py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:03:34 +02:00
Meta Vens
4da8bbe291 Ack Cut 1d batch 2 (C#): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:06 +02:00
Meta Vens
c45edf5858 Ack Cut 1d batch 2 (Python): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:06 +02:00
Meta Vens
9c562adaf2 Ack Cut 1d batch 2 (TypeScript): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

The cultmesh-ts document server has a test that it does not acknowledge by name a reliable packet its session did not receive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:06 +02:00
Meta Vens
7b46b4c481 Ack Cut 1d batch 2 (Rust): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:05 +02:00
Meta Vens
f58604a40d Ack map: connect-attempt rulings; record the receive-window divergence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:45:21 +02:00
Meta Vens
1569f013c6 Ack Cut 1d (C#): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

An accepted peer now stamps its liveness on accept, so one that goes silent times out.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:52 +02:00
Meta Vens
e56c96d6be Ack Cut 1d (Python): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:51 +02:00
Meta Vens
ba873d04c4 Ack Cut 1d (TypeScript): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

The cultmesh-ts document server acknowledges what the session received, not every reliable packet by name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:51 +02:00
Meta Vens
e02d027019 Ack Cut 1d (Rust): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:51 +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
29e60353f0 Merge media FEC Cut 3: one Reed-Solomon codec for audio and video
rs-gf256-v1 (construction documented, independent implementation matches
every KAT); audio parity with checked block arithmetic; every encoded
record must fit the producer's datagram budget (default 1431), and no
media record exceeds 64 KiB on encode, decode, send or reassembly. The
"audio" channel is retired. Muninn and Ratatoskr bump in lockstep at
Cuts 4-5. Three Soul passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:05: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
c12f6df0ea Ack map: stale-data and stale-Connect rulings after the Cut 1c Soul pass
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:46:56 +02:00
Meta Vens
c8326ce3dc Vector: the entry that owns the id does not list itself, so the reader order is observable
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:31:12 +02:00
Meta Vens
4d93dc858e Media wire tests: hold an unfragmented media packet to the ceiling
Mutation run showed the unfragmented receive path of the RUDP session was
undefended (only fragmented reassembly was tested).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:29:05 +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
df892e40e3 Ack Cut 1c tests (cultmesh-rs): the budget test sets a snapshot limit that fits its session budget
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:20:26 +02:00
Meta Vens
51967aff68 Contract: an id resolves to its owner first, and writers merge entries that share an id
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:19:16 +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
7c149bf0c8 Media wire tests: fix helper name clash and move fixtures to the last-pts deadline rule
The sized parity helpers collided with the existing fixtures of the same name,
so they are now sized_audio_parity and sized_video_parity. Two existing cases
encoded the old base-pts deadline rule: the corruption table expects the new
last pts_ticks message, the edge-of-bound cases admit a deadline at the last
pts, and the 254-shard audio case sets a deadline that covers its last slot.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:14:22 +02:00
Meta Vens
5b42eb0250 Merge publication Cut 4: CultNetDatabase rebuilt on the cache journal
Kind, prediction and log are decided in the journal under the cache gate;
the reorder buffer, drainer and prediction gate are gone. Q-P5 A: one
typed CultNetShardLogException per journal call, after publication. The
four old log bugs are fixed. No shard owns a schema means no write on any
path (typed refusal, wire code unowned_schema) and no row served; replicas
relay skipped entries unchanged. Four Soul passes; follow-ups recorded in
docs/cultcache-publication-cut.md (crash-safe log store is a hard
prerequisite before any live daemon enables durable shard logs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:13:38 +02:00
Meta Vens
1bbc0ace69 Media wire: the encoder refuses a record over the ceiling too
A producer that builds an oversized record without going through the FEC
policy would otherwise learn only when the receiver silently dropped it.
encode_media_wire_record now refuses anything over
GAMECULT_MEDIA_MAX_WIRE_BYTES, with the decoder's message. The decode ceiling
test pads a valid record by one byte to stand in for a peer that sent one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:11:26 +02:00
Meta Vens
51e9a550f9 Media wire: bound every record by one ceiling, and validate audio parity against its last pts
Removing the 720-byte shard cap left the decoder with no per-record size
bound: RUDP reassembles up to 16 MiB and the media channel set no cap, so a
4 MiB parity record decoded and recovery work scaled with it.

GAMECULT_MEDIA_MAX_WIRE_BYTES (64 KiB, one maximum UDP datagram, ~7x a jumbo
frame) is now the ceiling. decode_media_wire_record refuses a larger record,
the RUDP session caps the media channel to it on send and on reassembly (and
the profile advertises it), and MediaFecPolicy::validate refuses a
max_wire_bytes above it so no producer can promise a record the decoder drops.

validate_audio_parity_record now checks the deadline against the last slot's
pts, not the base, so recovery can no longer produce a packet that violates
deadline_ticks >= pts_ticks.

Tests: ceiling +-1 at decode for audio and video parity; jumbo record accepted;
session send and reassembly on the media channel only; profile advertisement;
the audio deadline boundary and recovery refusal; a policy over the ceiling
refused; a budget at the ceiling end to end; audio parity larger than every
packet it protects (the budget check relies on it). MediaFecPolicy docs tell
callers to build by struct update from STANDARD.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:10:07 +02:00
Meta Vens
3f942a57e7 Cut 4 Soul 4d: a skipped entry is relayed as the primary sent it; probes, wire cases and the closed-enum rule committed
F1: RecordUnappliedEntry keeps the primary's wire entry on the in-memory log
entry (internal Relayed) and ToLogEntryMessage returns it through the same
normalisation the store uses, so a replica with no store no longer relays a
skipped put as a delete; memory and disk relay identically.
Soul's 4d probes are committed (chained replica keeps the row, key-prefixed
reads serve only owned keys, primary-all serves everything; the two
measurements are explicit) with a memory/disk relay-parity test.
Rust: captured-bytes cases for unowned_schema and variant_unsupported (with and
without a subscription).
The error-code set is stated as a closed enum in the schema description (both
copies) and in cultnet-distributed-database.md: adding a code is one
coordinated change across runtimes, since Rust and TypeScript refuse an unknown
code. The Soul 4c probe file's header says what it guards.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:03:09 +02:00
Meta Vens
c5063ab3c4 Publication map: record Cut 4 final-Soul follow-ups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:59:05 +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
c08d982947 Vectors: the malformed current-layout catalogs carry one entry and only its records
The older-layout reader refused the second current-layout entry on its own, so an unconditional fallback still refused the vector.
With one entry, only a store not in the older layout is refused for its malformed slot, and the fallback's condition is pinned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:32:18 +02:00
Meta Vens
ebb8a468dd Ack map: record the unpinned schema/latest channel arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:28:56 +02:00
Meta Vens
784dc7d614 CultMesh: the untyped same-schema fallback reads through the database, which serves only what its shards own
The string ResolveShard check was order-dependent in the full Mesh suite (schema id of a same-schema alias pair); GetAsync<object> uses the descriptor path and adds no helper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:28:30 +02:00
Meta Vens
27edda82ea Python and TypeScript: tests for the store's publishing rule and the admission stamp; cultnet-py keeps its payload-based resolution
cultnet-py resolves a raw record whose schema id no document lists by the schema version its payload opens with, and three
cultmesh-py tests (foreign UI-generated ids) depend on it, so that resolution is restored pending a ruling. The compatible-id
replication test goes through the real apply_raw_document_record and the reopened store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:28:17 +02:00
Meta Vens
f66fd3d714 Ack Cut 1c tests (cultmesh-rs): the restarted client's message is the same size as the first
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:03:27 +02:00
Meta Vens
29eaabc4ff Dispose test: hold the writer in an earlier observer so Dispose must publish what it logged
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:03:15 +02:00
Meta Vens
2647f10d20 C#: a rewrite keeps the catalog entry that publishes a record's schema as a compatible id
The single-file and directory writers dropped an entry whose own id no record carried, leaving a record under a compatible id with
no catalog entry that publishes it: the same store nothing could open. An entry now stays while it publishes any id a record
carries. The conditional-commit readability test over compatible-id-only-v3 pins it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:02:06 +02:00
Meta Vens
b9ce601844 Python: a record is stamped with its registered schema id and the catalog publishes it; cultnet no longer infers a schema from a payload
put_envelope, put_envelopes and load stamp the registered schema's id, and the store writer publishes every record's id, so a
replicated record under a compatible id no longer leaves a store nothing can open. cultnet-py resolves a raw record by the schema
ids its documents list and nothing else.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:18 +02:00
Meta Vens
39a9de3b23 TypeScript: a record is stamped with its registered schema id and the catalog publishes it; the inspector reads what the reader reads
putEnvelope and admission stamp the registered schema's id, and the store writer publishes every record's id (the entry the
envelope carries when it lists that id, else a default), so a replicated record under a compatible id no longer leaves a store
nothing can open. cultnet-ts stops preserving the incoming id. The inspector refuses a store of the wrong slot count and a record
whose schema the catalog does not publish, instead of recovering one from the payload.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:18 +02:00
Meta Vens
c6f87fd3bf Rust: compatible schema ids publish a schema, and the refusal's message carries its cause
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:17 +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
0abb8d4380 Ack Cut 1c: a new generation drops what the old one owed before the queue is asked for room
A bounded session whose queue still held an unacknowledged Accept for a peer
that vanished refused the next Connect; the old server-mode path reset before
accepting, so this would have been a regression. accept_connect and
create_connect now end the generation first in every runtime, and a test per
runtime holds a one-slot queue to it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:50:23 +02:00
Meta Vens
b90fdc45aa Pin the zero-field policy refusal to the validator, not to a downstream failure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:46:14 +02:00
Meta Vens
a179e9c7c6 Ack Cut 1c tests (cultmesh-rs): a restarted client starts with a fresh payload budget
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:40:06 +02:00
Meta Vens
86c170fa0b Ack Cut 1c tests: a reconnect forgets what the old server sent; an Accept for an abandoned Connect does not connect; the cultmesh-py server test observes what the server sent
Two rules the first mutation pass found undefended in every runtime: a new
Connect on a session that had a peer clears the old peer's received window (a
restarted server's sequences overlap the old ones), and ending a session
abandons its pending Connect. The cultmesh-py server test now counts the
server's own data packets by sequence, so a peer replaced on a retransmitted
Connect is seen, and asserts a restarted client inherits no subscription.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:34:23 +02:00
Meta Vens
6e406879be Tests: the Mesh same-schema read serves only owned rows; the unowned put answer must try to send
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:23:09 +02:00
Meta Vens
79f144d9ec CultNetDatabase.Dispose publishes what a writer logged and had not yet published
The dispose test showed journal-first alone still lost the change: Dispose won the race against the writer's publication and cleared the stash. Dispose now drains the stash through the publisher after the journal is disposed (so nothing is stashed afterwards) and before the subject completes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:16:57 +02:00
Meta Vens
9d0701d4a7 Ack Cut 1c tests (cultmesh-ts): the document server replaces a restarted client and keeps a retransmitted Connect's session
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:15:12 +02:00
Meta Vens
557e70fd03 Cut 4 Soul 4c: committed-unlogged wire writes, database serves only what it owns, typed unowned refusal
Q1: HandlePutAsync/HandleDeleteAsync catch a CultNetShardLogException (or an
AggregateException holding one) before the generic catch and answer nothing: the
write committed and was published, so the peer is not told it was refused. It is
logged at warning level. The wire has no committed-with-degraded-log message.
Q2: GetAsync/GetAll/GetByName/GetByIndex, the shard-less snapshot, the selection
and the subscription snapshot serve only rows a shard owns, so read and watch
agree; the CultMesh collections follow through GetAll, and the Mesh untyped
variant fallback checks ownership. The raw cache still holds everything.
Q3: a replica log apply skips a row its shard does not own, as the snapshot path
does, and still records the entry so a chained replica sees no gap.
API: rowFilter is internal (public CreateSelectionResponse and
CreateRawSnapshotResponse signatures restored, internal overloads added);
exception tags on the write methods; Commit's unwrap rule is XML doc; the
unowned refusal answers on the wire with code unowned_schema (schema enum in both
copies, Rust CultNetErrorCode, CultNetErrorMessage.ForUnownedSchema) and put and
delete log it at warning level.
Dispose keeps journal-first; a test now fails if the observer goes first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:14:49 +02:00
Meta Vens
396cccafd4 Soul probes: commit the pass 4c probes (Q1-Q5) as recovered from 9c30f67f
The Finding-category probes fail on this commit; the fixes that follow are their tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:09:14 +02:00
Meta Vens
8d204a9473 Merge remote-tracking branch 'origin/main' into hands/pub-cut4 2026-09-30 06:08:26 +02:00
Meta Vens
7969d4dbf4 Ack Cut 1c tests (C#): the sequences that follow the Connect
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:03:17 +02:00
Meta Vens
d5bc28e207 Vectors: manifest row for missing-schema-versioned-v3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:02:24 +02:00
Meta Vens
ed61462a70 Tests: the recovery tests assert the refusal
Rust, TypeScript and Python each had a test that a record missing its catalog entry is recovered from its payload; each now asserts
the store is unreadable.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:58:47 +02:00
Meta Vens
a39d04e53e Ack Cut 1c tests (C#): sessions that stood in for a handshake with a hand-built Accept now handshake
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:58:30 +02:00
Meta Vens
45f98c5638 Ack Cut 1c tests (Python): the timeout probe uses a clock past the epoch clock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:58:29 +02:00
Meta Vens
8c950c06ea Ack Cut 1c tests (TypeScript): the request follows the Connect in sequence
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:57:31 +02:00
Meta Vens
acce92d406 Ack Cut 1c tests (Python): sessions that stood in for a handshake with a hand-built Accept now handshake
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:56:00 +02:00
Meta Vens
50d6eac405 Ack Cut 1c tests (TypeScript): sessions that stood in for a handshake with a hand-built Accept now handshake
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:55:39 +02:00
Meta Vens
02fa6ab854 Ack Cut 1c tests (cultmesh-py): the server keeps a peer on a retransmitted Connect and admits a restarted client
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:53:09 +02:00
Meta Vens
3a3de56341 Store readers: a schema is never recovered from a record's payload
The persistence contract requires the catalog to publish every schema its records name and specifies no recovery, so the catalog
is the store's own description: Rust, TypeScript and Python stop inferring a schema name from a payload's first field, as C#
never did, and a store naming an unpublished schema is unreadable in all four. missing-schema-versioned-v3 pins it with payloads
that open with a schema version.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:53:01 +02:00
Meta Vens
9741bfe2fa Ack Cut 1c (TypeScript): export randomInitialSequence from the package index
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:52:42 +02:00
Meta Vens
536e650d82 Ack Cut 1c tests (C#, Python): Connect generation, handshake-seeded watermark, restarts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:52:37 +02:00
Meta Vens
64eb3bd4b2 Ack Cut 1c (TypeScript): export randomInitialSequence; the repeated Connect test names acceptConnect
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:52:37 +02:00
Meta Vens
5ff1ee3222 Ack Cut 1c (C#): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
The session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. AcceptConnect answers both, ConnectRepeats is the predicate for servers
that keep one session per peer, and AnswerRepeatedConnect is private. Server
mode admits a Connect from a new endpoint and the session decides; the listener
no longer compares payloads (the payload stays application evidence).

CreateConnect on a session that has had a peer starts a new generation. An
Accept counts only while the Connect is pending and only if it names it. The
watermark is seeded by the Connect or Accept alone; reliable data before that
is neither remembered, acknowledged nor delivered. The default initial sequence
is random in [1, 2^31) in every options type. An ICMP connection reset no
longer sets DisconnectReason: nothing ended. The interop peer flushes before it
disconnects.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:51:14 +02:00
Meta Vens
2e56b46656 Ack Cut 1c (Python): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
The session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. accept_connect answers both, connect_repeats is the predicate for servers
that keep one session per peer, and answer_repeated_connect is private.
Server-mode transports admit a Connect from a new endpoint and the session
decides; the cultmesh-py server replaces a peer only when the Connect does not
repeat, so a retransmit no longer resets its subscriptions.

create_connect on a session that has had a peer starts a new generation. An
Accept counts only while the Connect is pending and only if it names it. The
watermark is seeded by the Connect or Accept alone; reliable data before that
is neither remembered, acknowledged nor delivered. The default initial sequence
is random in [1, 2^31) wherever a default is produced, and a timeout names
itself as the disconnect reason.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:51:14 +02:00
Meta Vens
efefbe270a Ack Cut 1c (TypeScript): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
The session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. acceptConnect answers both, connectRepeats is the predicate for servers
that keep one session per peer, and answerRepeatedConnect is private. The
server-mode transport no longer computes its own repeat; the cultmesh-ts
document server no longer compares payloads (the payload stays application
evidence).

createConnect on a session that has had a peer starts a new generation. An
Accept counts only while the Connect is pending and only if it names it. The
watermark is seeded by the Connect or Accept alone; reliable data before that
is neither remembered, acknowledged nor delivered, and createAckForReceived
acknowledges by name only what was received. The default initial sequence is
random in [1, 2^31).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:51:13 +02:00
Meta Vens
86909c1d1e Ack Cut 1c tests (TypeScript): Connect generation, handshake-seeded watermark, restarts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:49:46 +02:00
Meta Vens
40c670d652 Ack Cut 1c tests (Rust): read past another client's acks; a repeat's Accept was already acknowledged
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:48:32 +02:00
Meta Vens
450ec33352 Ack Cut 1c tests (Rust): Connect generation, handshake-seeded watermark, receipts, boundaries
Session and transport tests for the rules the change makes: a reconnect on the
same session and a server that accepts a new client are not stranded by what an
ended session owed (Soul F1); a retransmitted Connect after data has flowed does
not reset; a Connect with another sequence is not a repeat whatever it carries;
an Accept counts only when it names the pending Connect, and not after a local
disconnect (F6); reliable data before the Accept is neither delivered nor
acknowledged (F2); the default initial sequences are random; the hub, server
mode and the cultmesh-rs document server admit a restarted client; a receipt
asked of another transport reads Invalidated (F4); a timed-out flush names the
timeout; the hold buffer takes exactly 1024 frames and 64 channels (F5).

Existing tests that handshook with a hand-built Accept now use assume_connected
or a real handshake, and pin distinct sequences for peers that must differ.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:42:25 +02:00
Meta Vens
f7f8b17b39 Ack Cut 1c (Rust) tests: the repeated Connect test names accept_connect
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:35:17 +02:00
Meta Vens
a0007f3aeb Ack Cut 1c (Rust): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
A session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. accept_connect answers both cases, connect_repeats is the predicate for
servers that keep one session per peer, and answer_repeated_connect is gone.
create_connect on a session that has had a peer starts a new generation.

An Accept is honoured only while this side's Connect is pending and only if it
names that Connect. The watermark is seeded by the Connect or Accept alone;
reliable data before that is neither remembered, acknowledged nor delivered.
create_ack_for_received acknowledges by name only what was received.

The default initial sequence is random in [1, 2^31). The hub, the cultmesh-rs
document server and the server-mode transport no longer compare payloads or
reset on every Connect. A receipt carries its session's random id. A timeout
names itself as the disconnect reason.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:32:40 +02:00
Meta Vens
dac712b09c CultNetDatabaseServer: a selection skips cached rows no shard owns instead of failing
ResolveShard now throws for an unowned schema, so one such cached row would
have failed the whole selection. The server passes a row filter
(CultNetDatabase.Owns) to CreateSelectionResponse, which gains an optional
rowFilter that SelectPage already took, so those rows are not part of what the
database serves and the rest is answered.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:29:35 +02:00
Meta Vens
a014bf1b5b Ack map: 1b merges only with 1c; R is seeded by the handshake (Cut 1 Soul)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 05:28:27 +02:00
Meta Vens
e79882a8ec CultNetDatabase: no shard owns it, no write, on every path; the fallback shard is deleted
ResolveShardInternal throws CultNetUnownedSchemaException instead of falling
back to the first shard. PutAsync, DeleteAsync, PutPredictedAsync,
ApplyPutAsync and ApplyDeleteAsync refuse an unowned schema before the cache is
touched; a remote put reaches the server's generic catch and is answered as an
application rejection like any other refused remote put. ResolveShard throws
(documented), CanWriteAuthoritatively returns false, and a bare cache write no
shard owns is neither logged nor published. The unused string overload of
EnsureClientAuthority is deleted. No GameCult consumer configures shards
outside vendored CultLib copies; the default shard owns everything.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:31:03 +02:00
Meta Vens
91d464a038 Rust test: a refused store's cause is the error's source
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:23:02 +02:00
Meta Vens
e0075217b7 Networking test: the outside-the-shard fixture is written to the cache, since the database refuses it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:21:48 +02:00
Meta Vens
be5ba22264 Vectors: the variant renames itself and the legacy catalog publishes every schema it holds
variant-slot-v3 overrides the variant's name, which the reader requires of a variant, so C# reads it and the others refuse the
fifth slot. The legacy-layout catalog vectors carry an entry per schema, so a store the reader accepts differs from the one it
refuses only by the bytes after it. missing-schema-v3 is refused by C# too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:21:23 +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
fdafa15022 Python: StoreUnreadableError for a store file it cannot read; a store is exactly three slots
The refusal is a ValueError subclass with the cause chained. A store with more than three slots is refused instead of read.
The readability test walks every vector in the folder, asserts the error type and cause, the header a replaced file carries
and what it holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:04 +02:00
Meta Vens
c001358e8c TypeScript: a flush asks the same full reader as an open, and refuses with StoreUnreadableError
pushAll, push and delete read the file through the one store reader that pullAll uses, so a store holding a variant or a bad
body is not replaced by a header-only check. A store with fewer or more than three slots is refused. The readability test
walks every vector in the folder, asserts the error type and cause, the header a replaced file carries and what it holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:03 +02:00
Meta Vens
ed7fd14c1c Rust: a rewrite asks the same full reader as an open, and refuses with StoreUnreadableError
push_all and every rewrite read the file through the one store reader (framing, header, schema catalog, records), so a v3
store holding a variant or a bad body is no longer replaced by a header-only check. The refusal is one typed error with the
cause as its source. The readability test walks every vector in the folder, asserts the error type and the header a replaced
file carries, and the truncated v1 and v2 stores are refused again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:03 +02:00
Meta Vens
cd07f03a9f Mesh: the older-catalog reader takes only a store the current catalog layout cannot decode
A store refused for its framing, slot count or a record keeps that refusal instead of being re-read with the older layout, so
the record it names is the record the message names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:02 +02:00
Meta Vens
9f6b2d4081 CultNetDatabase: a write no shard owns is refused, not cached and never logged
PutAsync and DeleteAsync carry no door, so the journal cannot place an unowned
schema and now skips it; accepting the write anyway was a silent loss of
replication. They resolve their shard strictly and throw
CultNetUnownedSchemaException (an InvalidOperationException naming the schema)
before the cache is touched. Doors keep the fallback shard they were resolved
to. No GameCult consumer configures shards outside vendored CultLib copies:
AetheriaEve's node uses the default PrimaryAll shard, which owns everything.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:08:45 +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
51a6a6c1c1 Media channel test: observe expiry through the next send, where the session reclaims packets
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:51:45 +02:00
Meta Vens
f3076264fc Journal tests: a plain put of an unowned schema is unlogged, a door put keeps the fallback shard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:51:14 +02:00
Meta Vens
a813c09b6d Media FEC: enforce the datagram invariant at the producer, refuse overflowing audio blocks
The 720-byte shard cap stood in for "an encoded record fits one datagram" and
failed it at legal maximum-width identifiers. The invariant is now checked where
records are produced: protect_video_frame and protect_audio_block take the
producer's provenance, encode each record, and refuse the block when one exceeds
MediaFecPolicy::max_wire_bytes (default 1431, the IPv4/1500 MTU budget; smaller
paths pass less). The fixed cap is deleted; it contradicted a larger budget and
bounded nothing the decoder does not already hold in memory.

Audio block arithmetic lives in one function, audio_slot, used by the protect
path, the wire validator and recovery. The validator refuses a parity record
whose last packet id or pts overflows; recovery uses the same checked function
instead of adding.

Tests pin the policy 256-shard bounds, block identity on block_count and
block_data_start, the k=16 m=4 known answer, and the burst property
(worst always-recovering burst = blocks x smallest m for 1..=399 chunks), and
the wire behaviour of the media channel profile (delivery and expiry). Docs
state the rs-gf256-v1 construction and that audio playout gates on pts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:50:11 +02:00
Meta Vens
f28ca3a89f CultNetBurnedSequence: a plain struct, since netstandard2.1 has no IsExternalInit
The record struct in 2eb0e8d6 did not compile for the library's netstandard2.1
target.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:49:50 +02:00
Meta Vens
a6720b620f Merge Geometry references as CultRecordRef<T>
DomainKey, RequestKey and CutKey were plain strings marked [CultReference];
since e420410f (D11) the cache refuses a reference it cannot walk, and
every cache that registered the geometry catalog threw (7 tests red since
2026-09-22). The wire is unchanged. Geometry gains a reference to
GameCult.Caching.MessagePack for the formatter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:49:38 +02:00
Meta Vens
0dc3b91730 Merge CultPhasor caching support: converter and formatter
The Phacelle merge (15c6a7e2) added a public CultMath value type without
the Caching support every such type has; three Caching tests went red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:49:37 +02:00
Meta Vens
2eb0e8d6a6 Journal fixes: hole floor on restart, one log exception per call, no shape guard, no stray shard-0 logging
F1: InitializeLogSequencesFromStore raises the in-memory floor to the highest
gap above the store's compaction point, so a restart after a double refusal or
an interrupted compaction answers compacted_history instead of a gapped log.
F2: the journal raises one CultNetShardLogException per call carrying every
burned (shard, sequence); the unwrap rule for a cache-aggregated hold is
documented on the exception and on the cache commit.
F3: TakeDoor no longer refuses a door whose instance is the cached one, so
mutate-then-predict publishes Predicted and is not logged.
F6: a door-less change no shard matches is not logged; doors keep the
fallback shard they were resolved to.
Dispose disposes the journal before the observer. The log store doc says a
slow store blocks cache readers. Soul F4 is ignored as an accepted follow-up,
M1 is explicit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:48:49 +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
ae0f545a06 Readability vectors: a legacy array with bytes after it
Rust's read path let a legacy envelope array with trailing bytes through when the
whole-array check was loosened, and no vector caught it. Add the vector; every
runtime refuses it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:39:29 +02:00
Meta Vens
49582cf43e Soul probes: commit the pass-3 P-tests and the Cut 4 probes as recovered
SoulPub3Tests.cs is from 1ccc4927 (recovered pass-3 P-tests); SoulP4ProbeTests.cs
is from 8ae8b3dc. Committed as Soul wrote them; the Finding-category probes fail
on this commit and are the tests for the fixes that follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:38:50 +02:00
Meta Vens
6a87e53c23 Publication map: record Cut 4 Soul follow-ups (crash-safe log store prerequisite)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:38:41 +02:00
Meta Vens
fd34d4573e Geometry tests: import System.Linq for the reference walk test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:30:43 +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
9ea134aca9 Geometry: name the cache formatter on each CultRecordRef member
The MessagePack source generator cannot resolve CultRecordRef<T> on its own and
emitted a broken formatter reference; MessagePackKnownFormatter on the open
generic did not change that. An explicit MessagePackFormatter per member is the
same formatter the cache resolver uses, so the wire stays a key string.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:25:27 +02:00
Meta Vens
e35b009204 Geometry: fix the Caching.MessagePack project reference path
The previous commit lost its backslashes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:15:56 +02:00
Meta Vens
ef8f76a0d4 Geometry: register the cache's CultRecordRef formatter with the MessagePack generator
The MessagePack source generator emitted a broken formatter reference for the
CultRecordRef<T> reference members. Declaring CultRecordRefFormatter<> a known
formatter makes the generated resolver defer to the cache's own.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:15:47 +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
e1ff5b2278 Mesh tests: raw and typed writes share one readability vector test
The raw-write refusal test spelled the old header-only verdict (NotSupported for
a first slot that is a string, a legacy array replaced). The shared vector test
now drives both write paths; the old test keeps only the marked-file case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:05:56 +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
0479304d0b Geometry: reference members are CultRecordRef<T>, not [CultReference] strings
e420410f (D11) refuses a reference member whose CLR shape the cache cannot
walk; many:false needs CultRecordRef<T>. CultGeometryBuildRequest.DomainKey,
CultGeometrySelectedCutManifest.RequestKey and CultGeometryChunkArtifact.CutKey
were strings marked [CultReference], so registering the geometry catalog threw
and 7 geometry tests failed on main. All three are record keys of another
document, so the members become CultRecordRef<T> (the attribute is dropped, the
type declares the reference). The msgpack formatter writes a CultRecordRef as
its key string, so the wire bytes and the fingerprint inputs are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 01:56:03 +02:00
Meta Vens
76e449f33e Merge origin/main into hands/variants-c2a 2026-09-30 01:52:26 +02:00
Meta Vens
a6dc231d40 Ack Cut 3: TypeScript flow-window tests typecheck under noUncheckedIndexedAccess
readyToSend is optional on the receive result; the tests read it as empty
when absent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 01:10:33 +02:00
Meta Vens
13072fc5d7 CultNetDatabase: a door's wire entry carries only its message
The change kind in the door's wire entry was restated from the log entry by
NormalizeWireLogEntry, so computing it was dead code (Stryker survivor). The
entry now carries just the door's put or delete message.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 01:08:03 +02:00
Meta Vens
4f13eb6689 Ack Cut 3: a direct send behind a queued packet is refused and takes no sequence
The FIFO test now also pins the single-packet send's refusal while an earlier
packet waits in the queue, in Rust, C#, TypeScript and Python.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:57:21 +02:00
Meta Vens
ce552b0e1b Ack Cut D: fix the C# using and the Python request encoding in the new tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:56:51 +02:00
Meta Vens
eeda6d7fd6 Ack Cut 3: the sender's flow window (1,023 sequences and 4 MiB above the lowest unacked)
Admission in Rust, C#, TypeScript and Python goes through one predicate,
window_admits: the 32-packet slot, a sequence at most 1,023 above the lowest
unacked one, and at most 4 MiB of payload above it. With nothing pending any
packet is admissible. Admission is FIFO: nothing overtakes a queued packet.
The single-packet send refuses on the same predicate instead of queueing a
packet and then reporting that it produced none. The receive window is stated
in docs/cultnet-transport-parity.md. Kotlin trails (Cut K3).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:55:14 +02:00
Meta Vens
15e0654ca4 Ack Cut 3: failing tests for the sender's flow window in Rust, C#, TypeScript and Python
A reliable packet goes on the wire only while its sequence is at most 1,023
above the lowest unacked one and the payload above that sequence stays within
4 MiB. Tests cover the span (P8: a lost packet is still delivered), the byte
bound, a lone oversize packet, FIFO admission and the direct-send refusal.
They fail on the unbounded window.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:54:56 +02:00
Meta Vens
15c6a7e2d4 Merge CultMath Phacelle (Asura 2a-ii-b)
phacelle(p, side, offset, normalization) -> CultPhasor with exact
gradient and exact pruning, mirrored in HLSL. Soul: no correctness
defect; the MPL-2.0 port now lives in its own MPL-headed files and the
NuGet package is MIT AND MPL-2.0 with notices packed (0acc3728).
Release (Unity DLL rebuild, changelog) is 2a-ii-c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:54:19 +02:00
Meta Vens
e79f3ee370 Journal tests: pin the survivors Stryker found in the changed journal code
Restart over a retained log resumes above its highest sequence and reads its
latest from the durable log; put and delete doors log their own wire messages
and change kinds; a reconciled put is logged as an update; a replica's
watermark follows the primary's sequences; an admission racing disposal logs
nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:52:02 +02:00
Meta Vens
0b2f580c82 Ack Cut D (Python): a failed send to one peer no longer kills the RUDP thread
CultMeshLocalServer._send_rudp_packet let an OSError from sendto escape
_rudp_loop, which ended the RUDP thread for every peer. It now counts the
failure (rudp_send_failures) and returns. The test patches socket.sendto to
fail for one peer and serves another.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:51:31 +02:00
Meta Vens
8e609284d2 Ack Cut D (C#): a failed send to one peer is that peer's lost datagram
CultNetRudpSocketTransportServer.SendPacket let a SocketException escape, so
the first failed SendTo ended PollResends and TryReceiveOnce for every peer.
SendPacket now counts the failure in CultNetTransportStats.SendFailures and
returns; only an encode failure throws. The refusal goodbye's own catch is
redundant and goes.

The tests inject the failure at SendPacket through an internal FailingSendPeers
set, exposed to GameCult.Networking.Tests by InternalsVisibleTo.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:51:29 +02:00
Meta Vens
def93c5d89 Ack Cut D (Rust): a failed send to one peer is that peer's lost datagram
The cultmesh-rs document server and the cultnet-rs hub returned the first
failed send_to from maintain, poll_once, poll_resends and receive_event_once,
so one peer whose datagrams cannot be sent failed every poll for every peer.
send_packet now counts a send failure (send_failures() on the document server,
CultNetTransportStats.send_failures on the hub) and returns; only an encode
failure stays an error. The document server treats recv_from ConnectionReset
as Idle, as the socket transport does.

Tests inject the failure at the one send function through a cfg(test) set of
failing peers and prove the other peer is still served.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:47:53 +02:00
Meta Vens
9b8877ced7 Ack map: record Q-A7 A, Q-A8 A, Q-A9 B
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:45:41 +02:00
Meta Vens
03b1df7cf5 Ack map pass 3: FORWARD-TSN (Cut F), receipts port, Cut 1c ruled, Cut K, Cut D
Maps the four 2026-09-30 rulings. Cut 2 merges only with Cut F (Soul:
expiring reliable sets pin the bound). Adds Cut D (a failed send to one
peer no longer aborts a document server poll) and C# Cut 2c. Opens
Q-A7, Q-A8, Q-A9; Q-A7 and Q-A9 block Cut F.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:42:27 +02:00
Meta Vens
0acc3728ed CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
MPL-2.0 is file-level and the Phacelle port sat inside MIT files. phacelle and
CultPhasor now live in math.Phacelle.cs and CultPhasor.cs, and cultmath_phacelle
in CultMath.Phacelle.hlsl (included by CultMath.hlsl, both tracked copies), each
with the MPL header and Johansen / Luke Mitchell attribution. math stays one
partial class, so math.phacelle is called exactly as before. The GameCult.Math
package declares MIT AND MPL-2.0 and packs THIRD-PARTY-NOTICES.md and the new
include; the Unity LICENSE.md points at CultLib; build-unity-package.ps1
requires the new include.

The random sweeps never met a cell whose box bound sits just under the 2.25 cut
while its weight is still nonzero, so cuts of 2.245, 2.24 and 2.2 survived.
Six hard-coded points (found by an offline search) sit next to such cells; the
C# pruned-vs-unpruned test and the HLSL mirror test both use them, with a
witness check that re-derives the property from the hash.

Docs: the prune keeps about 45 of 64 cells (14 is the nonzero-weight count),
the support radius is 1.49998 (d^2 = 2.24995), normalization's range and
saturation, W = 0 only for adversarial hashes, and the support-edge gradient
seam size.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:34:27 +02:00
Meta Vens
af7751e263 Journal tests: the nested shard-log apply must finish, and the store-writes-cache probe fires once
The reentrant apply test now asserts the nested call completes without
throwing: a re-recorded sequence used to be swallowed by the subscriber's
handler and hide a non-replacing in-memory log. The store-writes-cache probe
writes once, so a cache that let the journal re-enter it fails the test instead
of recursing until the host crashes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:24:40 +02:00
Meta Vens
b841274f0e Ack Cut 1b tests: an ending does not forget what the peer sent
A frame held behind a gap survives a timeout and is released when the gap
fills, and a retransmit of a frame received before the end is still a
duplicate, in all four runtimes. Cut 1b drops what the session owes; what it
received from the peer is the peer's retransmit protection.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:24:19 +02:00
Meta Vens
b299582d71 Ack Cut 1b tests: queued writes, flush after a reconnect, the refusal goodbye
The lost write in every ending test now leaves part of a fragmented write
queued. A flush that starts after a reconnect, or after a server-mode
transport accepts a new Connect, must succeed. The three stray-packet tests
that read the peer's pending set after a Disconnect encoded the old rule
(the peer's owed write survives the goodbye); they now assert on the
goodbye's ack field and mask, which is where the refused frame must not be
acknowledged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:20:05 +02:00
Meta Vens
43d7e53efd Ack Cut 1 tests: three held frames, the Rust byte bound, reset reseeds
A gap now holds three ordered frames so a watermark that advances one step
per call fails; the reset test follows a second handshake at a different
sequence so a watermark that survives the reset fails; Rust gains the
4 MiB hold bound in bytes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:20:04 +02:00
Meta Vens
ebc7080165 Ack Cut 1b: every ending drops what it owed; a flush is bound to its generation
Each session gets one private primitive that ends its current generation:
not connected, pending and queued writes dropped, generation advanced,
"ended" raised until the next Connect or Accept begins one. A peer's
Disconnect, a local disconnect, a timeout and a reset (refusal included) all
call it, and it leaves what was received from the peer alone. The socket
transports' flush captures the session generation and fails when it changes
or the session has ended, in all four runtimes; the disconnect reason and,
in TypeScript, the transport's generation pair are report-only or gone. Rust
receipts carry the generation in place of the session scope UUID, so a
receipt reads Invalidated after any ending and stays so after a reconnect.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:13:22 +02:00
Meta Vens
f2b0f008db Ack Cut 1b tests: a write belongs to the session it was issued in
Every ending path (peer Disconnect, local disconnect, timeout, refusal or
reset) must drop the writes it owed, so that a later session neither
retransmits nor delivers them; Rust adds the receipt after a local
disconnect and a reconnect. A flush that starts after an ending, or that
started before one and sees a Connect begin a new session, must fail. The
C# and Python flush tests use a second thread for the mid-wait reconnect.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:13:09 +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
0be33c1198 Rust: a zero-byte store file is rewritten as an empty v1 store
Soul's mutant deleting the empty-file arm of write_all_unlocked survived: no
test rewrote a zero-byte file. Without the arm the empty file reaches
store_header and is refused as truncated. The test pushes onto a zero-byte
file and expects a v1 store holding the pushed record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:11:12 +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
385123277f Ack Cut 1: one watermark owns ordered delivery in all four runtimes
The per-channel next pointer, its first-use initialisation, the skip over
other channels' sequences, and the per-channel drain are deleted. Each
session keeps one contiguous received-through watermark, seeded by the
peer's Connect or Accept (or the first packet of an unhandshaken session),
advanced in the received-sequence bookkeeping. Ordered frames wait in one
map keyed by first sequence, and every reliable receipt drains the frames at
or below watermark + 1 in sequence order, after the unordered frame of the
same call. A frame is held only while a sequence below it is missing, so a
5 MiB ordered frame with no gap is never held. Rust bounds what is still
held after the drain (frames, bytes, channels with held frames).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:00:32 +02:00
Meta Vens
b25effebe4 Ack Cut 1 tests: ordered delivery is held only behind a missing sequence
P1, P1b and P6 as asserting tests in Rust, C#, TypeScript and Python, plus
the handshake-seeded watermark, duplicate and reset rules. They fail on the
per-channel next pointer and pass on one contiguous watermark. The three
tests that fed the receiver an impossible handshake (an Accept at sequence 91
for data that starts at 1) now use a real Connect and Accept, and the Rust
channel-bound test holds its frames behind a gap.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:00:05 +02:00
Meta Vens
2a3cab1bd7 CultMath tests: a dense sweep pins cultmath_phacelle against C# bit for bit
The generic mirror comparison passes about a hundred inputs through the shader and
missed a mirror whose prune cut was 2.0 instead of 2.25, because only a rare cell has
a nonzero weight under that cut. Twenty thousand random arguments compare the shader
text and math.phacelle field by field and kill that mutant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:58:43 +02:00
Meta Vens
e9469cb3a4 Ack map: record Q-A3 B, Q-A4 A, Q-A5 A and Q-A6 A
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:53:21 +02:00
Meta Vens
3fb2613abf Networking tests: order is data, so pin each change's own publication, not its delivery order
The salvaged S1 and in-place re-upsert tests also asserted the delivery order
of the rejected drainer. Under Q-P1 A delivery order across threads and a
nested write's depth-first delivery are not guaranteed; each change is
published once with its own context, and wire peers drop stale changes by
Sequence (Cut 6). Both assertions now say exactly that. The journal tests'
reentrant-write recorder subscribes before the handler that writes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 23:52:14 +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
9f18a432cb CultMath: document phacelle and record Johansen's MPL-2.0 provenance
design.md names CultPhasor as the second struct-return consumer and describes
phacelle (stripe wave vector, weight, normalization, exact gradient and its two
discontinuity classes, exact pruning). THIRD-PARTY-NOTICES.md, in both the package
root and the Unity package, cites Rune Skovbo Johansen's Phacelle Noise (MPL-2.0)
and records that the 3D-cell generalisation, the caller-supplied side vector, the
exact gradient and the exact pruning are new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:51:04 +02:00
Meta Vens
7904e15ed3 CultMath: phacelle, Johansen's Phacelle noise on 3D cells with an exact gradient
Cut 2a-ii-b. phacelle(float3 p, float3 side, float offset, float normalization)
returns CultPhasor { float4 cos; float4 sin }, each float4(gradient.xyz, value.w).

- 4x4x4 cells, pcg3d-hashed jitter in [-0.5, 0.5), weight max(0, exp(-2 d^2) - 0.01111),
  normalization as upstream (lpmitchell/AdvancedTerrainErosion PhacelleNoise). The
  caller supplies the stripe wave vector; offset is in cycles.
- The gradient is exact: weight derivatives and the normalization are included, not
  upstream's grad(cos) ~ -sin * side.
- Cells whose per-axis lower bound is at least 2.25 have weight exactly 0 and are
  skipped. An internal overload switches the prune off so the test can pin bit
  equality without a second copy of the loop.
- cultmath_phacelle and CultPhasor mirror it in both tracked CultMath.hlsl copies
  (the same blob). The mirror test compares the struct field by field; it now covers
  34 functions.
- PhacelleTests: value against an independent double-precision reference, quarter-cycle
  phase rotation, gradient against central differences on both sides of the
  normalization threshold (excluding the threshold and each cell's weight-support
  edge, where the exact gradient is discontinuous), stripes along the flow on the unit
  sphere (0.215 against the 0.25 bound), step-difference continuity across cell planes,
  and pruned == unpruned bit for bit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:51:02 +02:00
Meta Vens
fe7bca0cb5 CultNetDatabase: log on the cache journal and publish from Watch, no queue
Cut 4b-4d of the publication map. The reorder buffer, drainer, prediction gate
and the logging inside the Watch handler are gone. The database registers the
Watch observer, then one cache journal. The journal runs under the gate in
Sequence order: it takes the door, decides the change kind and prediction
bookkeeping, logs (a primary mints, a replica records the primary's entry) and
stashes the change; the Watch observer publishes the stash entry by Sequence.

A primary that cannot log a committed change burns that sequence, raises its
own compaction floor, tries CompactThrough, and throws CultNetShardLogException
(not an InvalidOperationException) after publication. A replica's refused
append reaches the pull loop and the cursor does not advance.

Fixes four defects the map named: the log is appended to disk before memory
(and memory is a SortedList keyed by sequence); the snapshot reads its log
sequence before its documents; a restart resumes above the compaction point;
GetCompactedMutationLogSequence and GetLatestMutationLogSequence honour the
in-memory floor. The snapshot removal door sets Removal.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 23:48:50 +02:00
Meta Vens
ce0d61fc71 Ack map pass 2: re-map on 3bf1c0c, split session generation into Cuts 1b and 1c
Applies the Q-A1/Q-A2 rulings (one public meaning: Acknowledged means
delivered). Adds Cut 1b (every session ending drops what it owed; flush is
bound to its session) and Cut 1c (how a new generation is recognised,
blocked on new Q-A6). Probes at 3bf1c0c reproduce G1, G1b, G2 and G3 in
all four runtimes. Kotlin trails as Cut K pending Q-A5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:47:14 +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
7d945b002d Merge the salvaged CultNet publication branch (queue removed in the next commit)
Cut 4a: brings the publication tests and the door table from
hands/cultnet-publish-commits at 28248ebe. The reorder buffer, drainer and
prediction gate die in 4b.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 23:36:55 +02:00
Meta Vens
af7209a2c8 Map publication Cut 4-6 on the journal (Imagination pass 4) and record Q-P5 A
Adds the pass 4 probes, the journal-floor rule, and the re-taken Cuts 4-6, salvage and ledger.
Records Q-P5 A: a primary that cannot log a committed change throws CultNetShardLogException.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:34:42 +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
bd58963701 Pin each disjunct and edge of the parity record validators
The audio parity validator now has a refusal row for every field in its
identity and timing disjunctions, and both parity validators have tests for
the inclusive edges: 256 shards, the video block that would make 257, and a
deadline equal to the base presentation time. cargo-mutants over the diff
had left these operators standing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:11:20 +02:00
Meta Vens
3616d0617c Rust: the header is read and the file proven whole in one pass
rmp_serde refuses a sequence visitor that stops early, so the prefix-only header read the
previous commit introduced could not decode any store. store_header is now one visitor: the
first slot is the header (None when it is not a string, the legacy envelope array), the rest
is walked, and a truncated file is an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:58:38 +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
Meta Vens
aa6b11006f CultMesh: a raw write refuses a file whose header it cannot read
ExistingHeader read the whole snapshot and swallowed every failure into "no header": a
truncated v3 file got a v1 raw write and an unknown v9 file was overwritten, where
TypeScript, Python and Rust refuse. CultDocumentMessagePackSerialization.ReadStoreHeader
reads the prefix header, refuses a file that is not one complete MessagePack value or whose
format this runtime cannot read, and the raw write keeps a whole marked file marked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:53:13 +02:00
Meta Vens
c4fbb53a3b C#: HoldsIds means what the stored form persists, wherever the record came from
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>
2026-09-29 22:53:13 +02:00
Meta Vens
8272b40645 Rust: a store cut short is refused by a rewrite, never taken for a legacy file
store_header returned None for any bytes it could not walk, so push_all read a truncated
variant store as legacy and overwrote it v1. It now proves the file is one complete
MessagePack value first (an error otherwise, the rewrite refuses and leaves the file), then
reads the header from the prefix; the reader's own header read no longer walks to the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:53:12 +02:00
Meta Vens
3bf1c0cea5 Merge RUDP stray-packet hardening across all four runtimes
A stray, malformed, foreign-id or refused datagram is dropped and counted,
never fatal; a refused reliable frame ends the peer's session instead of
being acked and lost; flush fails once its session ends; one reliable
Accept per session; the TS operation service expires idle sessions and
survives a throwing handler. Four Soul passes; session-generation and
reconnect semantics move to the ack cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:05:30 +02:00
Meta Vens
b0d4103e22 cultnet-ts: a timeout ends the session generation, and every non-repeated Connect ends a live one
Also adds a swept-after-handler test for the operation service and states
the Rust receipt comment exactly: only a peer Disconnect invalidates receipts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:01:43 +02:00
Meta Vens
d29e2a7d81 Ack map: the ending paths disagree about owed writes (Cut 1 scope)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:59:18 +02:00
Meta Vens
e9a5a60dbf TypeScript and Python: pin pushAll/push_all onto a store whose header the runtime cannot read
The behaviour was already a refusal that leaves the file untouched; now a test fails if a rewrite
ever reads the disk header less strictly than a load does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:56:08 +02:00
Meta Vens
7ea1aa6a9a Rust: a rewrite reads the header with the snapshot reader, not a hand-written prefix parser
push_all reads the file and reuses store_header and readable_store_format, the same pair a load
uses. The 96-byte prefix parser it replaces misread a str16 header and any str8 header past the
prefix as v1 and overwrote a store it could not read. Pins both encodings refused with the file
untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:56:07 +02:00
Meta Vens
41d0dd9bfb C# header: one function decides it for every writer; a store's own ids are the only source
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>
2026-09-29 21:54:01 +02:00
Meta Vens
36e052b33a cultnet-ts: the replaced-peer test acknowledges A's Accept so a repeated-Connect answer is distinguishable from an Accept
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:46:27 +02:00
Meta Vens
c412eed066 cultnet-rs: the reconnect test expects the Accept to have acknowledged the new Connect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:37:24 +02:00
Meta Vens
29f8bf575e Tests: a repeated Connect acknowledges what it carries (TS, Python, C#); fix the TS replaced-peer test
The repeated-Connect answer applies its acknowledgement field in every runtime;
no test failed when a runtime stopped doing so. The TS replaced-peer test now
lets client A receive its Accept before A sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:35:44 +02:00
Meta Vens
5f1e883d88 cultnet-rs: a peer's Disconnect ends its session's receipts and waits for good
A Disconnect left the session's pending writes and scope alone, and the
transport's Invalidated status leaned on disconnect_reason, which connect()
clears. After a reconnect the receipt returned to Pending and could become
Acknowledged in the new session. The session now rotates its scope and drops
what it still owed on Disconnect, so the session is the one owner of
Invalidated and the transport override is gone.

A flush records the session scope it started in and fails when a Connect that
replaces the peer rotates it, instead of resolving on the emptied queue.

Tests: Disconnect-then-reconnect receipt, stale end reason after a new Connect
is accepted, flush across a replaced peer, and a repeated Connect's ack field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:35:44 +02:00
Meta Vens
756da48943 cultnet-ts: a Connect from another endpoint replaces the peer; a flush is bound to its session; a busy operation session is not idle
A server-mode transport treated a Connect from a new endpoint as a repeated
Connect once the session was connected: the new client got an Ack and never an
Accept, and the old session never ended. Only a Connect from the accepted
endpoint repeats; another endpoint resets the session and gets its Accept.

A flush recorded nothing about the session it started in, so a disconnect
listener that reconnected cleared the end reason before the 5 ms poll saw it.
Sessions now carry a generation; a flush fails if its generation ended.

The operation service's idle sweep deleted a session whose handler was still
running, losing the response. A session with a handler running, or one that
finished a handler within the idle window, is not idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:32:05 +02:00
Meta Vens
ecfb4d6509 Ack map: record Q-A1 and Q-A2, one meaning for Acknowledged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:29:59 +02:00
Meta Vens
9321c98a38 Map the RUDP ack cut: Received and Delivered receipts
Imagination pass 1. Probes found acknowledged ordered frames lost in live
sessions in every runtime; the map derives Delivered at the sender from a
correct receiver watermark. Operator questions Q-A1..Q-A5 open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:24:52 +02:00
Meta Vens
591eeff70d Rust: a whole-store flush reads the on-disk header first and refuses one it cannot read
push_all overwrote a v9, variant-v2 or variant store with a v1 file, dropping its variants; TypeScript and Python
refuse. The rewrite header now comes from the file's own prefix and shares one readable-format rule with the
decoder. TypeScript pullAll resets its header when the file is gone, empty or legacy, as Python does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:20:11 +02:00
Meta Vens
6a7d17bdda C2a fix: a flattened record is marked by the document it stores; every writer decides its header by content
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>
2026-09-29 21:20:01 +02:00
Meta Vens
4252ce65aa Map: C2a Soul pass 3 and the C0-with-C2a release ruling
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:16:34 +02:00
Meta Vens
d85877a7be cultnet-rs: the session no longer keeps an initial sequence nothing reads
Reset used to reissue from it; issued sequences now survive a reset, so the
field was dead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:54:03 +02:00
Meta Vens
9475e3653d cultnet-rs, cultnet-ts: tests that fail when the fix is removed
The hub test counts replaced sessions, since a fresh session repeats the same
Accept sequence and the distinct-sequence count could not see it. The TS test
requires an Ack, not a new Accept, once the Accept was acknowledged. A peer
Disconnect is pinned as an ended session for flush and receipts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:47:53 +02:00
Meta Vens
ae8576d856 Merge CultCache publication Cut 3: the journal
AddJournal runs under the gate right after Apply, once per admission that
changed something, before observers. Soul: merge with follow-ups; the
dispose and catch-path fixes landed. Cut 4 must decide what a throwing
journal means for replicas and whether a journal needs the change origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:45:30 +02:00
Meta Vens
afd52e97fa GameCult.Networking: the replaced-peer reason belongs to the listener that reports it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:44:32 +02:00
Meta Vens
ca18dfac55 GameCult.Networking: an ended session fails its flush; a repeated Connect owes one Accept
FlushReliable throws for writes an ended session forgot. A server-mode
transport and the listener answer a Connect from the peer they already hold
with AnswerRepeatedConnect: the Accept still owed, or an Ack, queueing
nothing. The listener replaced the peer session on every Connect, reusing
sequences and never reporting PeerDisconnected; it now replaces only on a
different connect payload and reports the old peer, as the Rust hub does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:41:23 +02:00
Meta Vens
4814aea8b0 cultnet-ts: an ended session fails its flush; abandoned operation sessions idle out; a repeated Connect owes one Accept
flush now rejects for writes an ended session forgot. acceptConnect on a
connected session delegates to the new answerRepeatedConnect, so a repeated
Connect re-sends the Accept still owed, or an Ack once it was acknowledged,
and queues nothing; the operation service no longer builds a session per
Connect. The operation service dropped sessions never, so 64 abandoned
clients locked every new one out: a session idle for sessionIdleTimeoutMs
(default 30 s, the document server's session_idle_timeout) is now dropped.
A request whose handler rejects ends its session like a transport refusal,
so the invoker learns promptly and a retransmit is not acknowledged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:41:22 +02:00
Meta Vens
34054db03a cultnet-py: an ended session fails its flush; a repeated Connect owes one Accept
flush_reliable raised nothing for writes the ended session had forgotten; it
now raises ConnectionError with the reason. Server mode answered every
repeated Connect by queueing a fresh reliable Accept; a Connect from the peer
already accepted is now answered with the Accept still owed and queues
nothing, as the Rust hub does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:37:56 +02:00
Meta Vens
c7a61018da cultnet-rs, cultmesh-rs: an ended session fails its flush; issued sequences survive a reset; the doc server says goodbye
A session that refused a packet, or was disconnected, forgot its
unacknowledged writes, so flush_reliable returned success for writes the peer
never acknowledged. The transport now fails the flush with the reason and
reads such writes as Invalidated. Reset no longer reissues sequence numbers or
fragment ids: a late duplicate Connect in server mode reused sequences the
peer had seen. end_refused_session owns the reset-then-goodbye ordering for
the hub, the transport and the document server, which used to end a refusing
session silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:36:58 +02:00
Meta Vens
592a2d49d4 Journal registration counts as disposed only once removal succeeds; a throwing hold body surfaces journal failures
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>
2026-09-29 20:35:50 +02:00
Meta Vens
4b85a173e8 Drop the redundant complete-block short circuit from FEC recovery
A block with every data shard present passes through the code as a no-op
and yields no recovered records, so the early return in both recovery
functions did nothing the code did not already do. Mutation showed both
survive removal. The audio payload-length rule is now pinned by its
message, since the codec's own shard-size error would otherwise mask its
removal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:35:18 +02:00
Meta Vens
1da2a64fbc Pin the media FEC and impairment rules that mutation left standing
Adds the policy fields that must be non-zero, the inclusive shard limit at
the decoder for video and audio parity, admission statistics, seeded jitter
bounds, exact seeded loss, and arrival order for datagrams due together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:26:49 +02:00
Meta Vens
6285a92841 Close the media FEC test gaps found before mutation
Pins the block-boundary chunk and last-chunk length refusals in video
recovery, the audio recovery refusals (packets one either side of the block,
another session, wrong length, parity from different blocks), the remaining
audio block identity rules (session, stream, timebase, duration), and the
data-parity alternation inside a block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:20:06 +02:00
Meta Vens
858e6ec608 Add the cultnet_impair proxy example and a socketless media FEC loss matrix
cultnet_impair is a deterministic bidirectional UDP proxy for pressure
testing real CultNet RUDP sessions with seeded loss, burst, duplicate,
reorder, jitter and stall schedules. It is ported from the Odin donor
(80adfd3) as an example, not library surface. The decision policy (Profile,
Scheduler, seeded RNG) is split from the sockets into policy.rs, which the
proxy and the tests both compile, so a schedule has one owner. The ten
donor profiles live in tests/impairment/.

tests/impairment_policy.rs keeps the donor's six deterministic policy tests
(the burst test now pins the exact drops rather than a count) and adds
profile loading and rejection. tests/media_fec_loss.rs sends protected video
frames through those schedules with no sockets and asserts the guarantee: a
block recovers exactly when at most m of its records were lost, recovered
chunks equal what was sent, and a burst that the send order spreads under m
per block loses no frame. The recovered-frame ratios are printed, not
asserted; tests/impairment/loss-matrix.txt is that output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:17:46 +02:00
Meta Vens
9ed61dd290 C2a test: a flattened variant that holds override ids keeps the flush marked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:09:05 +02:00
Meta Vens
f7c58ce5bc Add the media FEC codec, video parity v3 and audio parity records to cultnet-rs
The codec owns the erasure math for both ends: protect_video_frame and
protect_audio_block on the producer, recover_video_block and
recover_audio_block on the consumer. Each block is systematic Reed-Solomon
over GF(2^8) (reed-solomon-erasure 6, default features), named on every
parity record by the scheme id rs-gf256-v1. Any <= m erasures recover
exactly; more report BeyondRepair. Video frames split into ceil(n/16)
near-equal blocks with m = max(2, ceil(k/4)); audio is 4+2. A frame's
records come back in send order, rotating one record from each block so a
burst lands as one loss per block.

Deleted first: gamecult.media_video_parity_shard.v2 and its XOR-stripe
validator, and the reliable "audio" channel (profile entry, send-options
arm, constant, tests). Audio now rides "media" with parity.

Added: gamecult.media_video_parity_shard.v3 (block geometry, scheme id,
shard length), gamecult.media_audio_parity_shard.v1, the AudioParity wire
variant with validation, and the parity record key now names the block.
Every shard is bounded at MEDIA_FEC_MAX_SHARD_PAYLOAD_BYTES (720) so its
wrapped record fits MEDIA_FEC_MAX_WIRE_BYTES (1431), measured with
identifiers of up to 32 bytes.

Tests pin: exhaustive recovery for every shipped (k, m) and 4+2, m+1
refusal, known-answer parity bytes for both paths, the datagram bound on
every record kind, send order, deadline carriage and decoder validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:08:54 +02:00
Meta Vens
36acb4295a Reach the empty admission through a commit that removes nothing
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>
2026-09-29 20:04:59 +02:00
Meta Vens
e8886ff8c0 C2a test: a reloaded variant holding override ids keeps the whole-store flush marked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:03:22 +02:00
Meta Vens
281e1b238c Keep the test frame from holding the subscription it checks the cache released
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:03:08 +02:00
Meta Vens
dfde61aba3 C2a test: a preset id and a loaded record holding one mark the store
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:52 +02:00
Meta Vens
5b3885a1e8 Detect an unsubscribed observer by what the cache still holds, and pin the empty admission
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>
2026-09-29 19:47:38 +02:00
Meta Vens
9740551839 cultnet-ts: pin the operation service handler rejection path as a counted drop
The fragment-id-0 test is caught by the session-refusal branch, so a frame that
fails inside the handler needs its own test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:45:38 +02:00
Meta Vens
9da5b35f34 TypeScript and Python: a whole-store flush applies the header the file on disk carries
TS pushAll wrote the header its instance last read (v1 when fresh) without reading the
file. It now reads the on-disk header first, as push and delete do through pullAll. Python
resets its remembered header when the file is gone or empty, so the disk decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:44:16 +02:00
Meta Vens
306412e822 GameCult.Networking.Tests: the malformed-length datagram has the 36-byte fixed header and no channel id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:43:55 +02:00
Meta Vens
cc6615b675 C2a test: the id-bearing variant overrides its name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:43:55 +02:00
Meta Vens
9dba01082c GameCult.Networking: the listener refusal local no longer shadows the datagram length
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:42:45 +02:00
Meta Vens
b579075dd5 GameCult.Networking: a declared length past int.MaxValue is malformed; server mode is claimed only by a Connect; a refusing session ends
CultNetRudpPacketCodec.Decode did checked((int)length), so a datagram declaring
0x80000000 threw OverflowException, which no receive loop catches; the length
is now compared as a long and takes the ordinary malformed-packet drop.
Server mode claimed its endpoint on the first datagram of any type; only a
Connect claims it now. A session that refuses a packet has already recorded
its reliable sequence, so the socket transport and the listener end it: the
session forgets the peer (so the goodbye does not acknowledge the refused
frame), the peer gets a Disconnect, the listener drops the peer and raises
PeerDisconnected, and a server-mode transport forgets its endpoint.
CultNetRudpSession gains ResetPeerState, and the session and the listener
reject an InitialSequence of uint.MaxValue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:41:56 +02:00
Meta Vens
490f7b811f C2a: mark the header by content, refuse an element twice in a list, route CultMesh single-file through the id rule
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>
2026-09-29 19:40:36 +02:00
Meta Vens
44ad5e8948 Journal admissions under the cache gate, in Sequence order
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>
2026-09-29 19:40:02 +02:00
Meta Vens
042dc75c82 cultnet-py: a server-mode transport is claimed only by a Connect; a refusing session ends
Server mode claimed the endpoint on the first datagram of any type with the
right connection id, so an orphan Ack made a later real client a stranger. Only
a Connect claims it now. A session that refuses a packet has already recorded
its reliable sequence, so the transport ends it: the session forgets the peer
(so the goodbye does not acknowledge the refused frame), a Disconnect goes to
the peer, disconnect_reason is set, and a server-mode transport forgets its
endpoint. CultNetRudpSession gains reset_peer_state and rejects an
initial_sequence of 0xFFFFFFFF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:39:35 +02:00
Meta Vens
610b5c86c2 cultnet-ts: end a refusing session, survive operation-service rejections, cap its sessions
A session that refuses a packet has already recorded its reliable sequence, so
the socket transport now ends it: the session forgets the peer (so the goodbye
does not acknowledge the refused frame), sends a Disconnect, forgets the
endpoint in server mode, and emits disconnect and close. The operation service
did `void handleServerDatagram(...)`, so any rejection was an unhandled
rejection that ended Node; it now drops and counts the packet (packetsDropped),
ends a session that refuses a frame, and holds at most 64 sessions, dropping a
Connect from a new peer beyond that. CultNetRudpSession rejects an
initialSequence of 0xFFFFFFFF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:37:05 +02:00
Meta Vens
384ee30ff6 Say that a conditional variant commit reads the getters a second time
Judging the merge against the file re-runs Project for the records the commit
lands and for re-resolved variants, so "once per landing record" holds only for
the admission. Handing the plan's projections to Judging would not cover the
re-resolved dependents, so the contract now states the second read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:37:03 +02:00
Meta Vens
be91ea837a Pin that a failed hold publishes what it minted and an unsubscribed observer stops
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>
2026-09-29 19:36:41 +02:00
Meta Vens
1d0dc3aa57 cultnet-rs: repeated Connect has one primitive; the goodbye must not ack the refused frame
The Disconnect a refusing session sent carried the ack of the very frame it
refused, so the sender saw the frame acknowledged. The session forgets its
peer's sequences before building the goodbye, and a server-mode transport
forgets its endpoint so only a Connect can claim it again.

CultNetRudpSession::answer_repeated_connect is the one answer to a repeated
Connect for the hub and the document server: it records the sequence (the
document server's old accept_connect did, and a client that reconnects in one
session depends on it), applies acks, queues nothing, and returns the pending
Accept or an Ack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:35:30 +02:00
Meta Vens
5adbcaa0c8 Maps: record the C2a rollout-order and mark-by-content rulings, publication Cuts 1-2 landing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:34:54 +02:00
Meta Vens
f9034eedb0 cultmesh-rs: a repeated Connect re-sends the pending Accept and queues nothing
The document server called accept_connect again for a Connect on an existing
key, queuing another reliable Accept each time: a resend storm, and the full
queue Err that the previous commit turned into a drop. The repeat now re-sends
the Accept still pending, or an Ack once acknowledged, as the hub does. A fresh
session's accept_connect cannot fail under validated options, so its error is
propagated again rather than counted as a drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:33:30 +02:00
Meta Vens
f6dbbf77ef cultnet-rs: end the session of a peer whose packet the session refuses
CultNetRudpSession::receive records a reliable sequence before reassembly, so
keeping the session after it refuses a frame made the sender's retransmit be
acknowledged and the frame silently lost. The hub and the socket transport now
end that peer's session, send it a Disconnect, and surface the end to the
caller (a Disconnected event, disconnect_reason()). The hub and socket
constructors reject initial_sequence u32::MAX and max_pending_reliable_packets
Some(0), so the accept_connect drops are unreachable. pending_accept_for_resend
becomes public for the document server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:33:30 +02:00
Meta Vens
2a1b8a8cda Merge CultCache publication Cuts 1-2: project before mint, isolated observers
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>
2026-09-29 19:30:04 +02:00
Meta Vens
69bd45ff14 Merge canonical CultMath serialization and the Geometry vector collapse (P4)
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>
2026-09-29 18:34:09 +02:00
Meta Vens
4e5327c61c Bring the CultMath JSON converters to System.Text.Json parity and remove their allocations
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>
2026-09-29 18:31:16 +02:00
Meta Vens
fee55e89c5 Check the CultMath release gate against the type-definition table
scripts/check-cultmath-types.cs reads the assembly with System.Reflection.Metadata
and reports which named types are not public top-level CultMath types. The old
NUL-delimited string search passed an appended string or a member named like the
type, and failed a real type whose name is tail-shared in the string heap.
Prerelease and range versions now get a clear message instead of a raw
System.Version error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:31:16 +02:00
Meta Vens
e85b201bd2 Contract: element ids, the v3 header and its rules
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:18 +02:00
Meta Vens
f4f8e3ed93 Old-reader probes: a reader from before element ids refuses the v3 header
old-reader-refusal.sh stages the C0 merge (e382bb4, or OLD_READER_REV) plus the v3 vector
and one probe per runtime onto a commit; the probe run from that tree is the old reader.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:17 +02:00
Meta Vens
adb4047936 TypeScript, Python and Rust read the v3 header and keep it on rewrite
A store that can hold element ids declares cultcache.store.v3. These runtimes pass payload
bytes through, so they read it and write back the header they found; they do not decide
when a document carries ids. Vector: tests/vectors/document-variants-c2a/v3-base.msgpack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:17 +02:00
Meta Vens
fed7204d9c Tests: the element id rules, each pinned against its own mutation
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>
2026-09-29 18:27:16 +02:00
Meta Vens
fc52f984db CultCache: element id rules, atomic minting, minted override values, the v3 marker
The id rule is one plan, made before anything is written: Plan decides every fill and
refuses a duplicate, a random id that is not 12 lowercase hex characters, and a derived
id whose source is null or empty, naming the record and list (CultElementIdException).
A load refuses what a write refuses. Apply and Undo make minting part of the admission:
a refused or uncommitted write leaves the caller's documents as they were.

Override values mint inside Resolve, into the decoded copy, so the public
CultVariantOverride.Set, C1-era overrides and Override() all get ids on write and on load;
Override() no longer mints. Interface unions are judged on their concrete types, the same
rule registration and minting share, and a subtype reusing the id's key is refused at
registration. Dictionary keys format with the invariant culture.

Records minted at load are marked per entry (IdsInMemoryOnly) instead of listed by key, so
a deleted record leaves the mark with it, and a flush of the whole store clears it. A store
whose types reach an object list declares cultcache.store.v3 (directory: v5), which every
reader older than ids refuses; a marked file stays marked when a writer merges onto it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:16 +02:00
Meta Vens
2ef7ae92c4 cultnet-rs: pin every RUDP stray drop site with an exact count
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:21:58 +02:00
Meta Vens
5fb3fca2ba Normalise a blank name once, where it is read
The held-ancestor path compared the raw empty string, so a blank variant name
and a blank held base name never met the blank rule; NameRead now returns null
for blank on both paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:14:24 +02:00
Meta Vens
6b3da283b4 cultmesh-rs: pin each RUDP document server drop site with an exact count
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:12:09 +02:00
Meta Vens
b8dbcfe40c GameCult.Networking: RUDP socket transports drop and count strays
A malformed frame, a foreign connection id or a packet the session refuses
threw out of TryReceiveOnce. They are now counted in Stats.PacketsDropped and
the read reports progress. The client-side transport checks the connection
id before it claims the peer endpoint; the server listener drops refused
packets instead of throwing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:51:32 +02:00
Meta Vens
dbe4f8a850 cultnet-py: RUDP socket transport drops and counts strays
A malformed frame, a foreign connection id or a refused packet raised out of
receive_once. They are now counted in stats.packets_dropped and receive_once
returns None. The id is checked before the peer endpoint is claimed, so a
stray arriving first can no longer become the server-mode peer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:48:10 +02:00
Meta Vens
0a543c9fb2 Kill the Stryker survivors in the projection and observer cuts
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>
2026-09-29 17:46:36 +02:00
Meta Vens
ecb80b4fd8 cultnet-ts: RUDP socket transport drops and counts strays instead of emitting error
A foreign connection id, a malformed frame or a refused packet was emitted
as an error event; with no listener that throws and ends the process. They
are now counted in stats.packetsDropped. The id is checked before the peer
endpoint or session state is touched, and an unadmitted first packet in
server mode no longer claims the endpoint. Listener faults still surface as
error events.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:44:37 +02:00
Meta Vens
ba8466114d cultmesh-rs: RUDP document server drops refused Connects instead of dying
A Connect storm from a flow that never hears its Accept filled the session
reliable queue and accept_connect returned Err out of poll_once. A refused
Connect, a malformed frame and a packet for an unknown session are now
counted in packets_dropped() and poll_once keeps serving.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:38:20 +02:00
Meta Vens
7b7666f2c3 cultnet-rs: drop and count packets that belong to no session
A foreign connection id, a malformed frame, a sender no Connect admitted, a
Connect with the peer table full, and any packet the session refuses ended
the transport loop with an Err. Odin died twice this way after nginx reloads
moved its UDP flows (require_connection: connection id does not match).

The hub and the socket transport now drop such packets and count them in
CultNetTransportStats::packets_dropped. Only socket I/O errors leave the
loop. The socket transport checks the connection id before it touches the
peer endpoint or session, so a stray Connect can no longer move the endpoint
or reset the session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:38:20 +02:00
Meta Vens
26a4f4240b Pin that a zero or missing Random state decodes to the generator's default seed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:29:02 +02:00
Meta Vens
817c2627b8 Rebuild the CultMath JSON converters on a streaming reader with STJ number semantics
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>
2026-09-29 17:29:02 +02:00
Meta Vens
9b5597e89c Gate the CultLib Unity release on the CultMath package it depends on
The package build now fails when the declared org.gamecult.cultmath version is
ahead of the CultMath package, or when that package's CultMath.dll lacks a type
CultMathResolver formats. It also drops the shipped-plugin guard that could not
fire, since the output root is wiped first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:20:17 +02:00
Meta Vens
9b23b33f8c Let Stryker mutate the variant name rule
The is-not pattern binding made every mutant in RefuseVariantNameSharing a
compile error, so Stryker removed the whole method from its run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:11:47 +02:00
Meta Vens
9dcce987fd Update Mesh docs that still named CultVec2 and CultRect
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:57:31 +02:00
Meta Vens
20c30224fd Add canonical System.Text.Json converters for every CultMath value type
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>
2026-09-29 16:57:31 +02:00
Meta Vens
77602d5c0e Pin every CultMath MessagePack shape and decode a missing Color32 alpha as opaque
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>
2026-09-29 16:57:31 +02:00
Meta Vens
41b964c6f1 Narrow the escaped-observer probe to what this base can show
A database write is published after its cache write returns, so a write whose
observer threw never reaches the database observers on this base. Keep the
probe to the exception reaching the writer and later changes still publishing
and logging; the ordering question belongs to the database cut.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:57:04 +02:00
Meta Vens
b8ceab84ac Drive the escaped-observer probe through a database write
A direct cache write is not a database change on this base, so the database
observer could not show it. PutAsync is, and the observer after the thrower must
see it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:54:40 +02:00
Meta Vens
5687d8c355 Make the CultLib Unity package depend on org.gamecult.cultmath
GameCult.Caching.MessagePack now references CultMath, so the Unity package
declares org.gamecult.cultmath in package.json, references CultMath.dll from the
asmdef, and the package build refuses to ship a second CultMath.dll and checks
the asmdef against the shipped plus external assembly lists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:52:44 +02:00
Meta Vens
ecee41e8ff CultCache: own the observer list and isolate every observer
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>
2026-09-29 16:51:33 +02:00
Meta Vens
6b0cf66f0e Count serializer reads apart in the once-per-landed-record projection test
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>
2026-09-29 16:50:29 +02:00
Meta Vens
7362257f08 CultCache: read names and indexes once before minting; a throwing getter refuses the whole change set
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>
2026-09-29 16:48:19 +02:00
Meta Vens
60663f952c Record publication rulings Q-P1..Q-P4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:41:19 +02:00
Meta Vens
2dc7c122f9 Map CultCache publication: order as data via a journal under the cache gate
Imagination rebuilt the writer-thread scheduler Soul pass 3 proposed: it
deadlocks exactly as bcae483 did (deleted in 4562340). The map keeps per-writer
delivery, validates getters before minting, isolates observers, and moves the
shard log onto a gate-held journal. Q-P1..Q-P4 open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:36:18 +02:00
Meta Vens
1e3a337b81 Record C2a Soul hold, the tripwire and duplicate-id rulings, and the C2b precondition
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:35:33 +02:00
Meta Vens
884602c708 Merge origin/main into variants-c2a 2026-09-29 16:35:00 +02:00
Meta Vens
20ac3b0b6c Collapse CultVec2, CultVec3 and CultRect into CultMath float2, float3 and rect
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>
2026-09-29 16:18:09 +02:00
Meta Vens
14621b3893 Reference CultMath from Caching.MessagePack and scope the span helpers
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>
2026-09-29 16:15:29 +02:00
Meta Vens
139deb0e28 Add the canonical MessagePack encoding for every public CultMath value type
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>
2026-09-29 16:13:22 +02:00
Meta Vens
acdf36c81e Merge Studio grouping Cut 2: tree lowering, create in group, drag onto refs
Soul: merge. The drop gate calls the model's one candidate rule; every ref write
ends in CreateRecordRef; node ids injective (69,905-string brute force, 0
collisions). Operator click-through still owed (steps in the Cut 2 report).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:09:38 +02:00
Meta Vens
b9cca38dbf Cover valid surrogate pairs and a second unpaired surrogate in node ids
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>
2026-09-29 15:49:51 +02:00
Meta Vens
640ad9a437 Interop: an element id crosses C#, TypeScript, Python and Rust
The C# cultcache interop peer gains an object-list document (a deck of marks).
C# writes it with cache-minted ids; TypeScript, Python and Rust read the ids as
ordinary members, and TypeScript-written ids survive a C# read.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:03:09 +02:00
Meta Vens
09dabd38c5 CultNet: witness element types carry ids; peers and fixtures follow
CultWitnessContractPin, CultWitnessArtifactEntry and CultWitnessTimingEntry get
an id slot. The JSON schema contract and the Python schema copy gain an optional
id per element, and the Python and TypeScript fixtures carry and assert ids.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:03:09 +02:00
Meta Vens
ded5436569 CultCache: derived element ids; the CDN chunk ref carries one
[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>
2026-09-29 15:03:09 +02:00
Meta Vens
857e6e8a54 Studio README: grouping, create in group, drag to reference
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:10:39 +02:00
Meta Vens
88a5726017 Studio: lower the grouping tree, create in group, drag source
The record list draws GroupRecords as foldouts (fold state keyed by listed
type and node id), each with Create through CreateInGroup; an ungrouped type
lists RecordCandidates flat under the model's notice. Add, Create and
Duplicate reveal the new record's path. Rows start the record drag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:10:39 +02:00
Meta Vens
af370cd29e Studio: record drag payload, ref drop target and the model's ref label
A record row dragged onto a CultRecordRef value sets it when the ref's picker
would offer that record (IsRecordCandidate), refusing in a disabled scope, a
foreign model, or the key already held. The ref popup's None/Missing label
now comes from RecordRefLabel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:10:38 +02:00
Meta Vens
28248ebede Adopt Soul pass 2 probes and pin log-before-publish and in-place re-upsert
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:06:05 +02:00
Meta Vens
fb5ffc7483 CultNet: release cache changes in Sequence order and match door contexts by change shape
The handler now parks a change ahead of a gap and drains one thread at a time, so
the log and stream follow cache admission order and a subscriber that writes the
cache queues behind the change it handles. A door context matches only a change of
its own shape: a put door wrote an instance no cache held, a removal door removes
it, so neither a plain removal nor an in-place re-upsert can wear it. A replica
records every primary entry even when the key is absent, so chained replicas see
no gap. Predictions are locked and cleared by the handler before publication.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:06:05 +02:00
Meta Vens
384c7df02b Make group node id escaping injective
Uri.EscapeDataString maps every unpaired surrogate to %EF%BF%BD, so distinct
values shared one node id. An unpaired surrogate now spells %uXXXX.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:03:58 +02:00
Meta Vens
7defd82c20 Pin node-id escaping, case-distinct nodes and the flat ungrouped list
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>
2026-09-29 14:00:47 +02:00
Meta Vens
65311c9ad7 Merge the snoise seam fix (Asura Q-N1 a): upstream kernel r2=0.5, scale 105, permute +10
Soul: no defect; independent port of stegu 22434e04d7 matches (3D and 2D), goldens
confirmed independently, value jump 2.2e-6 over 20k crossings, gradient FD 9.7e-6,
every hand mutant killed. 2D follows upstream by ruling. Release follow-ups: rebuild
the Unity CultMath.dll, CHANGELOG entry, pin the two HLSL copies equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:55:08 +02:00
Meta Vens
c60af61e73 Merge Studio grouping Cut 1: the model owns declared record grouping
Soul: merge. [CultInspectorGroupBy], GroupingOf/GroupRecords/CreateInGroup,
IsRecordCandidate (one candidate predicate), RecordRefLabel; variants group by
resolved value and follow their base. Follow-ups in Cut 2: pin '%' escaping and
case-distinct keys, distinct ids for lone surrogates, fold state keyed by type+id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:55:08 +02:00
Meta Vens
4f7d8821af Pin the create-in-group node check against a value that fits another grouping
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:29:09 +02:00
Meta Vens
93fe1b9c8e Merge CultMath bounded least squares (locomotion Cut 2)
Closed after three Soul passes: the double iterate and progress stop hold over
~9000 cases against exact enumeration, zero allocation, deterministic; the stall
rule and fScale are now pinned (all six survivors killed). Ships in the next
CultMath release with Asura. Recorded, outside the documented range: 18/6000
gap misses at cond 1e5-3e5 with A scaled 1e-4..1e4 (solver or reference unknown).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:22:54 +02:00
Meta Vens
d8d973c418 Pin the single-owner publication cases Soul falsified
Observer rewrite of the door key, variant log order, replica log sequences with
and without a local write, 4x5000 concurrent writers, a write after a database
delete, shard-snapshot resync, and a disposed database.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:21:39 +02:00
Meta Vens
f3343c86b4 CultNet: the Watch handler is the only publisher and logger
The previous cut left two owners of publication: doors published and logged their
own change while a flow-scoped key mark told the Watch handler to skip it. A write
an observer made to the door key inside that scope was dropped, a door change and
its re-resolved variant were logged in the wrong order, and log state was
unlocked. Doors now register the context they alone know (wire message,
reconciled prediction, replicated entry) under the identity of the instance they
write or remove; the Watch handler consumes it once for that change and is the
sole publisher and logger. Only a shard primary mints log sequences; a replica
logs only the primary entries its replica door hands over. Log state sits under
one gate. The AsyncLocal mark and every door-side publish or append are deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:21:39 +02:00
Meta Vens
439efd1550 Drop the inert inherit flag from the global check and its derived-type case
[CultGlobal] is not inherited, so the flag changed nothing and no test can tell.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:16:11 +02:00
Meta Vens
30e1f3fb95 Cover ungrouped types, null strings, derived globals and id separators
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>
2026-09-29 13:15:03 +02:00
Meta Vens
09507901d0 cultmath: BoundedLeastSquares doc states the cost gap is relative to |b|^2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:14:21 +02:00
Meta Vens
c8227c6347 cultmath: BoundedLeastSquares tests drive stalled passes to the stall rule
Batches of small boxed problems whose passes end with the KKT test failing and
the cost unmoved (dependent free columns, releases the factor drops, weak-direction
noise-floor problems). They must report Converged, and land within the documented
gap of an exact active-set enumeration. Pins the stall comparison and the cost
scale in Objective, which no test reached before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:14:21 +02:00
Meta Vens
069ecc369c Note the operator's future cross-store variant direction under ruling 3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:10:36 +02:00
x
84c9a79972 Pin string case ordering and node id spelling in the grouping tests
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:52:05 +02:00
x
3594a88e4b Docs: consumer sweep list for element ids (C2a)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:31:12 +02:00
x
0d2645d056 CultCache: element ids on every object-list element (C2a)
[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>
2026-09-29 12:31:12 +02:00
x
3d07ba0d20 Give the overriding variant its own name in the grouping test
A variant must override its name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:21:28 +02:00
x
f3180f0194 Pin CultNet publication of cache commits over RUDP
Commit and upsert, database put/delete, replicated put, load removal and a base
edit re-resolving a variant dependent, each observed by a real subscriber.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:21:14 +02:00
x
574dba0a64 CultNet publishes every committed cache change once
CultNetDatabase watched OnUpdate, which fires for loads alone, so cache.Commit,
cache.UpsertAsync, variant dependents re-resolved by a base edit and Flatten never
reached subscribers or the mutation log, and a load removal carried an empty key.
The database now observes cache.Watch, the one owner of admitted changes. A write
door of the database (PutAsync, DeleteAsync, ApplyPut/Delete, replica apply, sync)
marks the key it is committing and publishes that change itself, since it carries
the shard-log entry; every other change publishes from the cache, keyed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:21:14 +02:00
x
0a5f4bf571 CultMath: regenerate snoise golden values for the upstream kernel
Seven values change: four snoise(float3) in HlslSemanticsTests and three snoise(float2)
in MathTests (the 2D (0,0) golden is 0 either way). Regenerated from the code after the
continuity probe passed. The tests no longer claim to match Unity.Mathematics, whose
snoise keeps the old constants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:09:43 +02:00
x
79e9318698 Add model-owned record grouping: declaration, tree, create-in-group, ref labels
[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>
2026-09-29 12:09:25 +02:00
x
641041822d CultMath: noise gradient tests carry only numbers the harness produces
Drops the unreproducible floors and detection-scale claims, and the seam-fluke
consistency filter (the spikes it hid were the snoise seam, now fixed and pinned by the
continuity probe). Tightens the tolerances to at least 5x the harness maximum (ridged
0.5 -> 0.05), corrects the point count in the bit-equality test, and caps the ridged
compounding loop so a mutant fails loudly instead of hanging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:04:08 +02:00
x
8ad0691bdd Declare the label fixture's key on its base member
The registry requires an override to repeat or omit the base declaration's [Key].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:03:28 +02:00
Meta Vens
a6382475e4 Collapse the ref-candidate predicate and read inherited member attributes
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>
2026-09-29 11:55:41 +02:00
Meta Vens
dc6bcd1857 Record grouping Q6 A and Q7 B
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:53:11 +02:00
Meta Vens
4a4e212d9c Refresh the Studio grouping map against main feeced8
Re-anchored to current main and Aetheria fire-control-12. Grouping gets its own
[CultInspectorGroupBy] attribute; CultIndex stays unique. Sequenced with variants
C2a/C2b/C3 into one joint release (cultlib 1.0.61, Studio 1.5.0); Aetheria
annotations fold into variants C4. Q6 and Q7 open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:51:35 +02:00
Meta Vens
feeced8949 Place the Studio grouping map on main and record Q2-Q4 (Q5 overtaken)
The map carried only on codex/studio-grouping moves to main. The operator
ruled Q2 A, Q3 A, Q4 A on 2026-09-30. Q5 is overtaken: one Studio release
with variants C3. A refresh pass is due before Hands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:40:26 +02:00
Meta Vens
75501fbc06 Record the index scan and resolution: CultIndex is unique; grouping is Studio's
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:36:29 +02:00
Meta Vens
f9f107ad27 Record the operator rulings: duplicate indices refuse, CultNet invisibility is a defect, hop refusal stays
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:14:48 +02:00
Meta Vens
8703b44636 CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
Meta Vens
4ca1cf388a CultMath: continuity probe for snoise across simplex-cell boundaries (red on the 0.6 kernel)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:10:18 +02:00
Meta Vens
468915b058 Merge remote-tracking branch 'origin/main' into hands/cultmath-snoise-grad 2026-09-29 11:09:07 +02:00
Meta Vens
6328f15345 cultmath: BoundedLeastSquares test drives the tolerance below the noise floor
With b along the smallest left singular vector, |A^T b| is tiny, so the KKT
tolerance (1e-6 of it) sits below the gradient's rounding noise at cond(A) >=
1e6 and can never be met. The solver must stop on the progress rule, in at most
six passes, never at the cap. This is the path the progress rule exists for;
the earlier fixtures reached it only by accident.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:52:39 +02:00
Meta Vens
6ffe17acf4 Merge variants C1: the C# core resolves document variants
A variant record (v2 store, slot 4 [baseKey, overrides], empty payload)
holds top-level Set overrides and is resolved in the admitting hold.
Readers see complete documents. A base edit re-resolves dependents in the
same commit. Rulings honoured: Q1 a plain Upsert at a variant key is
refused; Q4 an override follows the plain-record outcome (a dropped slot
soft-drifts and is shed, an undecodable value refuses); Q5 the v2 header
only with variants; Q6 the directory store and CultNet refuse variants
(after selection, typed, subscriptions withdrawn). Self's defaults:
invalid overrides are refused at write; R6 is extended to indexes (no
variant shares an indexed value with another record). The held index is
truthful (value to holders, smallest key wins among plain duplicates).
A conditional merge onto disk is judged on the merged set.

Soul made five passes. The held index keeping one key per value caused
three rounds of store-bricking findings until the owner was fixed.
Caching 249, Networking 293, Mesh 262, cross-runtime interop 36/36.
Follow-ups: TS/Python plain-duplicate parity, a subscription id on
per-change refusals, a removal-order test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:41:55 +02:00
Meta Vens
854f01e4c7 cultmath: BoundedLeastSquares tests pin the tolerance scale and the zero gradient
Two tolerance behaviours had no observer: the scale taken at the box point
nearest the origin (a 6e-5 release on a 1000-scale problem, in either column
position), and the strict comparisons when the scale and the gradient are both
exactly zero (b = 0 at the origin takes no iteration).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:37:27 +02:00
Meta Vens
cbd3b4f3ce CultNet: a withdrawn hop subscription is refused with a typed error and its rows are withdrawn
Reconcile's withdrawal sent an untyped error with no subscription id and left the
peer holding the rows the subscription had delivered. It now sends removals for
those rows, then a variant_unsupported error whose details carry
field "subscriptionId" and the id (the existing details slots; no new wire key).
The subscribe and per-change paths use the same code. The error schema and the
Rust CultNetErrorCode accept variant_unsupported, so a Rust peer decodes it
instead of refusing an unknown code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:12:20 +02:00
Meta Vens
2eec436850 Variants C1: the index rule is linear, a merge judges only when a variant is involved, duplicates are deterministic
- 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>
2026-09-29 10:12:20 +02:00
Meta Vens
0a80624930 cultmath: BoundedLeastSquares tests observe the warm-start clamp and the workspace
The full-step clamp repairs an out-of-box start on a free column, so the
warm-start clamp was invisible to the old test (Stryker: five survivors). The
new table runs one column with b beyond the box and a cap of 0, where the
solver returns the clamped start untouched, across finite, infinite and NaN
starts and finite and infinite bounds.

Workspace junk (including NaN doubles) is now checked on random boxed problems
that release bounds and use the tolerance, with bit-identical x and equal
iteration counts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:44:04 +02:00
Meta Vens
d808941b87 cultmath: BoundedLeastSquares sheds redundant checks and scratch arithmetic
Measured with Stryker once Solve was actually mutated (21 survivors), most of
them were code that decided nothing:

- The finite-input loops over A and b duplicated the non-finite test on the
  accumulated A^T b, which is exactly equivalent (floats cannot overflow a
  double product, and NaN/infinity never cancel to a finite value). One check
  on each A^T b entry remains.
- The tolerance scale is the gradient at the box point nearest the origin,
  computed inline; the max with |A^T b| duplicated it whenever the origin is in
  the box, and the scratch vector for it is gone.
- Bound violation is state * gradient, which is zero for a free column; the
  three-way ternary duplicated the free-stationarity check.
- The warm-start clamp is Math.Clamp plus a NaN test.
- Workspace slices come off a cursor (Take) instead of hand-summed offsets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:44:04 +02:00
Meta Vens
4072fce0e7 cultmath: BoundedLeastSquares drops the refinement step it cannot observe
Stryker left the refinement step (removed, negated, or subtracted) unobserved,
and it is unobservable for a reason: refinement with a same-precision residual
does not improve forward error, and Cholesky is backward stable, so the
residual the KKT test reads is already at rounding level. Measured on n=24
problems at cond(A) 1e3..1e6, the solver with and without the step gives the
same iteration counts and the same x error against a Gram-Schmidt QR
reference (both limited by cond(A^T A) * eps).

The refinement pass, its dp buffer and the scratch vector are deleted;
WorkspaceLength returns to 4n^2 + 15n. The doc states where the normal
equations stop being optimal (cost gap below 1e-6 |b|^2 through cond(A) 3e5,
about 2e-5 at 1e6, independent of the scale of A) and that a ridge row beyond
that is the caller's policy.

status is now initialised: the unassigned local made Stryker discard every
mutant inside Solve as a compile error, so 255 mutants were never measured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:29:06 +02:00
Meta Vens
a0baa542d0 CultNet: refuse hop edges whose citer is a variant; withdraw a refused hop subscription
An edge carries the reference payload of its citer's resolved document, and the
citer is never a selected row under `cited`, so a variant citer slipped past the
selected-row refusal. Edges are now checked by the same rule (typed
NotSupportedException naming the variant), on the paged and the whole-set paths.

Reconcile refused a hop subscriber whose selection selected a variant without
withdrawing it, so it was told again on every store change. It withdraws as the
subscribe path does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:20:48 +02:00
Meta Vens
282e337c2c Variants C1: the held index tells the truth; a merge is judged on what the file will hold
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>
2026-09-29 09:20:47 +02:00
Meta Vens
4f632fbf46 Drive the hop-subscription test through the database write path
Batch commits on the cache do not reach the database's change stream, so the subscriber's citers were written with database.PutAsync; the variant goes in through UpsertVariantAsync. On the previous source the second subscriber stops receiving after the variant exists (red); it now keeps receiving.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:42:32 +02:00
Meta Vens
b572823d0d Refuse variants after the selection is applied, and never tell a subscriber about the removal of one it was only refused
The selection paths (SelectPage, SelectAll) refused every stored variant before the selection ran, so once any variant existed a hop-bearing subscription threw out of its change handler and went silent. They now refuse only a variant the selection selects; the subscription server delivers that as a typed error and keeps its projection. The database server no longer sends removed for a key the subscriber only ever received as an error. A flatten racing the delivery check is recorded in a comment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:28:24 +02:00
Meta Vens
354f9743e6 Refuse a plain write that shares a variant's indexed value; a flush sheds overrides the type lacks
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>
2026-09-29 08:28:24 +02:00
Meta Vens
5d6847e220 Align the C1 tests with the unique-index rule and fix the live-subscription fixture
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>
2026-09-29 07:04:01 +02:00
Meta Vens
2845d3591b Regenerate the C1 variant vector: the variant overrides its indexed code
The unique-index rule refuses a variant that inherits its base's [CultIndex]
value, so the vector's laser-big now carries a code override. Written by the
code under test through the production path; the other runtimes still refuse
it by version and by the laser-big key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 06:59:29 +02:00
Meta Vens
c66d79e248 Variant overrides follow the plain-record outcome; bad writes and shared indexes are refused
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>
2026-09-29 06:58:51 +02:00
Meta Vens
cdb8ee5dcc CultNet refuses a variant on both live-change paths, loudly, by key
A load or write that landed a variant reached CultNet subscribers as a plain
"added" record carrying the full resolved payload; a later base edit published
only the base, so the subscriber's copy of the variant went stale. Under Q6
CultNet carries no variants until its own cut. Both live paths now refuse a
change whose key holds a variant, after selection and authorization match, and
send the matching subscriber a CultNetErrorMessage naming the variant and its
base. Refused loudly rather than omitted: an omission is a silent hole in the
subscriber's view, and the snapshot path already refuses by throwing.

Tests: a real load through a real RUDP subscription never delivers the variant
as a record and delivers the typed error; the database server's change builder
refuses the same change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 06:58:51 +02:00
Meta Vens
4a781b150c Record Soul on variants C1 and Self's defaults for its fix batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 06:48:42 +02:00
Meta Vens
de65c82768 Merge remote-tracking branch 'origin/main' into hands/variants-c1 2026-09-29 06:48:32 +02:00
Meta Vens
6982adfaaf cultmath: BoundedLeastSquares tests pin iteration counts and the b = 0 start
- n=24 fully free problems at cond 1e2/1e3/1e4, and A x 1e6 with unscaled
  bounds, converge in at most two iterations (a regression to cap-spinning
  fails on the count, not on a clock).
- b = 0 from a far start reaches the optimum instead of the start.
- Warm-start clamp table, free duplicate column gets no step, parallel columns
  at absolute scales 1e3 and 1e-3, near-singular free sets confirm once and
  stop, near-collinear optimum at A x 1e6, and zero allocation on the invalid,
  converged and cap paths (first and second call).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 05:36:50 +02:00
Meta Vens
43035bc827 cultmath: BoundedLeastSquares iterates in double and stops on progress
The working iterate now lives in double inside the workspace (WorkspaceLength
4n^2 + 17n); A, b and the API x stay float and x is converted in at the start
and out at the end. The gradient, the free-stationarity check and the KKT test
all run in the double domain, which removes the float noise floor that made the
free-stationarity re-solve spin to the cap on optimal points (cond >= ~20).

A fixed tolerance is no longer the only stopping rule: after a stationarity
miss or a release, a pass that lowers the double cost by no more than rounding
reports Converged.

The tolerance scale is the larger of the gradient at the origin and at the box
point nearest the origin, so it never depends on the warm start; b = 0 with a
far start is no longer judged converged by its own steepness.

Two clauses were equivalent or unobservable and are deleted: the d <= 0 pivot
test (implied by d <= tol * diag) and the dependent[] exclusion from the
free-stationarity check (the progress rule now terminates it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 05:36:50 +02:00
Meta Vens
5e521c832d Merge small follow-ups: Windows-only share-mode tests, a fixture generator spec
The three Caching tests that hold a FileShare.Read handle and expect a
delete or replace to fail are marked [Platform("Win")]. They cannot pass
on Linux, where unlink and rename ignore open handles. The Idunn runtime
fixture generator takes its sets as flags or @file response files, so a
consumer such as StreamPixels no longer patches CultLib's example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 05:12:15 +02:00
Meta Vens
c3add7dd1b Idunn runtime fixture generator takes its specification as input
The four hard-coded sets become flags, with @file response-file splicing.
A consumer keeps its own .args file instead of committing a patch against
the example. fixture.args carries CultLib own sets; the README documents
the flags. Committed fixture bytes are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 04:30:40 +02:00
Meta Vens
136adce9ff Prove TypeScript, Python and Rust refuse the C#-written variant store by name
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 04:21:59 +02:00
Meta Vens
b93934f6b4 Pin the variant rules a mutant survived or that lost their only test
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>
2026-09-29 04:21:59 +02:00
Meta Vens
33664434f9 Drop the unreachable single-record CultMesh variant refusal; tolerate duplicate catalog slots
A variant needs its base beside it, so a single-record store can never hold one:
that overload's refusal could not fire. SlotNames no longer throws on a catalog
that repeats a slot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 04:21:58 +02:00
Meta Vens
1ddf6e0d56 Mark the three share-mode tests Windows-only
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>
2026-09-29 04:17:31 +02:00
Meta Vens
fc78b9ad2b Test the C# variant path and add the C#-written variant store as a shared vector
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>
2026-09-29 03:52:35 +02:00
Meta Vens
e382bb4e20 Merge variants C0: every reader refuses what C1 will write, by name
C#, TypeScript, Python, Rust and CultMesh refuse an unknown
cultcache.store.* header and a fifth record slot, naming the version or the
key and schemaId. The silent slot-skipping and C#'s ignored header are
deleted, and CultMesh's second record decoder is gone. Shared vectors are in
tests/vectors/document-variants-c0/. v1 stores stay byte-identical. At base,
the single-file commit's read-modify-write rewrote a v2 store as v1, which
lost data; it now refuses.

Soul passed it. Cross-runtime interop was run and passes. Three C#
Caching tests fail on Linux at both base and tip: they are Windows
share-mode tests (a FileShare.Read handle blocking delete/replace), which
cannot pass on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:52:07 +02:00
Meta Vens
dbc87b9447 C# core: variant records, top-level Set overrides, resolution in the admitting hold
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>
2026-09-29 03:51:53 +02:00
Meta Vens
50f26de4a7 cultmath: BoundedLeastSquares factors the normal equations in double
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:49:29 +02:00
Meta Vens
44affbb9b6 cultmath: scale-free KKT, InvalidInput contract, free-set refinement
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:46:36 +02:00
Meta Vens
8b1a1ffde0 cultmath: near-collinear test uses generators that reproduce the gap on the old solver
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:44:57 +02:00
Meta Vens
6f8cb642ff cultmath: InvalidInput status and Soul tests (red on the old solver)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:43:32 +02:00
Meta Vens
6251cbcd37 Merge BP-1: the content plane in cultnet-rs
The Rust runtime gains the reference's content chunk messages on the raw
path, CultMeshCdnArtifactManifest with wire-ordered metadata,
normalize_hash, pack_content, validate_manifest,
answer_content_chunk_request (every bad request is answered, byte
identical to the reference server) and fetch_content (one owner of
receiving-side verification; refuses zero-size floods and unnormalised
hashes before any request). Vectors run both ways between C# and Rust.
The operator ruled Q-BP1 A: chunks travel on the session already held.
This is the explicit Rust content path until an authenticated connector
exists.

Soul passed it after two fix batches. cultnet-rs: content 24, cultnet 53,
rudp 7, lib 110. cargo-mutants on content.rs misses 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:31:20 +02:00
Meta Vens
5b538aaf07 Answer chunk requests whose string fields are nil
A C# caller that leaves ChunkHash, MessageId or RecordKey null writes msgpack nil; the
reference reads it back as null, treats it as blank and answers found:false. Rust now
decodes nil as blank in content chunk requests, so those requests are answered with the
reference bytes instead of dropped. Three nil vectors from the reference server are
judged by decoding them in Rust. Record two content-plane limits in the parity scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:29:41 +02:00
Meta Vens
30ee8b97ab Merge route continuity C1: local route proof in the TypeScript Idunn runtime
The runtime splits into a signer (load, sign, answer a route challenge
locally, no Odin, no network) and an optional Odin presence publisher for
CultMesh-aware services. The signer owns health (reportHealth, starting at
warming), records every warming it signs as a lease target, allows one
signer per frozen authority, and ships a subpath export that does not
load the publisher. Rust judges TS output: committed vectors from
examples/idunn_runtime_fixture.rs, verified by the Rust authenticator and
correlator, including a stateful lease-bound set.

Soul passed it after two fix batches. cultnet-ts has 113/113 tests on
node:24. StreamPixels' web and service call sites move in C2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:27:19 +02:00
Meta Vens
5ed9dbbcb8 cultnet-ts: stop exporting signIdunnRuntimePresence; document two limits
The function took state and lease digest from the caller, so anyone holding the
authority could sign active without reportHealth or assertWriteLease. It is now
private to the signer. Test: the subpath and root do not expose it. Docs:
waitForWriteLease throws out of publish if health is already non-warming;
MAX_RECENT_WARMING_PROOFS=64 is a recorded limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:26:10 +02:00
Meta Vens
7584a5c08a Merge remote-tracking branch 'origin/main' into hands/variants-c1 2026-09-29 03:20:38 +02:00
Meta Vens
a5edd976d0 Sweep Q3 into C4: no per-type opt-in
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:20:30 +02:00
Meta Vens
4cdc8864a2 Sweep Q3 (ids everywhere, minted on load) into the C2 spec
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:20:11 +02:00
Meta Vens
addaf10c22 Sweep the Q4 reversal into the C1 spec: overrides soft-drift
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:19:42 +02:00
Meta Vens
986711c9d5 cultmath: tests for Stryker survivors (dirty workspace, exact caps, spans, near-duplicates)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:19:28 +02:00
Meta Vens
322b7c815a cultnet-ts test: read the signed document, not the SignedRuntimePresence wrapper
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:15:40 +02:00
Meta Vens
06d250070a cultnet-ts: add an idunn-runtime subpath export that does not load dgram
`cultnet-ts/idunn-runtime` exposes the signer and loader alone. Web apps import
it instead of the package root, which also carries the Odin publisher and
node:dgram. The root export is unchanged. Test loads both under a stubbed
dgram: the subpath loads, the root fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:13:37 +02:00
Meta Vens
67b8ae3eda cultnet-ts: freeze the Idunn authority and allow one signer per authority
The loaded authority is deeply frozen (bind, health contract, Expected,
Activation, identities), so a caller cannot steer what the signer signs; the
typed Buffers that could not be frozen are dropped (Expected canonical bytes,
Activation public key kept as hex only for the load-time comparison). A second
createIdunnRuntimeSigner on the same authority throws, so two signers cannot
emit sequence 1 under one runtimeInstanceId.

Tests: stateful set pinned against Rust (warming, active, degraded), lease
accepted for a route-answered warming, refused for another warming and another
target, one signer per authority, frozen authority.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:13:28 +02:00
Meta Vens
8072314b7c cultnet-ts: the Idunn signer owns the reported health and warming proofs
The signer holds the last reported health, starting at warming, and gains
reportHealth(state). sign(detail), answerRouteObservation(request) and the Odin
publisher's publish(detail) no longer take a state, so the route answer and the
Odin publish cannot disagree and a fresh signer cannot answer active. Every
warming presence the signer signs becomes a valid write-lease target; the
publisher's recordPublishedWarming is deleted. Tests now assert every committed
stateless vector, dead ones included.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:11:48 +02:00
Meta Vens
4ec75eec18 cultnet-ts: regenerate Idunn runtime fixtures with the stateful lease-bound set
Output of the Rust generator (3c9ce38) on Yggdrasil. Every set is re-enrolled, so
all bytes change; adds service-stateful with lease.cc, the two wrong leases and
the warming/active/degraded vectors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:10:55 +02:00
Meta Vens
3c9ce3823c cultnet-rs fixture example: add a lease-bound stateful Idunn set
Adds `service-stateful` (streampixels-service shape, state lineage set,
write_lease_required) with a Rust-issued lease.cc, warming/active/degraded
vectors bound to it, and two wrong leases (other warming digest, other target).
Each vector still passes authenticate + correlate before it is written.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:05:12 +02:00
Meta Vens
a7eb9967e9 Pin the metadata decode message so the last content.rs mutant is caught
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:59:59 +02:00
Meta Vens
32fb7c1503 cultmath: FISTA reference keeps the random KKT test fast
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:52:51 +02:00
Meta Vens
66bd07b941 Pin the legacy-layout and missing-header refusals
The legacy catalog layout reaches CultMesh's fallback reader, and a nil header
was the only defaulted header; each gets one vector or test. The C# store
tests catch the refusal where the store attaches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:35:57 +02:00
Meta Vens
36aa8d8791 Rust: drain the header peek so rmp-serde accepts the array
The peek left snapshot slots unread and rmp-serde refused the length
mismatch, so every store fell through to the legacy decode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:34:11 +02:00
Meta Vens
c65777b400 cultmath: stop the projected-gradient reference once it stops moving
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:33:51 +02:00
Meta Vens
5382b81036 cultmath: widen fixed-column fixture so no other column blocks
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:14:39 +02:00
Meta Vens
dc9162f85d Correct the content-plane parity claim for metadata order and bad requests
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:12:44 +02:00
Meta Vens
16432d4b1e Fix the BP-1 findings in the cultnet-rs content plane
A hostile manifest of empty chunks no longer becomes a request flood: fetch_content
refuses a zero-size chunk beside other chunks, and settles the content hash and every
chunk hash (normalised, non-empty) before the first request.

Every bad chunk request is now answered found:false in the reference handler's
validation order, never refused at decode. validate_message no longer refuses content
chunk requests or responses, so a blank message id or hash, a prefix-only hash and a
negative size reach answer_content_chunk_request. The answer follows the reference's
double normalisation (handler, then CreateRecordKey), which fixes the prefix-only
hash spelling to ArgumentException on 'chunkHash'. The cs vectors gain nine
reference-server answers (blank and whitespace id, blank, whitespace and prefix-only
hash, negative size, record-key case, blank record key, double prefix) and a
metadata-order manifest; Rust answers all of them byte-identically.

Manifest metadata is Vec<(String, String)> in wire order and refuses a duplicate key,
replacing the BTreeMap that re-sorted keys.

Tests pin the surviving mutants: unusable chunk sizes, the payload-length and
claimed-size halves of the chunk check, stable sort at equal offsets, the normalised
hash and canonical key actually sent, and the kind/mime defaults never trimming.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:12:43 +02:00
Meta Vens
795fd1023c cultnet-ts: split Idunn authority from the Odin publisher; answer route challenges locally
idunn-runtime-authority.ts now loads and signs only: no Odin endpoint, no Odin
dependency check, no RUDP. createIdunnRuntimeSigner signs presence and answers
Idunn's route challenge with answerRouteObservation(request, state), a
synchronous local signature in the state the app reports. The RUDP publisher
moves to idunn-odin-presence-publisher.ts for services that declare
odin.verse-rendezvous, and keeps the Expected-endpoint check.

Deleted: the TS capability-minimum refusal (the Rust correlation records the
typed disagreement), latestPresenceDocument, the forced "active", and the
publisher's redundant health-contract and bind checks. Route validation now
accepts http, tcp and rudp as Rust does; the machine-id falls back to
/var/lib/dbus/machine-id. Tests run against the Rust-written fixtures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:05:14 +02:00
Meta Vens
6b107c1adc cultnet-ts: Rust-generated Idunn runtime fixtures
Expected, Activation, identities and signed presence vectors from the cultnet-rs
example; the README says how to regenerate them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:05:14 +02:00
Meta Vens
95f04c1efb Rust: name the record and the version when a store is refused
PersistedRecord refuses a fifth slot naming its key and schema id, and an
unknown cultcache.store.* header no longer falls through to the legacy
envelope decode that hid the cause.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:04:34 +02:00
Meta Vens
c8777fe08d Python: refuse unknown store headers and extra record slots by name
Delete the raw_record[:4] slice and the legacy-envelope fallthrough for
unknown cultcache.store.* headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:04:34 +02:00
Meta Vens
569d668141 TypeScript: refuse unknown store headers and extra record slots by name
The store reader and the inspector share one header and record-slot rule;
an unknown cultcache.store.* header no longer falls through to the legacy
envelope decoder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:04:34 +02:00
Meta Vens
98ed042fcc C#: refuse unknown store headers and extra record slots by name
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>
2026-09-29 02:04:33 +02:00
Meta Vens
2cb694d612 Pin the C0 refusal vectors shared by every runtime
An unknown store header, a record slot beyond four with a full payload, the
v2 variant record C1 will write, and a v1 store written by the base-commit
Python writer. Every runtime's tests read these bytes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:04:33 +02:00
Meta Vens
69a21bb057 Record the operator's rulings on document variants Q1-Q7
Q3 is ids everywhere, minted on load (not opt-in). Q4 is soft drift for
dangling overrides, which supersedes one of the target's refusals. The
others were ruled as recommended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:55:09 +02:00
Meta Vens
9ba6ac4a2e cultnet-rs fixture example: name an Odin port a dev machine does not run
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:44:04 +02:00
Meta Vens
f5c597a81d cultnet-rs fixture example: remove the store lock files it leaves
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:40:30 +02:00
Meta Vens
eec1e40c25 cultmath: pin that a fixed column is never released
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:38:56 +02:00
Meta Vens
1e86bf0dc3 Place the document variants target and cut map on main
The target (2026-09-17, with the operator's rulings) lived only on
codex/document-variants. The cut map is Imagination's pass of 2026-09-29.
Its operator questions Q1-Q7 are open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:34:42 +02:00
Meta Vens
60c3d6fb0d cultmath: BoundedLeastSquares primal active-set solver
Dense small bounded LS, zero allocation, caller workspace, C#-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:33:08 +02:00
Meta Vens
0e7d2204cd cultnet-rs: example generating Rust-owned Idunn runtime fixtures
Real enrolled identities, a validated Expected, an Activation issued by
IdunnRuntimeActivationLaunch, bundle files through the CultCache store, and
runtime-presence vectors signed by the Rust signers and run through
authenticate/correlate before they are written. cultnet-ts tests will commit
the output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:32:57 +02:00
Meta Vens
de57d8dc9c Merge the Idunn TypeScript runtime into main (route continuity Q6)
The operator ruled Q6 (a) on 2026-09-29: this branch lands on main before
route continuity cut C1 changes it. What lands is the ten 09-28 commits
that StreamPixels pins at 542ddd3, plus the Soul-gated fixes:
- runtime presence and daemon-health details are bounded by UTF-8 bytes
  through one helper, made well formed first, matching the Rust owner's
  len() > 512;
- the dead readAuthorityRecordBytes is deleted;
- the TypeScript packages require Node >= 20.

Soul confirmed byte-level wire parity with Idunn's Rust owners on real
Rust-written bundles, including the full write-lease flow. cultnet-ts has
90/90 tests on node:24. Capability-floor split authority, forced active
and the http-only route filter are recorded for C1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:21:49 +02:00
Meta Vens
0791e1b7d0 Raise the TypeScript packages' Node floor to 20
Some checks failed
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
The UTF-8 detail bound calls String.prototype.toWellFormed, which
Node 20 introduced. Node 18 reached end of life in April 2025, and CI
runs Node 24. All four TypeScript packages move together so the family
keeps one floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:21:18 +02:00
Meta Vens
a957f07539 cultnet-ts: one UTF-8 byte bound for presence and daemon health detail
signed-daemon-health sliced detail at 512 UTF-16 units while the Rust owner
counts bytes, so 300 x "é" failed shape validation. truncateUtf8Bytes now
serves runtime presence and both daemon-health sites, replacing
truncateRuntimePresenceDetail. It makes the string well formed first, so a
lone surrogate becomes U+FFFD instead of invalid UTF-8 in msgpack. The
unsigned payload is extracted as unsignedDaemonHealthPayload so it can be
tested without a socket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:19:41 +02:00
Meta Vens
8e578ea2ab cultnet-ts: delete unused readAuthorityRecordBytes
Never called; removed with nothing else depending on it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:12:15 +02:00
Meta Vens
8b8650221b cultnet-ts: bound runtime presence detail by UTF-8 bytes
The Rust owner refuses detail over 512 UTF-8 bytes; TypeScript counted
UTF-16 units, so 300 x "é" was signed and then refused, and a slice inside a
surrogate pair produced invalid UTF-8. One helper,
truncateRuntimePresenceDetail, now cuts on a code point boundary at 512
bytes and serves both the publisher and validateRuntimePresence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 01:12:15 +02:00
Meta Vens
8a66b1d230 Commit the Rust-written content vectors and document the Rust content path
content-vectors.rs-written.json is written by cultnet-rs and is identical
to the reference's file. transport-planes.md and content-sessions.md name the
Rust runtime's content path as the explicit compatibility path on the session
the client already holds, lasting until an authenticated content connector
exists behind the same fetch_content owner; runtime-parity-scope.md gets the
evidence row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 00:51:55 +02:00
Meta Vens
11485fb491 Pin the content plane against the reference's bytes
content-vectors.cs-written.json is written by CultMeshContentVectorTests on
Linux, and includes the answers CultMeshLegacyRudpContentServer gives, so the
failure spellings are the reference's. tests/content.rs judges it. The
rs-written mirror follows in the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 00:49:58 +02:00
Meta Vens
29c3e4c2b5 Add the content plane to cultnet-rs: chunk messages, pack, answer, verified fetch
ContentChunkRequest/Response join CultNetMessage on the raw path, byte
identical to the reference's field order. content.rs owns normalize_hash,
pack_content, validate_manifest, answer_content_chunk_request and
fetch_content, mirroring CultMeshCdn and CultMeshLegacyRudpContentServer.
The C# vector test is added here; the vector files and Rust tests follow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 00:48:13 +02:00
Meta Vens
542ddd3e58 Bind runtime presence to route challenges 2026-09-28 03:54:21 +02:00
Meta Vens
265f716391 Expose published Idunn presence snapshots 2026-09-28 03:29:01 +02:00
Meta Vens
02f13527eb Use explicit Ajv ESM entrypoint 2026-09-28 02:51:12 +02:00
Meta Vens
564d5bf732 Fix Idunn runtime presence RUDP publication
Some checks failed
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
2026-09-28 02:11:12 +02:00
Meta Vens
2c5dc3e278 Decode Idunn identity byte vectors 2026-09-28 01:00:52 +02:00
Meta Vens
5dd25718af Read Idunn identity envelope format 2026-09-28 00:52:03 +02:00
Meta Vens
5697802dbf Read inherited Idunn descriptors directly 2026-09-28 00:35:15 +02:00
Meta Vens
71aa615d93 Allow systemd descriptors in PID namespace init 2026-09-28 00:24:02 +02:00
Meta Vens
21dfd83211 Validate systemd descriptor PID across namespaces 2026-09-28 00:10:26 +02:00
Meta Vens
0bedcdeb37 Add TypeScript Idunn runtime presence publisher 2026-09-27 20:55:49 +02:00
Meta Vens
c9dabbeef6 cultmath: fix stale test-name references in snoise_grad/fbm_grad/ridged_grad comments
The doc comments named test classes/methods (SnoiseGradTests, FbmGradTests,
RidgedGradTests) that were never committed under those names; the actual
coverage lives in NoiseGradTests. Point the comments at the real methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 03:05:08 +02:00
Meta Vens
bbf00394de tests: behavioural coverage for snoise_grad, fbm_grad, ridged_grad
snoise_grad.w is bit-equal to snoise (stronger than the spec's 1e-6
floor, measured over 20,000 points). fbm_grad/ridged_grad with 1/3
octaves are pinned bit-exact against a closed form built with the same
incremental gain/lacunarity multiply chain the production loop uses,
catching both a swapped-argument mutant and an applied-once mutant.
The shared invariant-8 property harness central-differences all three
functions' own .w, filtering simplex-lattice seam flukes by comparing
the estimate at two step sizes (measured tolerances and detection
floor in the harness comment). A dedicated fixture bisects to a snoise
zero crossing and pins that ridged_grad's two one-sided gradients there
have opposite sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:54:17 +02:00
Meta Vens
256f8e20be tests: cover snoise_grad, fbm_grad, ridged_grad mirrors in HLSL compat test
Adds explicit function-presence assertions and bumps the reflective
bit-parity oracle's expected mirror count from 30 to 33 now that the
three new cultmath_ mirrors are covered automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:54:17 +02:00
Meta Vens
03f0ed7a0a docs: describe snoise_grad, fbm_grad, ridged_grad in cultmath design.md
Extends the invariant-8 primitives list with the new gradient-noise
family: the constant-retargeting for snoise_grad, the per-octave
frequency/amplitude chain rule, ridged_grad's deliberate zero crease,
and the octave clamp's reason (the HLSL mirror test's bit-parity
oracle).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:37:05 +02:00
Meta Vens
1249167989 cultmath: mirror snoise_grad, fbm_grad, ridged_grad to HLSL (cut 2a-ii-a)
cultmath_snoise_grad/fbm_grad/ridged_grad mirror the new C# math
functions in both tracked CultMath.hlsl copies, which stay one blob.
Extends the existing Ashima notice entry in both THIRD-PARTY-NOTICES.md
copies to cover the ported differentiation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:36:59 +02:00
Meta Vens
e87dcc9ec5 cultmath: add snoise_grad, fbm_grad, ridged_grad to C# math (cut 2a-ii-a)
Analytic-gradient simplex noise, ported from webgl-noise's
noise3Dgrad.glsl differentiation but retargeted onto this repo's own
snoise(float3) constants (0.6 falloff radius, +1 permute offset, 42
scale) so snoise_grad.w stays exactly snoise's own field. fbm_grad and
ridged_grad are octave sums with per-octave frequency/amplitude
compounding (invariant 8). octaves is clamped to [0, 16] so the HLSL
mirror test's full-int32-range oracle cannot turn a garbage octave
count into a multi-billion-iteration loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:36:52 +02:00
Meta Vens
268e0ef17c Merge cellular, smooth-min gradient and the struct mirror into CultMath (Asura cut 2a-i)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:24:47 +02:00
Meta Vens
4922438334 cultmath: correct F6 proof arithmetic and chi-square provenance in cellular docs
Soul pass 3 confirmed two documentation defects: the F6 search-radius proof
misstated 3 - 2m + 3m^2 at m=1/2 as 2.5 (actual 2.75; true min on [0,1/2] is
8/3 at m=1/3), and both math.cs and design.md cited a fabricated 203.8
chi-square statistic for float-bit hashing that Soul could not reproduce
(6+ seeds scored 7.7-30.9). Corrected the arithmetic and replaced the
unreproducible measurement with what actually pins integer hashing: the
oracle and mirror tests, and the chi-square test's real scope (the hash as
cellular calls it, not the call site itself). No test or code changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 02:23:55 +02:00
Meta Vens
05ca826091 tests: pin cellular's HLSL mirror against radius-2, 2^24, and tie inputs
EveryMirrorFunctionMatchesCSharpMath's generic random/special-value cases
never happen to hit a point where a radius-2 cell wins (about 3.5e-5 of
points) or an integer point at or past 2^23, so an HLSL mutant of cellular's
search radius, its lower-bound prune, or the removed F2=0 guard could change
cultmath_cellular's real output and still pass this test.

Adds three targeted point cases, reusing CellularAndSminGradTests' own
fixtures so the mirror test does not carry a second copy of the construction:

- the six points from AxisAlignedRadiusTwoOffsets/FindPointRealizingOffset,
  each of which realizes a different axis-aligned radius-2 offset as F1 or
  F2 in the independent 7x7x7 oracle;
- four integer points at 2^24 (both signs), where distinct cells' jittered
  features can round onto the same float32 value, reaching the F2 = 0 guard;
- one exact F1 = F2 tie point, the midpoint of two adjacent cells' feature
  points (bit-exact-equidistant from both by construction, no search
  needed).

Verified against four HLSL mutants applied to shaders/CultMath.hlsl on
scratch copies (not committed) and reverted after each check:
- `dz < 2` instead of `dz <= 2`: killed by the axis-slice point at
  (-56.671272, -67.01973, -79.001).
- `dx` starting at -1 instead of -2: killed by the axis-slice point at
  (62, -77.91331, -79.42451).
- pruning against f1 instead of f2: killed by an axis-slice point and both
  of the 2^24 integer points (3 mismatches).
- the F2 = 0 guard removed (`grad2 = (p - c2) / f2`, unconditional): killed
  by the (2^24, 2^24, 2^24) integer point (NaN vs the guarded zero).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:14:10 +02:00
Meta Vens
91596884b0 tests: fix hashing fixtures, false comments, and gradient tolerance
CellularAndSminGradTests.cs changes for cut 2a-i's pass-2 findings:

- FeaturePoint and the two other independent pcg3d/pcg4d call sites now hash
  int3(cell)/int4(int3(cellId), 0) to match production's integer-hashing fix
  (previous commit); the oracle would otherwise diverge from cellular() and
  every comparison test would fail.

- New JitterAndIdPassAChiSquareUniformityTest: jitter.x/y/z and id each pass
  a chi-square uniformity test (16 bins, 15 df, critical value 37.7 at
  alpha=0.001) over 200,000 cells. This is the behavioural pin for the
  hashing fix; reverting to pcg3d(cell)'s float-bit hash fails it.

- Two false comments corrected to the measured facts: the
  NearestFeatureWins test's claim that |component|=2 offsets are "provably
  never the true nearest" is false (their lower bound is 1, not sqrt(3); Soul
  measured 3 F1 wins in 2,000,000 samples), and SearchReachesEveryAxis...'s
  claim of "lower bound is exactly 2" and "zero hits in 3,000,000" is also
  false (lower bound is 1; Soul measured about 1 hit in 28,000).

- PrunedSearchIsBitIdentical... renamed to
  ...BelowTwoToTheTwentyFifth and its comment states that domain explicitly
  (design.md, previous commit).

- GradientTolerance split into SminGradientTolerance (2.5e-4f) and
  CellularGradientTolerance (1.3e-3f): the two functions have different
  curvature (smin_grad is exactly quadratic in a band; cellular's F1/F2 are
  real distance fields), so a shared 3e-4 tolerance carried almost no
  headroom over either's actual measured noise (re-measured here at 5.3e-4
  for smin's old eps, 2.0e-4/2.6e-4 for cellular), not the "~6e-5" the old
  comment claimed. smin_grad's eps moves from 0.001f to 0.01f (no truncation
  cost there; dilutes float noise 8x) with a matching widened seam-exclusion
  band so p +/- eps cannot step across the seam the check at p already
  avoided. cellular's eps stays at 0.0005f (a small sweep found it already
  near the truncation/noise sweet spot for a real distance field). Both new
  tolerances keep >5x headroom over their own measured worst case and both
  still die under a 1.01x-scale mutant on the returned gradient — smin down
  to 1.0005x, cellular (M9/M16-style, either gradient) down to 1.002x.

- GradientMatchesCentralDifferencesAwayFromTheF1EqualsF2Seam also excludes a
  band around F2 = F3 now, not just F1 = F2: the second-nearest feature's
  identity swaps there too, a genuine kink the old test could silently hit
  (an unfiltered large sample found a 0.78 edge error this way, swamping the
  ~2e-4 the filtered test actually sees).

- AxisAlignedRadiusTwoOffsets and FindPointRealizingOffset are extracted from
  SearchReachesEveryAxisAlignedRadiusTwoSlice as internal fixtures so
  HlslSourceCompatibilityTests (next commit) can reuse the same construction
  instead of a second copy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:50 +02:00
Meta Vens
cb94b74ae3 docs: correct F6 proof, integer hashing, and the prune's precision domain
design.md follow-ups matching the math.cs/HLSL fix in the previous commit:

- The F6 "5x5x5 is exact" argument is rewritten with the corrected near-side
  neighbour bound (choose the axis closest to its own boundary, not a fixed
  axis), the m <= 1/2 derivation, and the counterexample that showed the old
  argument wrong. Same text as the math.cs comment, so the two do not drift.

- "Integer hashing" now states that cellular hashes the winning cell's
  integer coordinate, not its float32 bit pattern, and records the measured
  chi-square evidence (203.8 over 1,000,000 cells, 15 df, critical value
  37.7) for why the old bit-pattern hash failed a uniformity check.

- "Precision domain" gets a new paragraph: the lower-bound prune's bit
  identity to the unpruned 5x5x5 search holds for |p| < 2^25, not
  unconditionally. Above that, cell + offset itself starts to round in
  float32, so the pruned and unpruned loops can visit different actual
  neighbours at the same nominal offset (Soul, cut 2a-i: 180 of 1,100,000
  points differ at |p| ~= 3.4e7). This is a float32 domain limit on the
  equivalence proof, not a bug in the prune; the committed test's [-50, 50]^3
  sweep is comfortably inside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:25 +02:00
Meta Vens
9207a59280 cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:

- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
  integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
  instead of the integer coordinate itself. Two adjacent cells' bit patterns
  share more structure (same exponent, mantissa one increment apart) than
  the integers do going into pcg3d's own mixing, and that structure survived
  into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
  cells against 15 degrees of freedom (critical value 37.7). Both call sites
  now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
  HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
  as before). This changes cellular's noise output — nothing has shipped, so
  that is acceptable, but it is a real behavioural change, not a comment fix.

- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
  picking the near-side neighbour on a FIXED axis is not always within
  sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
  jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
  exclusion). The comment is rewritten to the correct argument: choose the
  axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
  1/2 on that axis), and the near-side neighbour on THAT axis has squared
  distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
  strictly below 3 since the jitter never reaches the boundary. Margin
  2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
  sqrt(3), only in the unattainable corner limit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
Meta Vens
e739ff1e52 tests: kill a single-slice loop-bound gap in cellular's 5x5x5 search
Stryker (mtp, --since 29e50ad3) found 261 survivors / 24 timeouts across the
whole repo; triaging the ones in this fix batch's own new code (cellular's
loop, its prune, cellular_unit) surfaced one real gap disguised as
collateral timeout noise from a batched infinite-loop mutant: dz < 2 (and
the dy/dx equivalents), silently dropping the +2 layer on one axis alone,
passed every existing random-sampled test. That is not the sweeps being
weak: the exactness proof's own margin (F1, F2 always < sqrt(3), radius-2
cells' lower bound is exactly 2) makes a genuine win from any single
|offset component| = 2 cell astronomically rare over uniformly random
points (measured zero hits in 3,000,000 samples).

SearchReachesEveryAxisAlignedRadiusTwoSlice constructs a real example for
each of the six axis-aligned radius-2 offsets directly: for a candidate
integer cell, build the query point that is the closest point of that
cell's own unit cube to the target neighbour's actual jittered feature.
That construction finds a hit for every offset within a couple thousand
candidate cells, deterministically. This kills dz < 2 / dy < 2 / dx < 2 (all
manually confirmed).

Also manually confirmed via Stryker's own reported mutants: the dz--/dy--/
dx-- (increment-to-decrement) mutants are genuine infinite loops, correctly
caught as timeouts. The f2 >= 0.0f mutant (disabling the F2 = 0 guard) is
killed by the existing NoNaNAtIntegerPointsNear2To24 test. The remaining
survivors/timeouts in the new code (the lower-bound prune's >>= vs >
comparison, its arithmetic terms, and cellular_unit's >> vs >>> shift on an
already-unsigned operand) are equivalent mutants: since the per-axis lower
bound is always 0 or 1 by construction, and pruning is a proven-safe
optimization that never changes cellular's output, no mutation confined to
"when does the prune trigger" can be observed by any value-based test.
Verified by hand: disabling pruning entirely reproduces the unpruned
reference bit for bit, as it must.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 21:28:36 +02:00
Meta Vens
4163001c91 Merge surface nets and face-weighted normals into GameCult.Geometry
Asura Cut 2 (GameCult/Asura docs/cut-map.md). CultGeometrySurfaceNets.Extract
emits a welded quad mesh with each quad named by the grid edge it crosses,
(cell, axis), in strictly ascending order; orientation comes from the inside
flag and a per-axis handedness table, one authority. Face-weighted normals
are area-weighted via quad diagonals. Non-finite inputs are rejected.
Stryker is scoped to GameCult.Geometry.

Closed after three Soul passes; byte-identical behaviour across the
closing refactor on 31 fixtures. The 7 GeometryDocumentTests failures
predate this merge (e420410, registry check D11) and are tracked
separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 21:05:58 +02:00
Meta Vens
3b1ea71eef docs: correct cellular/smin_grad design.md to match fix batch 1
- smin_grad is Quilez's quadratic-polynomial smin, not cubic.
- cellular's search radius is 5x5x5 (F6), not 3x3x3; adds the exactness
  proof and the pruning claim.
- id source is now pcg4d over the winning cell, not the pcg3d hash that
  produced its jitter (F4).
- F2 = 0 is documented as reachable (past |p| of about 2^23), not
  unreachable; removes the false "unreachable" claim and adds a "Precision
  domain" note stating the float32 feature-spacing/tie behaviour honestly
  (F3, minor finding).
- The struct-mirror comparison note now says it also matches field name and
  type in declaration order, not just position (F2).

No entry added to THIRD-PARTY-NOTICES for the Quilez/Worley formulas: per
Self's standing ruling, a published formula is not ported code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:43 +02:00
Meta Vens
f6fb589ac3 Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):

- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
  3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
  comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
  are always < sqrt(3), every excluded cell is always > 2). A cheap
  position-only lower-bound prune skips the pcg3d/jitter/distance work for
  cells that provably cannot beat the running F2, without changing any
  output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
  fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
  hash that produced that cell's jitter, so id and jitter are no longer
  deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
  the mapping is always inside [0, 1); the old formula rounded
  0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
  noise at the committed eps is about 6e-5), and cellular's own gradient
  test now differentiates cellular's RETURNED .w values instead of a
  duplicate FindF1F2 search, so a mutant that corrupts only the returned
  gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
  field name and declared type in declaration order, not just position, so
  a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
  (H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
  pin production's exact agreement with it, the pruned search's bit-for-bit
  equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
  correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
  cubic.

Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.

Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
Meta Vens
d64de1b69d fix: reject non-finite isoValue and origin in Extract
samples and cellSize were validated but isoValue and origin were not:
iso = NaN or +inf silently returned an empty mesh, and origin = NaN gave
NaN positions. Extend Extract's existing top-of-method validation authority
to reject non-finite isoValue and non-finite origin components with
ArgumentException, matching the existing style, with tests for each
component.

Overflow at extreme magnitudes (cellSize ~3e38, or finite samples whose
interpolation overflows to inf) is a separate, float-domain limit rather
than a validated-input error; documented in the XML doc and provenance
notes, deliberately not range-checked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:31:44 +02:00
Meta Vens
bef3d3eba3 refactor: replace TryGetOtherEndpoint's dead bool with a structural loop bound
TryGetOtherEndpoint's bool return was dead control flow: when a mutant
flipped its `return false` guard to `return true`, `other` stayed a clone
of `coord` (only one component would have been incremented on the true
path that never runs for that guard), so the caller compared a sample
against itself and behaved identically either way. That produced the
equivalent survivors at Extract's :92 continue and the guard's :198
return.

Bound the edge loop with the same condition (coord[axis] < cellCounts[axis])
directly, computing `other` inline once the bound holds. One path instead
of two that happened to agree; the helper is gone, and so is the `continue`
that used to depend on it. Behaviour is unchanged: verified with a
before/after run of the existing suite plus a temporary harness comparing
Extract's Positions/Quads/QuadEdges against ca220a5's code on sphere,
non-cubic, open-plane (each axis), checkerboard, and minimal 2x2x2
fixtures - all byte-for-byte identical.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:30:19 +02:00
Meta Vens
b4db5cb6ed test: cover CultGeometryGridEdge.Equals(object) behaviourally
Mutating obj is CultGeometryGridEdge other && Equals(other) to && true
passed the suite: nothing exercised object.Equals directly with a mismatched
X/Y/Z/Axis, null, or an unrelated type. Add a behavioural test that checks
each component independently, rejects null and other types, and agrees with
the typed Equals and GetHashCode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:29:00 +02:00
Meta Vens
15d172df3f docs: state the actual quad-winding rule in SURFACE-NETS-PROVENANCE.md
The provenance doc still described the deleted geometric winding rule
(diagonal cross product must have a positive dot product with the
inside->outside direction) as the spec. That rule was replaced by the
inside-flag times per-axis handedness table (OrientationSign: +1, -1, +1;
axis 1's perpendicular pair (X, Z) is left-handed) when the geometric
fallback was deleted. State that as the actual rule, with the
outward-cross-product property following only on non-degenerate quads.
Also record that non-finite isoValue/origin are rejected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:28:21 +02:00
Meta Vens
396b2996a5 docs: correct the mutation-testing.md false-survivor claim on OrientationSign
The `==`->`!=` and `< 0`->`> 0` mutants on Extract's orientation check are
both Killed. The one survivor, `OrientationSign[axis] <= 0`, is an
equivalent mutant, not a coverage gap: OrientationSign only ever holds ±1,
so `< 0` and `<= 0` agree on every value it can produce. No vstest
coverage-based false survivor has actually been confirmed for
GameCult.Geometry; remove the withdrawn claim and keep the correct notes
about full-SHA --since, safe.directory, and config-change scope fan-out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:27:43 +02:00
Meta Vens
ca220a5d35 Record a confirmed vstest false survivor on the new orientation authority
Stryker's diff-scoped run against this cut reported CultGeometrySurfaceNets
.Extract's `if (inside == (OrientationSign[axis] < 0))` - the sole authority
for quad winding after F4's fix deleted the geometric cross-product fallback
- as a Survived Equality mutation (== -> !=). Hand-mutating it and running
dotnet test directly failed 4 of 38 SurfaceNetsTests (the three per-axis
corner-order tests and the outward-winding test), confirming it is a real
vstest coverage-based test-selection miss, not a genuine gap. Recorded this
as a second confirmed case alongside the mtp/static-readonly-initializer
caveat, per this cut's verification requirement to hand-confirm any survivor
that looks like it must be covered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:58:54 +02:00
Meta Vens
1add078eea Delete cellular's unreachable F2=0 guard instead of leaving it undefended
Stryker (scoped to this cut's diff) survived two mutants on the grad2 ternary
in math.cellular: forcing the true branch unconditionally, and loosening
f2 > 0.0f to f2 >= 0.0f. Both are real findings about the code, not the
tests: F2 = 0 needs two distinct integer cells' pcg3d-jittered feature points
to land on the exact float32 value in every component, which the jitter's
~24-bit float precision makes unreachable for any p this function is
actually called with. The spec only asks for the degenerate guard at F1 = 0;
F2's copy of it was added for symmetry and had no reachable case to defend.

Deleted the guard (grad2 is now a bare (p - c2) / f2) in math.cs and both
HLSL mirrors, and corrected design.md's invariant-8 section, which had
overstated this as symmetric with F1's guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:47:36 +02:00
Meta Vens
13a11ed64a Correct F2's vstest survivor triage in mutation-testing.md
The three TryGetOtherEndpoint survivors were triaged as a vstest coverage
false-survivor, mirroring the confirmed mtp/static-readonly caveat above.
That triage was wrong: the actual mutant is `return false` -> `return true`
inside each per-axis guard, which is equivalent (it makes the method report
an edge going nowhere, so the caller compares a sample against itself and
skips it exactly as it would with a correct `false`). Rewrote the paragraph
to say so, and removed the unsupported claim that this project has a
confirmed vstest false-survivor case. Also recorded, per this cut's
verification requirements, that `--since` needs a full commit SHA and that
`safe.directory` must be set in a container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:30:29 +02:00
Meta Vens
6a4b0ed21f Add tests for surface-nets F1/F3/F4/F6/F7 mutation findings
- Fixed An_extremely_small_edge_delta_falls_back_to_the_edge_midpoint (renamed
  A_tiny_but_normal_edge_delta_...): it pinned the wrong value (1/6) for the
  fallback that F3's production commit deleted; now asserts the exact
  interpolated value, hand-derived in double precision independently of
  Interpolate. Added a genuinely subnormal-delta sibling test.

- Added two more vertex-placement fixtures (crossing counts 4 and 6) to kill
  the sum/count -> sum/3 mutant, each with a hand-derived expected mean.
  Counts of 1 and 2 from the finding are not used: a single cell's 12 edges
  form the cube graph Q3 (3-regular, 8 vertices), whose edge-cut size for any
  corner sign pattern is never 1 or 2 for any nonzero cut (minimum nonzero
  cut is 3, already covered by the existing symmetric/unequal fixtures) -
  see the fork note in this cut's report.

- Added three corner-order pinning tests (one per axis), each asserting the
  exact welded vertex at each of Quads[0..3] for a chosen edge against a
  hand-computed position, including the axis-1 v1/v3 swap that F4's fixed
  OrientationSign now performs. These kill the axis-0-mirrored, c0/c2-swapped
  and equivalent per-axis corner-order mutants.

- Added four degenerate-quad orientation tests (sample exactly at iso,
  large-origin/tiny-cellSize sphere, octahedron at iso, tiny-tolerance
  sphere) asserting every shared undirected edge is traversed in opposite
  directions (net zero), via a new AssertEdgeDirectionsBalance helper that
  also covers the 4-use case. Restricted the existing
  Every_quad_winds_outward_from_inside_to_outside test's geometric check to
  non-degenerate (non-zero cross product) quads, per spec.

- Added Grid_edge_inequality_is_the_negation_of_equality (F6: != is compiler-
  mandated alongside ==, and needed its own direct test) and
  A_non_finite_sample_is_rejected (F7: NaN and +-Infinity samples).

All 38 SurfaceNetsTests pass; the 7 pre-existing GeometryDocumentTests
failures from CultLib commit e420410 are untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:30:14 +02:00
Meta Vens
967142e413 Fix surface-nets mutation findings F1/F3/F4/F7/F8 (production code)
F1: Extract's locals (position, v0..v3) were assigned through pattern-match
and short-circuited out-var chains whose definite-assignment analysis breaks
under a single mutated operator, so Stryker's Safe Mode dropped all 98
mutants in Extract as compile errors. Restructured to plain null checks and
a for-loop over a uint[4] so every local is unconditionally assigned; no
mutation of a boolean operator can make it fail to compile.

F3: deleted the `<= 1e-20f` fallback in Interpolate. A straddling edge always
has delta > 0 (.NET preserves subnormals), so the guard only ever replaced a
correct interpolated value with a wrong 0.5 midpoint.

F4: deleted the per-quad geometric cross-product winding test. Orientation
now comes only from the topological inside/outside flag plus a fixed
per-axis handedness sign (OrientationSign; axis 1's (X, Z) perpendicular
pairing is the one left-handed case, X x Z == -Y). This is now determined
purely by which endpoint of the crossing edge is inside and the axis, never
by quad geometry, so degenerate (zero-area) quads orient consistently with
their neighbours instead of falling back to an unstable dot-product sign.

F7: Extract now rejects any non-finite sample (NaN or +-Infinity) with
ArgumentException, in the same style as the existing shape/cellSize checks.

F8: collapsed three duplicated (edge, axis) mappings - TryGetOtherEndpoint's
per-axis switch, the three Axis*EdgeStarts tables plus the axis==0?1:0
ternaries in AccumulateAxisEdges, and TryGetQuadCells' three axis-specific
corner blocks - into one PerpAxes/PerimeterLoop table pair, used by all
three. Replaced the three duplicated `<= isoValue` inside tests with one
IsInside predicate. Deleted the per-axis trailing `InRange(x, cellsX)` /
`InRange(y, cellsY)` / `InRange(z, cellsZ)` checks (provably always true,
since TryGetOtherEndpoint already guarantees the edge's own-axis coordinate
indexes a valid cell) and the explicit TryGetValue+throw invariant guard
(the four cells around a crossing edge are proven to always have a vertex;
a direct dictionary index now fails loudly via KeyNotFoundException if that
invariant is ever actually violated, with no reachable branch to maintain).

F6: deleted CultGeometryGridEdge's <, >, <=, >= operators (CultGeometryQuadMesh.cs).
Nothing in production or tests used them; ordering goes through CompareTo.
!= is kept only because C# requires it whenever == is defined (CS0216).

Builds clean; does not touch CultGeometryIsoSurface.cs, CultGeometryDocuments.cs,
or ISOSURFACE-PROVENANCE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:29:45 +02:00
Meta Vens
08b0dced53 Fix flaky central-difference assertions: use absolute tolerance, not rounding
dotnet test found two failures where a fixed-decimal-place Assert.Equal
snapped two numbers that already agreed to within about 2e-5 to different
rounded digits, because their true difference straddled a rounding boundary
(0.824987829 vs 0.825006127 -> 0.82 vs 0.83). Both were central-difference
checks (smin_grad's own invariant-8 property test, and cellular's gradient-
matches-central-differences test), not a correctness bug in the primitives.

AssertWithinTolerance replaces the precision-based assertion for both central-
difference comparisons with an explicit absolute-difference check, which
states the actual claim (agreement within the finite-difference step's own
truncation error) without the rounding-boundary artifact. Also dropped an
unused local function (F2At) the build flagged.

Found by the third ygg-verify.sh test run on 77c3580.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:28:36 +02:00
Meta Vens
77c35808f5 Fix a build break: bare Random resolved to CultMath's own xorshift32
Inside namespace CultMath.Tests, unqualified `Random` resolves to the
enclosing CultMath namespace's own Random struct (design.md: "Random is
CultMath's own xorshift32") before `using System;` is even considered, so
`new Random(seed)` in the new test file was constructing CultMath.Random,
which has no NextSingle(). HlslSourceCompatibilityTests.cs already works
around this the same way (`new System.Random(...)`); CellularAndSminGradTests
now qualifies every use the same way.

Found by the second ygg-verify.sh build/test run on e613ed6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:23:54 +02:00
Meta Vens
e613ed6277 Fix a build break: float3.zero shadowed by math's own float3(...) method
Inside the static math class, the unqualified name `float3` resolves to
math's own `float3(float,float,float)` constructor-function first, not the
CultMath.float3 type, so `float3.zero` fails to compile there (CS0119). No
prior code in math.cs used the Type.zero static-field spelling from inside
math itself for this reason. Switched cellular's zero vectors to
`new float3(0.0f, 0.0f, 0.0f)`, which is unambiguous under `new`.

Found by the first ygg-verify.sh build/test run on 3eed434.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:21:37 +02:00
Meta Vens
3eed434b31 Test smin_grad and cellular's invariants behaviourally
CellularAndSminGradTests pins the rules the cut map calls out, each against a
mutant it must kill:

- smin's value never exceeds the ordinary minimum, and matches min with that
  input's own gradient once |a.w - b.w| exceeds k on either side.
- smin_grad's gradient matches central differences of its own .w at random
  seeded points (the formula is exact, not an approximation, so no exclusion
  band is needed away from the |a.w-b.w|=k seam itself).
- A direct case falsifies blending the gradient with a constant 0.5 or with
  (1 - h) instead of h.
- cellular's F1 never exceeds F2, matched against an independent brute-force
  Worley search that shares only the public pcg3d hash, not cellular's control
  flow.
- The true nearest feature wins even when an independent search finds it in
  the (1, 1, 1) neighbour offset, the last one cellular's own loop order
  visits, ruling out a "first found" or early-exit mutant.
- The gradient at a feature point is finite zero, not NaN.
- Both nearest and edge gradients match central differences away from a band
  around the F1 = F2 set, where the nearest and second-nearest identity swap
  discontinuously.
- id is constant across small moves that stay in the same feature's region and
  differs from a distant cell's id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:18:43 +02:00
Meta Vens
c13e7f7871 Extend the HLSL mirror comparison to one struct return shape
HlslSourceCompatibilityTests previously flattened only bare vector returns to
scalar leaves. Values() now recurses through struct fields of any type, so a
struct return (invariant 8's one shape, e.g. CultCellular) is compared field
by field down to bit-for-bit scalars, exactly like a vector return already
was. Components() stays scoped to scalar-typed fields, since it only ever
builds function arguments and must not recurse.

TransformHlslToCSharp gains transformation 5: struct fields gain `public`,
since HLSL has no field access-modifier concept and a C# struct's fields
default to private, which would hide them from Values()' reflection. The old
wrap-in-a-class transformation is renumbered 6. design.md's transformation
list documents both, matching design.md:92's numbered list.

StructReturnComparisonRejectsAMismatchedField is the extension's own test: two
hand-built structs with the same shape (mirroring a real mirror-vs-C#
comparison) prove the field-by-field, bit-for-bit walk actually rejects a
struct whose leaf field differs, or whose nested vector's own component
differs, not just a length check.

EveryMirrorFunctionMatchesCSharpMath and HlslMirrorTests' required-symbol list
now cover cultmath_smin_grad and cultmath_cellular; the mirrored-function
count moves from 28 to 30.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:18:21 +02:00
Meta Vens
d6bfc166da Mirror smin_grad and cellular in CultMath.hlsl
Adds cultmath_smin_grad, the CultCellular struct, and cultmath_cellular to
both tracked copies of CultMath.hlsl (packages/cultmath/shaders and the Unity
package), placed after cultmath_pcg4d so cultmath_cellular can call
cultmath_pcg3d and cultmath_distance, both of which HLSL requires to be
declared earlier in the file. Both copies stay byte-identical.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:18:01 +02:00
Meta Vens
1dca43596e Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.

cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.

Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.

design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
Meta Vens
659e3dd516 Record two Stryker findings from the surface nets mutation pass
Three TryGetOtherEndpoint boundary-guard mutants (per-axis "if (x + 1 >=
sizeX) return false;" forced to "if (true) return false;") were reported
Survived despite being exercised by the X/Y/Z open-boundary tests. Hand-
mutating all three and running dotnet test directly failed 11 of 24
SurfaceNetsTests, confirming a vstest-runner coverage-attribution false
survivor (same class of caveat already recorded for CultMath's mtp runner,
here under the default runner). Recorded under Known tool caveat.

Separately, adding stryker-config.json in the same cut that first uses it
defeats --since line scoping: Stryker fell back to mutating the whole
GameCult.Geometry.csproj glob ("Non-CSharp files in test project were
changed"), not just the new files, which is why the run's survivor list
includes CultGeometryIsoSurface.cs, CultGeometryDocuments.cs, and
CultGeometryPrimitives.cs. Recorded under Follow-ups so a later diff-only
cut isn't surprised by the difference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:44:36 +02:00
Meta Vens
af39e05c21 Kill mutation survivors: degenerate fixtures, missing axis and edge tests
A --since:29e50ad Stryker pass found real gaps, all inside this cut's own
files:

- vertex-placement and normals fixtures anchored a vertex at the world
  origin, so a mutated (+ instead of -) or dropped cellSize multiply was
  invisible against a zero operand; moved them off the origin;
- the cube normals test only checked length, and three orthogonal
  +-2-length face contributions summed to the same length no matter which
  one a mutant negated; now checks the exact expected direction per vertex;
- the open-boundary rule only had an X-axis test, leaving TryGetQuadCells's
  Y- and Z-axis guards (and TryGetOtherEndpoint's Y/Z cases) uncovered;
  added the Y- and Z-axis twins of the existing plane test;
- the 1e-20 near-zero-delta fallback in Interpolate was never exercised;
  added a fixture with an edge delta of ~3e-21;
- CultGeometryGridEdge's Equals and ToString had no direct test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:38:15 +02:00
Meta Vens
dcfb267ac7 Fix the checkerboard fixture's open-boundary contamination
The 4x4x4 pure-checkerboard field had no fully-interior cell: every active
cell bordered the array edge, so some crossing edges lost a quad to the open-
boundary rule and edge usage came out odd (observed: all-3) for reasons
unrelated to the ambiguity the test means to exercise. Wrapped the
checkerboard in a one-sample uniform-outside margin (5x5x5 grid, checkerboard
only on samples 1..3) so every crossing edge under test keeps all four
surrounding cells in range, isolating the even-but-not-2 claim from
open-boundary effects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:15:28 +02:00
Meta Vens
7a76b89a94 Fix expression-tree-incompatible test lambdas for FluentAssertions OnlyContain
Two spots failed to build: a method-group reference to float.IsFinite and an
`is 1 or 2` pattern inside a lambda that FluentAssertions compiles as an
Expression<Func<T, bool>>; expression trees can't contain pattern-matching
`is`, and a method group isn't implicitly convertible to an expression tree
either. Rewritten as explicit lambdas with `==`/`||`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:10:19 +02:00
Meta Vens
09a85edc47 Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
Meta Vens
7a656a7ec2 Add surface nets extraction and face-weighted normals to GameCult.Geometry
Implements CultGeometrySurfaceNets.Extract (welded CultGeometryQuadMesh, one
quad per interior crossing grid edge, outward-oriented winding) and
CultGeometryQuadNormals.FaceWeighted (unnormalized diagonal-cross-product
area weighting, matching CultGeometryIsoSurface's sign convention and
degenerate-normal rule). Fresh implementation per SURFACE-NETS-PROVENANCE.md;
no code or tables copied from the cited literature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:29 +02:00
Meta Vens
29e50ad315 Merge Stryker adoption and the CultMath tests it pointed at
Stryker.NET becomes CultLib's mutation tool (CultMath config, baseline 2243
mutants, 350 survived). Behavioural tests close the three areas Aetheria
leans on: the BatchMath SIMD body, first_order_intercept's branches, and
equality and comparison across the vector types. 350 -> 264 survivors;
Soul passed the cut after one accumulation test was added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:13:30 +02:00
Meta Vens
fd75c2acf9 Test that the float2 radial falloff overload accumulates, not assigns
Soul's pass on 41440cb found BatchMath.cs:115
(AddRadialFalloffAcceleration2D(ReadOnlySpan<float2>, ...)) accumulates
onto the caller's acceleration buffer, but no test ever seeded that
buffer with nonzero values: mutating += to = passed all 133 tests. The
float-array overload already had this guard
(RadialFalloffAccumulatesOntoExistingAcceleration); add the float2
equivalent, with a nonzero, per-element-distinct starting buffer, mixing
in-range and out-of-range particles so both the accumulated and
unchanged-seed cases are checked.

Checked every other += site in BatchMath.cs for the same gap: the
array-overload's scalar tail and vector body, and both
IntegrateSemiImplicitEuler2D overloads, all already seed their
accumulator buffers (vx/vy/px/py, velocity/position) with nonzero,
per-element values in their existing tests. Only this one site was bare.

Verified by hand-mutating BatchMath.cs:115 (+= to =): the new test fails
(expected 6.78, actual 0.48); reverted the source byte-exactly after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:13:05 +02:00
Meta Vens
41440cb538 Tie the scalar comparison overloads too, not just vector-vector
The vector-vector self-tie added in the previous commit does not cover
float2/float3/float4's vector-scalar and scalar-vector overloads: their
existing no-tie fixture (a distinct vector against a single scalar) never
had a component equal to that scalar, so their boundary-flip mutants
(< to <=, > to >=) kept surviving. Add a uniform vector (every component
the same value) against a scalar equal to that value, tying every
component of both scalar overloads at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:49:23 +02:00
Meta Vens
eeb67e2a22 Add tied-value comparisons to kill boundary-flip mutants for real
The no-tie fixture in each *ComparisonOperatorsDifferPerComponent test
correctly kills direction mutations (< swapped for >) but leaves every
strict/non-strict boundary swap (< to <=, > to >=) surviving, since with
no equal component the two behave identically. That default is
appropriate for a float-threshold epsilon check, but these are
general-purpose vector comparison operators (CultMath's own
RectIntersectionTreatsTouchingEdgesAsContact test compares literal
endpoints), and for the int types an exact tie is the routine case, not
the rare one. Add a complementary "compared against itself" call per type
(vector-vector for double2/double3/int2/int3/int4, plus vector-scalar and
scalar-vector for float2/float3/float4) so every component ties and the
boundary swaps are actually exercised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:44:58 +02:00
Meta Vens
6d8195811d Fix a tolerance-overload bug that made intercept precision asserts inert
`Precision` was declared `const float`, so `Assert.Equal(a, b, Precision)`
bound to xunit's absolute-tolerance overload (allowing any two values
within 4.0 of each other) instead of the intended 4-decimal-place
overload. Hand-mutating the `t2 > 0 ? min(t1, t2) : t1` ternary to always
return `t1` (math.cs:287) exposed it: the assertion silently accepted a
result almost 0.27 away from the expected value. Declaring it `const int`
selects the correct overload; confirmed the same hand mutation now fails.

Also add the float2-overload euler-integration length-validation test the
mutation report showed missing (BatchMath.cs:208's ValidateEqualLengths
call had no test at all).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:38:30 +02:00
Meta Vens
446859927a Fix degenerate fixtures that let real mutants survive their own tests
Soul-style self-check against the actual mutation report (not just green
tests) found several fixtures whose values coincidentally produced the
same output on the buggy path as on the correct one:

- BatchMath's scalar-tail fixtures put the one remainder particle/step
  either outside the falloff radius or mask-frozen, so the y-axis
  arithmetic a mutation would touch was never reached with an observable
  effect. Pin the remainder particle in range (radial falloff) and dynamic
  (Euler integration) so the scalar tail's own math is exercised, not just
  the vector body's.
- The float2-overload of AddRadialFalloffAcceleration2D had no no-op
  (radius <= 0 / strength == 0) test at all.
- first_order_intercept_time's negative-determinant test used a
  perpendicular relative velocity, making b = 2*dot(vel, pos) exactly
  zero; a mutant that drops the "no solution" branch entirely and falls
  through to the linear fallback coincidentally computes 0 too. Use a
  non-perpendicular fixture so the fallback's nonzero value is
  distinguishable from the correct zero.
- No fixture ever reached the `t1 > 0` branch at all (a > 0, both
  quadratic roots real and positive), so the boundary/negate checks
  guarding it and the `t2 > 0 ? min(t1, t2) : t1` line were never
  covered. Add that case.
- bool2.@false/@true, bool3.@true and bool4.@false have no consumer
  anywhere in the suite (bool3.@false and bool4.@true are read by
  HlslSemanticsTests); pin all six vector constants directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:26:48 +02:00
Meta Vens
6d5e209656 Merge CultMath 0.2.4: erf and erfinv reach the Unity package
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:10:30 +02:00
Meta Vens
8c86bc90e3 Give the CultMath package changelog its Unity meta
Soul: a git-URL package lands read-only in Unity's cache, where a file with
no meta is warned about and ignored on every import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:10:25 +02:00
Meta Vens
ab98462c06 Test vector Equals/GetHashCode/comparison operators per component
Hand-verified that weakening float3.Equals from && to || passes the whole
suite. The same shape recurs with no shared base type across float2/3/4,
double2/3, int2/3/4, bool2/3/4, quaternion, float2x2 and float3x3. Add
data-driven tests, per component position, that a value differing at
exactly one component is unequal under Equals/==/!=, that GetHashCode
stays consistent with Equals, and that </>/<=/>= (including the
vector-scalar and scalar-vector overloads on the float types) each report
a different result per component. float2x2/float3x3 are covered at row
granularity since they have no == operator; quaternion and bool2/3/4 have
no ordering operators and are covered for equality only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:06:41 +02:00
Meta Vens
71c42b125f Test first_order_intercept's branches beyond the one happy path
math.first_order_intercept_time had a single closing-target test. Add
coverage for zero relative velocity, the a-approx-zero linear fallback, a
target that outruns the projectile (negative determinant), a receding
target with both quadratic roots non-positive, and a closing case with
off-origin, off-axis shooter and target state. The closing cases assert
that the projectile (inheriting the shooter's velocity) and the target
actually coincide at the returned time; the no-solution branches pin the
value the source's own contract specifies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:06:41 +02:00
Meta Vens
e3d52406db Test BatchMath's vector body, remainder, and guard branches
Both existing BatchMath tests used 2-3 particles while LaneCount >= 4, so
only the scalar tail ever ran, and both held y at 0 for every particle, so
every y-axis mutation was invisible. Add fixtures sized LaneCount * 3 + 1
with distinct nonzero x/y per particle, pinned against a scalar reference
computed independently in the test, plus explicit coverage of the early
return, negative deltaTime, length-mismatch, and empty-span guards on all
four BatchMath overloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:06:40 +02:00
Meta Vens
20e6da5942 Release CultMath 0.2.4: erf and erfinv reach the Unity package
math.erf/math.erfinv (source, tests, README) already landed on main via
87b95ae, b688cb2, fc9ce97, 4cdc02b, 6e8362b, cc71f17. This is the release
step 0.2.3 skipped needing: bump the package version, rebuild the tracked
Unity DLL/pdb deterministically, and add the CHANGELOG.md this package
never had (docs/semver-policy.md names this exact gap).

- packages/cultmath/src/CultMath/CultMath.csproj: Version 0.2.3 -> 0.2.4.
- packages/cultmath/unity/org.gamecult.cultmath/package.json: 0.2.3 -> 0.2.4.
- packages/cultmath/unity/org.gamecult.cultmath/CHANGELOG.md: new, [0.2.4]
  entry only (Added: erf, erfinv). Earlier versions are not backfilled, per
  the semver policy's own ruling on this package's pre-existing gap.
- packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll
  and .pdb: rebuilt via packages/cultmath/scripts/build-unity-package.ps1
  (dotnet build, netstandard2.1, Release, --no-incremental, no Source Link).
  Built on Windows; the assembly is platform-neutral IL. The script's own
  determinism check (fresh build hash == tracked hash) and its version-parity
  check (package.json version == assembly FileVersion) both pass.
- No HLSL mirror change: erf/erfinv are C#-only, and
  packages/cultmath/shaders/CultMath.hlsl already matches the copy under
  unity/org.gamecult.cultmath/Shaders/CultMath.hlsl byte for byte.

Verified independent of build-unity-package.ps1's own checks: a scratch
console app loaded the tagged cultmath-unity-v0.2.3 DLL and this build's
fresh DLL into separate AssemblyLoadContexts and diffed their public
surface. Only two members were added (math.erf(float), math.erfinv(float)),
none removed; AssemblyVersion/FileVersion/InformationalVersion match apart
from the 0.2.3 -> 0.2.4 bump; TargetFrameworkAttribute is unchanged
(.NETStandard,Version=v2.1). erf/erfinv round-trip and domain-edge behavior
(erfinv(+-1) = +-Infinity, erfinv(1.5) = NaN) verified by invoking the
loaded methods directly.

dotnet test packages/cultmath/tests/CultMath.Tests -c Release: 102 passed,
0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:02:40 +02:00
Meta Vens
d110d9d6ff Merge the TypeScript QUIC realtime line (Cuts 3-6 and retention)
What lands:
- Cut 3: the native MsQuic bridge, with the Windows release leg and F5
  closed on both platforms, and the closerace fixture fix.
- Cut 4: the koffi Node binding, consumer, session manager and trust.
- Cut 5: the TS provider. It reaches the C# native connector, which is
  the Unity overlay's path, on Windows.
- Fix batches 1-4: use-after-free, handler isolation, the
  handshake-timer eviction, the koffi allocation leak, one C#-shaped
  inbox, encode-once, advertisedHost, publish-only
  (acceptClientFrames: false), and maxConnections.
- Retention: the provider seeds late joiners with the newest
  latest-only frame per (channel, body), in TS and C#.
- Cut 6, narrowed: native binaries committed under
  packages/cultmesh-ts/native, built by Actions. A second from-scratch
  build reproduced them byte for byte.

Gates: four Opus Soul passes on the runtime and one Sonnet pass on the
packaging. The last runtime pass found that the connection death under
load was the old handshake eviction, which fix 3a had removed, and it
confirmed StreamPixels' load (60 Hz latest-only for 10 minutes) with
flat memory.

Also carried, per the operator's ruling: CultMath erf/erfinv (Cut 12.0
and its fix batch) from the branch base.

Map conflicts were resolved in favour of main, which owns the maps. The
only branch-side lines main lacked were a Cut 3 "still outstanding"
block that main's closure supersedes.

Open follow-ups, recorded on the map: IPv6-literal endpoints cannot be
dialled; consumer-side native queues are unbounded while Unity is not
polling; a client can see connect() resolve past maxConnections before
the refusal arrives; subtraction (about -130, mostly comments) is
deferred.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:51:19 +02:00
Meta Vens
cdb8f3ca6d Keep the native digest list and manifest LF on every checkout
Soul found that on a Windows clone (core.autocrlf=true) SHA256SUMS and
MANIFEST.txt were written with CRLF, so the documented
`sha256sum -c SHA256SUMS` failed on every line: each filename ended in
\r. CI stayed green only because it checks out on Linux. After this
rule and a fresh checkout, the check passes on Starfire for all four
binaries.

Provenance, recorded here because verify-committed only checks the
committed files against the committed sums: run 35912901846 rebuilt
all four binaries from scratch after they were committed, and every
artifact is byte-identical to the committed file (libmsquic.so.2
c09a72f9..., libgamecult_mesh_quic_native.so 0ee3d283..., msquic.dll
c17c6581..., gamecult_mesh_quic_native.dll 9dae391a...). Two
independent CI builds produce the same bytes, which shows both where
they came from and that the build is reproducible.

Self edited this one-line config directly instead of dispatching Hands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:50:32 +02:00
Meta Vens
2785f28f5b Rename the release tarballs to stop tripping 11.7's parked-package grep
`cultmesh-quic-native-{win32-x64,linux-x64}.tar.gz` collided by substring
with the negative grep for the parked `@gamecult/cultmesh-quic-native-*`
platform package names (11.9, not built in this campaign). Rename to
`quic-native-{win32-x64,linux-x64}.tar.gz`, matching the upload-artifact
names already used. The release job has not run (no tag pushed), so this
has no runtime effect yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 22:03:38 +02:00
Meta Vens
4fccdf17ff Remove the TEMPORARY push-to-quic-cut6 trigger
The binaries are committed and verify-committed went green on the trigger's
own run (https://github.com/GameCult/CultLib/actions/runs/35912901846).
Reverts quic-native.yml to the triggers named in cut 6 (docs/typescript-
quic-realtime-cut.md:2153-2156): push to main and pull_request run
verify-committed only, workflow_dispatch runs the two builds, a
cultmesh-quic-native-v* tag runs all four. Diff against 4d75d90 (the file
before the temporary trigger was added) is empty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 22:00:25 +02:00
Meta Vens
fcc7b14555 Commit the QUIC native binaries, SHA256SUMS and MANIFEST.txt
Downloaded from the TEMPORARY push-triggered run
https://github.com/GameCult/CultLib/actions/runs/35912386052 (commit
e5bf1c9), built by build-win32-x64 and build-linux-x64 in quic-native.yml.
Every file's digest was verified against the job logs before this commit
(win32-x64/msquic.dll's also matches build-quic-native.ps1's own pinned
expectedRuntimeSha256, confirming it is the pinned MsQuic 2.5.9 runtime).

  win32-x64/gamecult_mesh_quic_native.dll  9dae391a44d59f12e19baa08a44d86c7e8b4916ca5d40007a42d65913ce0896f
  win32-x64/msquic.dll                     c17c65813fe007eb486a5270d244f3659261355795b27abb99bd19ff77539263
  linux-x64/libgamecult_mesh_quic_native.so 0ee3d28324c79a5b0135368187e7bd75c1969ad94fc6985755e81de4ec96e430
  linux-x64/libmsquic.so.2                 c09a72f9b3bfd9119044b305c6064731c1e1879411e6f54276474fb58b524b68

SHA256SUMS is generated sha256sum output over exactly these four relative
paths, committed verbatim. MANIFEST.txt records the pinned MsQuic sources,
the bridge commit, the run URL, and each build job's printed host/compiler
line; build.release notes this run used the TEMPORARY push trigger rather
than a tag, since no release exists yet.

Cut 6 (packaging), 11.5/11.7, Q8 Option 1: the operator (via this session)
downloads, verifies and commits; no workflow writes to the tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:58:54 +02:00
Meta Vens
e5bf1c9243 Add a TEMPORARY push trigger to exercise quic-native.yml on this branch
workflow_dispatch cannot be dispatched against quic-cut6 until this workflow
file is registered from the default branch (a GitHub constraint, not a
permissions gap: `gh workflow run` and a raw dispatch API call both 404).
Rather than touch main or push a tag, add push: branches: [quic-cut6] and
extend verify-committed's and both build jobs' `if:` guards with a
`refs/heads/quic-cut6` arm, so this push itself runs the three jobs. The
release job's guard is untouched: it still matches only a
cultmesh-quic-native-v* tag, so this push does not create a release.

Every added line is marked TEMPORARY and paired with its match; both are
removed in one commit once the binaries are committed and verify-committed
goes green on that push.

Cut 6 (packaging).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:54:02 +02:00
Meta Vens
4d75d90da8 Add the QUIC native release workflow
Four jobs: verify-committed checks packages/cultmesh-ts/native/SHA256SUMS
against the tree on every push to main and every pull request; build-win32-x64
and build-linux-x64 run scripts/build-quic-native.{ps1,sh} on
workflow_dispatch or a cultmesh-quic-native-v* tag and upload their output as
artifacts; release, needing both builds and gated to the tag push, is the
only job with write permission and attaches the two platform archives plus a
committed-format SHA256SUMS and a draft MANIFEST.txt to a GitHub Release.

build-linux-x64 runs inside the pinned debian:13@sha256:f324c7ff… container
per Q6 A: the Actions container is the only permitted Linux release builder,
because the artifact links against that image's glibc and the deploy host
(Yggdrasil) runs the same OS. No release or tag has been pushed by this
commit; the release job exists but has not run.

Cut 6 (packaging), docs/typescript-quic-realtime-cut.md:2131-2156, narrowed
per StreamPixels cut map section 4.3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:51:28 +02:00
Meta Vens
f228ccd1e5 Copy the MsQuic MIT licence into cultmesh-ts's native/ payload
The package directory must be self-contained once copied out of the repo
(pnpm materialises only `files`), so the licence text already committed
beside the Unity plugin binaries is copied here rather than referenced.
Same MIT text for both platforms. Cut 6 (packaging), 11.4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:51:18 +02:00
Meta Vens
99eeae2047 Assert the cultmesh-ts tarball carries its native/ payload
The closure smoke already asserted dist/index.js and dist/index.d.ts for
every workspace package; extend it for cultmesh-ts to assert the four
committed native binaries plus SHA256SUMS, MANIFEST.txt and
MSQUIC-LICENSE.txt are in the packed file list, on every host. The tarball
carries both platforms; the host only decides which one loads. Cut 6
(packaging), 11.5/11.6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:51:11 +02:00
Meta Vens
5650304169 Mark the committed QUIC native binaries binary in .gitattributes
`* text=auto` already autodetects them by content, but that is a heuristic.
Say explicitly that packages/cultmesh-ts/native/**/*.dll and *.so(.N) are
binary so no future text-processing rule can apply EOL conversion and poison
the digests SHA256SUMS pins. Cut 6 (packaging).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:51:03 +02:00
Meta Vens
bbb072551c List native/ in cultmesh-ts's packed files
The committed QUIC native binaries only reach a consumer if `files` carries
them: pnpm materialises a file: dependency from `files` alone, and the same
allowlist gates `npm pack` and the deploy `git archive`. Cut 6 (packaging).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:50:55 +02:00
Meta Vens
057cf19fd9 Bring Cut 3's closerace fixture fix onto the packaging line
The fixture fix (44ab376) lived only on quic-cut3-winrel. Cut 4 branched
from cc71f17 before it, so the QUIC line never carried it. This merges it
into quic-cut6 before packaging, so the release line runs the closerace
fixture whose poll timeout outlasts its own settle budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:47:19 +02:00
Meta Vens
6f7b052a47 Fix P17 to actually kill its mutant: check refCount, not connectionCount
The synthetic connection P17 drives directly at acceptConnection() never
reaches CONNECTED (it was never dialed anywhere), so it never joins `peers`
either way — connectionCount alone couldn't tell "refused" apart from
"attached but still handshaking". Manually restoring the disposed-check's
removal and rerunning showed the original assertion passing regardless.
retain() is synchronous and only the attach path calls it, so comparing
CultMeshQuicNativeRuntime.refCount immediately around the acceptConnection()
call is what actually distinguishes the two: confirmed by hand that the
mutation now fails this test by assertion (2 !== 1) before restoring it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:43:22 +02:00
Meta Vens
94af7fd07e Make P3 deterministic and add tests for Soul's surviving mutants and the new caps
P3 ("a latest-only send failure evicts the peer...") raced wall-clock
against a tight while-loop that does no real I/O per iteration (broadcast()
never awaits a latest-only peer's send), so it spun encoding and enqueueing
frames fast enough to starve the event-loop turns the native pump needs to
notice the dead connection — a self-inflicted race that any extra load in
the test process (mutation testing included, regardless of which line was
mutated) could push past the 2s bound with nothing to do with the mutant
under test. Fixed by decoupling how many attempts to seed (a small fixed
count, each with a real yield) from how long to wait for the result (one
waitUntil afterward).

New tests for mutants Soul found surviving by assertion:
- I1: an inbox key keeps its first-arrival queue position when a later
  publish overwrites its value.
- L1: the outbox keeps one send in flight per peer, proven by
  prototype-patching sendEncodedFrame to track concurrency (the provider
  builds its peer transports internally, so this is the only vantage point
  from outside the module).
- P15: broadcast() after dispose rejects.
- P17: an accept arriving after dispose is refused, not attached, driven
  directly at acceptConnection() with a real (but otherwise unused) native
  connection handle rather than a fabricated pointer.

Plus coverage for the new options: a publish-only provider faults a client
that sends to it and never queues anything, and a connection beyond
maxConnections is refused without disturbing existing peers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:38:25 +02:00
Meta Vens
40944dee17 Cap the QUIC provider's inbound memory: publish-only mode and a connection cap
Soul's probe proved a client can grow the provider's memory without limit
(20k reliable frames plus 20k distinct latest-only keys took
receiveQueueSize to 40000 and RSS from 97 to 318 MB) because StreamPixels
never calls receive() to drain it, and there was no cap on connections
either. The pinned certificate only authenticates the server, so any
internet client can dial in.

Add acceptClientFrames (default true, unchanged for existing callers and
interop peers, which all use receive()): when false, any inbound stream a
peer opens faults that connection at STREAM_STARTED, before a frame can be
decoded or queued, so no inbox or generation-map entry is ever created for
client data, and receive() itself rejects clearly.

Add maxConnections (default 1024): a connection beyond the cap is refused
at accept, shut down before any transport, outbox, or retained-frame
seeding is allocated for it. Existing peers are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 21:38:03 +02:00
Meta Vens
58a6fc5c06 Merge the koffi allocation-leak fix into the retention line
This brings fix batch 3c (per-runtime out-buffers, freed on the last
release, plus a steady-state RSS test in its own child process) onto
the branch carrying fix batches 3a and 3b and retention.

The merge was automatic. Checked by hand: release() frees the reused
buffers only after the reference count reaches zero and the pump has
exited. retain() (from 3a) is called only while the provider already
holds a reference, so it cannot resurrect a runtime that is closing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:06:29 +02:00
Meta Vens
f0f0694a81 Add a test for the inbox's drain-before-error rule
Hand-mutating CultMeshRealtimeInbox.receive() to check `completed`
before the ready queue (so dispose() rejects an already-queued frame
instead of draining it first) found no existing test that dies for it.
Add one: two reliable-ordered frames sit in the provider's receive
queue, the provider is disposed, and both frames must still come back
from receive() before the next call rejects.

Verified by hand against the final code and restored, per the fix
batch's mutation list; not committed as a harness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 20:02:38 +02:00
Meta Vens
2e7f10993f Add tests for the provider's bounded client fan-in
Reproduces Soul's probe directly: 20,000 client-sent latest-only frames
on one key, none ever drained by receive(), leave exactly 1 pending
frame in the provider's receive queue, and that frame carries the
newest sequence sent. A second test sends 20 frames across 50 distinct
keys and checks the queue holds exactly 50 (bounded by key count, not
frame count). A third proves reliable-ordered client frames are never
coalesced and stay in order at the provider.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:51:51 +02:00
Meta Vens
f0f114701a Bound the QUIC provider's client fan-in with one inbox class shared everywhere
Soul's probe found readyFrames growing without limit: a publish-only
consumer of CultMeshQuicRealtimeProvider.receive() (StreamPixels' Node
service) let a single unauthenticated client key grow the provider's
receive queue by one entry per frame sent, forever.

Replace CultMeshRealtimeLatestOnlyCoalescer plus the three separate
ready-queue/waiter implementations built around it (the transport's
inbound path, the provider's outbox pump, and the provider's own
readyFrames array) with one CultMeshRealtimeInbox<T>, shaped like the
C# reference's CultMeshRealtimeInbox: latest-only entries coalesce to
at most one pending value per (channel, body) key, keeping that key's
first-arrival queue position; reliable-ordered entries always queue,
unbounded; receive() drains whatever is queued before a complete(error)
rejects further calls.

The provider's pumpReceivedFrom now publishes into this inbox under the
frame's (channel, body) key instead of pushing onto an unbounded array,
so a slow or absent receive() caller backs up to at most one pending
frame per distinct key. Added provider.receiveQueueSize (test-visible,
mirrors the existing trackedStreamCount precedent) to make that bound
observable.

realtime-quic.ts: 1298 -> 1273 lines (HEAD 1b0c71d), -16 against the
275010a anchor. Soul estimated -90 to -120 from unifying four ~209-line
sites into one ~80-line class; the actual saving is smaller because two
of those sites already need their own promise/waiter/AbortSignal
plumbing in TypeScript (no Channel with native CancellationToken
support), so consolidating them removes duplication but the surviving
single copy of that plumbing is not itself small. No further
correctness-neutral cut was found without code-golfing working logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:51:44 +02:00
Meta Vens
1b0c71d963 Bound P8's race so a regression fails by assertion, not a file-wide hang
Without sendOnStream's disposed guard, the queued send never settles (no
event or sweep is left to reject it), so a bare assert.rejects(send, ...)
would hang the whole file instead of failing this one test. Race the send
against a 3s timeout that rejects with a different message, so the mutant
is a loud assertion mismatch either way the native layer happens to fail
first.

Verified by hand: reverted sendOnStream's `if (this.disposed)` guard to
`if (false)` locally, rebuilt, and ran P8 alone — it died by assertion in
81ms (a native stream-send error on the already-shut-down connection,
still not matching /disposed/i), not a timeout. Restored the guard before
committing. Also hand-verified fix 1 the same way: commenting out the
`clearTimeout(handshakeTimer)` on CONNECTED reproduces the original
handshake-timer eviction, and the new fix-1 test dies by assertion
(0 !== 1) rather than a hang.

Full Windows pass on Starfire after restoring: 158/158 cultmesh-ts tests
and all 5 QUIC interop tests (including the native-connector and
late-join lanes), msquic.dll confirmed as the pinned build (4,181,856
bytes, SHA-256 C17C6581...).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:32:43 +02:00
Meta Vens
fedba286c5 cultmesh-ts: isolate the memory probe in its own child process
By the time the memory test ran in-process, ~150 earlier tests in the
suite had already grown and partly freed the glibc heap, so a real
per-call native leak could land in already-mapped free space and never
show up as RSS growth in that process: confirmed by hand, the
in-process version of this test kept passing even with `nextEvent`
mutated back to allocating fresh koffi buffers every call, once it ran
inside the full suite rather than alone.

Spawn a dedicated `node --expose-gc` child process for the measurement
instead. A fresh process has no such history, matching the ~8 MiB
(fixed) vs ~30 MiB (pre-fix) split measured in isolation. Confirmed by
hand again with the same mutation: the child-process version now fails
reliably (~28.7 MiB growth) inside the full 150-test suite, not just
standalone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:24:54 +02:00
Meta Vens
058fd6cee7 Fix P10b's flaky post-fault accept cycle by proving B works via a real frame
The prior version opened connection B only after A had already faulted,
then asserted a boolean flag set by B's own CONNECTED event. That flag
never flipped on Linux: something about accepting a second connection
right after the first one's forced shutdown made the assertion
unreliable, even though A's fault itself isolated correctly (visible in
the log: the onFault throw fired once, promptly).

Rewritten to establish B as a normal, fully connected loopback pair
first, fault A only afterward, then prove B's handler is still live by
sending an actual frame on B and confirming the server side still
receives it — a stronger assertion than a connected flag, and one that
does not depend on a second connection's accept timing relative to a
fault that just tore another one down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:22:16 +02:00
Meta Vens
329a78c255 Fix three test bugs found by the first Linux verify run
P8's test opened a fresh reliable stream on the first send, so disposing
before its microtask ran hit streamOpen() against an already-shut-down
connection (its own native error) instead of ever reaching sendOnStream's
disposed guard. Prime the stream with an awaited first send so the second,
raced send chains straight onto sendOnStream with no streamOpen call in
between.

P10b's test registered connection A's and B's server-side listeners after
an `await`, the exact async-registration gap fix 1 exists to close: a
connection's next native event can be dispatched, and silently dropped
(no listener registered yet), before that await's continuation runs.
Register both listeners synchronously inside their LISTENER_NEW_CONNECTION
callback instead.

The IPv6 fix 3 test asserted the wrong expectation: WHATWG URL's own
`hostname` keeps the brackets for an IPv6 literal, so the round trip
through parseQuicRealtimeEndpoint correctly returns "[::1]", not "::1".

Confirmed via a Linux verify run (cultlib-quic-native-dev image): before
this commit, exactly these three new tests failed and every other test
(155) passed, including every other fix/P3 test added alongside them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:19:38 +02:00
Meta Vens
a534af791f cultmesh-ts: calibrate the memory test's RSS bound against measured numbers
The 5 MiB bound was a guess and false-failed on the fixed code: 200k
direct `nextEvent` calls carry their own ~8 MiB RSS noise floor from
thread-pool marshaling and promise bookkeeping, even with zero
`koffi.alloc` calls in the loop. Measured on Yggdrasil, same build and
host, same 200k-call loop:

  fixed code (0 koffi.alloc calls in the loop):  ~8.0 MiB RSS growth
  pre-fix code (400k koffi.alloc calls, unfreed): ~29.9 MiB RSS growth

18 MiB sits well above the fixed baseline and well below the leaking
one, so the test stays robust to ordinary run-to-run noise while still
catching the regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:18:03 +02:00
Meta Vens
28b6d3a67b cultmesh-ts: stop the NextEventAsyncFn doc comment from tripping the async-callback grep
The `.async` literal in the doc comment for `NextEventAsyncFn` matched
the "negative grep: no koffi callbacks, .async only on nextEvent" test's
`/\.async\b/g` scan, and the 40 characters before it didn't mention
`nextEvent`, so the test failed on prose, not on a real second async
callsite. Reworded to describe koffi's async export without the
literal `.async` token.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:17:55 +02:00
Meta Vens
0bb0fcba82 Add tests for the handshake-timer fix, encode-once broadcast, and P3/P8/P10b
realtime-quic-provider.test.ts: a peer stays connected and keeps
receiving well past handshakeTimeoutMs (must fail on the pre-fix code);
an invalid reliable-ordered or latest-only frame throws from broadcast()
without evicting any peer; advertisedHost is used in place of the bind
address and round-trips through the module's own parser; an IPv6 bind
advertises a bracketed literal; receive() with an already-aborted signal
rejects immediately; and a latest-only send failure evicts the peer
(P3 — the outbox's own send-failure path, not covered by the existing
reliable-ordered eviction test).

realtime-quic-consumer.test.ts: a reliable send still queued behind
reliableSendTail rejects instead of hanging when disposal wins the race
(P8 — sendOnStream's this.disposed guard).

realtime-quic-native.test.ts: a throwing onFault handler is isolated to
its own connection; the pump survives and a second connection's handler
still runs (P10b).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:16:06 +02:00
Meta Vens
e3937bc277 Fix the QUIC provider's handshake-timer eviction structurally (batch 3a)
Soul's probes found every provider peer evicted at handshakeTimeoutMs
(10s by default): StreamPixels' Node service would drop every Unity
overlay 10s after it connected. Root cause: acceptConnection registered
a temporary buffering handler that was the only thing setting
state.connected, then attachPeer awaited CultMeshQuicNativeRuntime.open()
before swapping in the real handler and replaying buffered events. The
native bridge always emits NEW_CONNECTION before CONNECTED as separate
events; when CONNECTED landed after the swap, it went straight to
transport.handleConnectionEvent (which has no case for CONNECTED) with
nothing left to clear the handshake timer, so a fully healthy peer was
evicted on schedule regardless.

Fix: acceptConnection takes a synchronous retain() on the provider's own
already-open runtime instead of awaiting a fresh open(), so one handler
owns the connection from NEW_CONNECTION onward with no async window, no
handler swap, and no buffered-event replay. PendingAccept, its buffering
handler, and attachPeer are gone; dispose() now sweeps `accepted` (every
transport a connection ever got, whether or not its handshake reached
CONNECTED) instead of the deleted pendingAccepts set. Cut C's
retained-frame seeding still happens atomically before the peer joins
`peers`.

Also, while touching broadcast() and sendFrame():

- Encode and validate each frame exactly once in broadcast(), fanning the
  encoded bytes out to every peer and outbox, instead of re-encoding once
  per peer inside sendFrame(). An invalid frame (e.g. a 70,000-byte
  channelId) now throws to the caller before touching any peer, rather
  than failing the same way for every peer in turn and evicting all of
  them while broadcast() itself reported success. The provider's retained
  latest-only frames now cache their encoded bytes too, so seeding a newly
  attached peer never re-encodes them either.
- Add an `advertisedHost` provider option, independent of the bind
  address: `host` is typically 0.0.0.0, an empty string, or an IPv6
  wildcard for binding, none of which describe a route a client can dial,
  and the native listener only accepts IP addresses in the first place,
  not a DNS name. IPv6 literals are bracketed correctly in the advertised
  endpoint. The uppercase cert-sha256 pin is unchanged (the operator ruled
  StreamPixels pins a long-lived self-signed certificate).
- provider.receive() with an already-aborted signal now rejects
  immediately instead of registering a waiter an already-fired abort event
  can never reach.
- The duplicated "datagrams not supported" message (sendFrame and
  broadcast each had their own copy) is now one shared constant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:15:57 +02:00
Meta Vens
236583c8b0 Add CultMeshQuicNativeRuntime.retain(): a synchronous extra reference
A caller that already holds a live runtime instance (the QUIC realtime
provider, handing a reference to each newly accepted peer) needs one more
reference without crossing an async boundary. `open()` itself has no I/O
to await, but being `async` still forces every caller through a microtask
hop; `retain()` just increments the shared refcount and returns `this`,
synchronously, so the caller stays inside the same turn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:15:42 +02:00
Meta Vens
9a7e20a8d2 cultmesh-ts: add a steady-state memory test for the nextEvent leak
Calls the runtime's private `nextEvent` directly (0 ms timeout, no
listener/connection open, so each round trip returns immediately with
status 0) 200,000 times after a warm-up, then asserts RSS growth
across the run stays under 5 MiB with `global.gc()` forced before and
after. The old per-call `koffi.alloc` code allocates a ~64 B event
struct plus a 4 B int32 every iteration and never frees either, so it
clears this bound by roughly 2x; the fixed code, which allocates
nothing per call, stays flat. Skips when no native bridge is built or
when Node wasn't started with `--expose-gc`; `npm test` now passes
`--expose-gc` so the check runs by default instead of always skipping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:11:41 +02:00
Meta Vens
f2f261bc3f cultmesh-ts: infer LoadedNativeLibrary from buildLibrary's return instead of restating it
The `LoadedNativeLibrary` interface just re-listed the sixteen fields of
the object literal `loadBindings` already built, one liability the
methods below never needed a second time. Split the library-building
body into `buildLibrary()` and let `LoadedNativeLibrary` be
`ReturnType<typeof buildLibrary>`; `loadBindings` is now a thin cache
wrapper around it. `nextEventAsync`'s signature stays explicit as
`NextEventAsyncFn`, since `promisify` can't infer one from koffi's
untyped `.async`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:11:08 +02:00
Meta Vens
33da8a1298 cultmesh-ts: stop leaking koffi.alloc buffers in the QUIC native binding
A Soul-verified probe found every koffi out-parameter allocation
(runtime open, listener open, connection open, stream open, and both
per-`nextEvent` buffers) was never freed. `nextEvent` runs in the pump's
tight poll loop, so this leaked roughly 53 bytes per idle cycle even
with no traffic, and grew unbounded under real frame volume (214 MB of
RSS over 4M allocations on the pinned koffi 3.3.0 build).

Allocate `nextEvent`'s out-event and out-required buffers once per
runtime instance and reuse them for every call instead of once per
call: the pump never has more than one `nextEvent` in flight, so
decode-then-reuse is safe, and they are freed once in `release()`
after the pump loop exits. Every other allocation (`open`,
`listenerOpen`, `connectionOpen`, `streamOpen`) is synchronous and
short-lived, so those are freed with `koffi.free` right after decode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:10:52 +02:00
Meta Vens
2e7e17d65d Record Cut 5, fix batch 2, retention, and the provider's Soul gate
Cut 5's TS provider reaches the C# native connector on Windows (the Unity
path). Fix batch 2 closed Cut 4's use-after-free, handler and leak
findings, and fixed the root lockfile. Retention seeds late joiners in
both runtimes.

The Soul gate blocked a merge. Every peer is evicted at the handshake
timeout, koffi allocations leak, client frames pile up without bound,
and the chain-validated connect path does not exist. For the last one
the operator ruled a pinned self-signed certificate.

Fixes 3a (provider) and 3c (binding leak) are in flight as separate
per-file batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:04:48 +02:00
Meta Vens
8cf89a7e56 Extend the Cut 5 interop lane: a late C# native connector gets the TS retained frame
Cut C's headline cross-process proof. The TS `serve` peer already tore its
listener down the instant it finished broadcasting, so nothing could dial it
late; it gains `--linger-ms` to hold the listener open afterward instead. The
interop harness starts that provider, runs a throwaway `dial` peer that
receives every frame and disconnects (the provider requires a first peer
before it broadcasts at all, so this is also what proves broadcasting
finished), then only after that dials with the C# native connector and
asserts the frame it gets is the retained one, by payload, not just
non-empty. New `NativeConnectorReceivesTheRetainedFrameWhenJoiningLate` in
GameCult.Mesh.Quic.Native.Tests carries the assertion; the existing
`NativeConnectorReceivesFromExternalProvider` is untouched. Windows-only,
matching the native connector under test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:32:39 +02:00
Meta Vens
7b7de5b071 Fix a flaky disconnect wait in the reliable-ordered retention test
AReliableOrderedFrameIsNeverRetainedAndIsNotDeliveredToALateJoiner disposed
the first session and then waited on server.ConnectionCount == 0 before
attaching a second one, which timed out under Yggdrasil's ygg-verify
container (dotnet test, cultlib-quic-native-dev image): the retention rule
under test does not depend on the first peer disconnecting, so the fix keeps
both sessions connected concurrently and drops the flaky wait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:20:36 +02:00
Meta Vens
5c6398473f Document latest-only retention for newly attached QUIC peers
One sentence each in the C# QUIC README and the transport-planes doc: a
provider retains the newest latest-only frame per (channel, body) and
delivers it to a peer as soon as it attaches, bounded by the number of
distinct keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:17:37 +02:00
Meta Vens
680af452c6 C# QUIC server: retain the newest latest-only frame per (channel, body)
CultMeshQuicRealtimeServer mirrors the TS provider: a `_retained` dictionary
holds the newest latest-only frame per (channel, body) key, updated in
BroadcastAsync with the same CultMeshGeneration comparison the receive-side
filter already uses. AcceptLoopAsync seeds a newly accepted client with the
retained frames. Both the retained update+fan-out and the client
registration+seed run under one lock (`_retainedGate`), so a seed and a
concurrent broadcast are totally ordered and a client can never end up seeded
with a frame older than one it already received via broadcast. Reliable-
ordered frames are events and are never retained.

CultMeshGeneration is now internal so the server can reuse the transport's
own generation comparison instead of a second implementation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:17:28 +02:00
Meta Vens
1c87251d80 TS QUIC provider: retain the newest latest-only frame per (channel, body)
CultMeshQuicRealtimeProvider now keeps a `retained` map of the newest
latest-only frame per (channel, body) key, updated on broadcast using the
same generation comparison the receive-side filter already uses. Every peer
attached after that point is seeded with the retained frames before it is
added to the peer set, so a StreamPixels overlay that connects or reconnects
after state was published converges on it instead of waiting for the next
change. Reliable-ordered frames are events and are never retained.

Operator ruling (2026-09-23, OQ1 = A): the provider retains, not a service
heartbeat or a per-peer attach hook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:17:16 +02:00
Meta Vens
275010a6d6 Fix M2's last hang: stabilize refCount before using it as a test baseline
The mid-handshake test still hung under M2 in the full workspace test run
even after the previous two fixes (pendingAccepts sweep, try/finally).
Diagnosed by printing the stuck value: refCount settled one below its own
"before" snapshot, not above — the opposite of a leak. Root cause:
CultMeshQuicRealtimeProvider.dispose() releases its runtime reference
fire-and-forget (`void this.runtime.release()`), so when M2 makes every
preceding test's own dial fail during the handshake too, their releases
can still be landing when this test starts and snapshots "before". One
lands mid-test, after the snapshot, and refCount never returns to a value
that already excluded it.

Added waitForRefCountToStabilize(): polls until refCount stops moving for
a short quiet window before treating it as a baseline. Verified against
the actual M2 mutation, in the full `npm run test` workspace run (not
just this one file, which had already stayed clean in isolation): the
file now exits in the low tens of seconds with 9 expected failures (every
test that dials through the now-broken cert check) and none of the
"Promise resolution is still pending" hangs from the three earlier
attempts. Confirmed clean on the unmutated tree first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:02:42 +02:00
Meta Vens
2cd63f3ee9 Bound the remaining fault-path tests against a regressed fault/cleanup path
Same hygiene sweep as the previous commits, applied to the three tests
whose own subject is a fault or cleanup path that, if it regressed, would
leave a receiveFrame() call with nothing to ever resolve or reject it:
the two "fix 1" throwing-handler tests (a malformed frame that should
fault the connection, and a peer shutdown that should run cleanup()) and
the "fix 3" peer-shutdown test. Each now wraps its transport in
try/finally and uses receiveFrameOrTimeout() instead of a bare
receiveFrame() behind assert.rejects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:42:31 +02:00
Meta Vens
f5d1de0a8c Wrap the mid-handshake test's provider in try/finally
Re-running M1 after the previous fix: the assertion now correctly failed
(assertRejectsAndDisposes disposed the resolved client transport), but
the file still hung. Root cause: under M1 the handshake actually
completes (the connector accepts any certificate), so the provider's
peer is fully attached rather than left mid-handshake, and the test's
`provider` was never wrapped in try/finally — the failed assertion threw
past `provider.dispose()`, leaking the listener and the now-live peer.
provider.dispose() is idempotent, so wrapping the whole body costs
nothing on the normal path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:36:58 +02:00
Meta Vens
20ffef7cc6 Apply the same assertRejectsAndDisposes guard to the new provider test
The "dispose() evicts a connection still mid-handshake" test (previous
commit) used a bare assert.rejects(connector.connect(...)) to force the
handshake-rejection path it needs, which is exactly the pattern that hung
realtime-quic-consumer.test.ts under M1: found re-running M1 against the
full workspace after the previous fix — it hung again, this time in
realtime-quic-provider.test.ts, at this new test, for the same reason.
Ported the same assertRejectsAndDisposes() helper into this file rather
than importing it across test files (each file already keeps its own
small helpers).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:29:27 +02:00
Meta Vens
a390f61053 Fix: provider.dispose() left mid-handshake connections dangling
Found running the M2 mutation (certificate pin compared case-sensitively)
on Yggdrasil: it broke the handshake for every provider test that dials
with the provider's uppercase-advertised pin, each test correctly failed
its own assertion in under 40ms, but the whole file then hung for two and
a half minutes past its last visible test result.

CultMeshQuicRealtimeProvider.dispose() only disposed connections already
in `this.peers` (attached after reaching CONNECTED). A connection whose
handshake never gets there — a rejected certificate, a stalled client —
sits in acceptConnection()/attachPeer()'s own state with its own
handshakeTimeoutMs timer and, once attachPeer's own
CultMeshQuicNativeRuntime.open() resolves, its own runtime reference,
entirely outside dispose()'s reach. Nine tests each leaked one such
pending accept under M2, each held open for up to its 10s default
handshake timeout, serialized enough to add up to the observed hang.

Fixed by tracking every in-flight accept's `evict` closure in a
`pendingAccepts` set that dispose() sweeps alongside `peers`; each accept
removes itself once it settles, whether attached or evicted, so the set
never grows for a healthy, long-lived connection. Added a regression test
that pins a handshake to reject on a long (30s) handshakeTimeoutMs and
asserts dispose() releases the runtime reference in under 2 seconds, not
after the timeout.

Also fixed the same underlying test-hygiene gap the M2 investigation
surfaced directly: the M4 and M4b tests (a stream-kind/delivery mismatch
should fault a connection) left `transportA` undisposed with an unbounded
`receiveFrame()` behind a bare `assert.rejects`; if the fault path
regresses, that call hangs forever instead of failing. Added
receiveFrameOrTimeout() (a bounded receiveFrame, in the same spirit as
assertRejectsAndDisposes()) and a `finally { transportA.dispose(); }` to
both tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:23:36 +02:00
Meta Vens
4525c44ef9 Regenerate the root lockfile: koffi was missing since Cut 4
packages/cultmesh-ts/package.json has depended on koffi 3.3.0 since Cut 4
landed the native QUIC binding, but the root package-lock.json was never
regenerated to match: it carried no koffi entry at all, so `npm ci` at the
repo root failed with EUSAGE. The StreamPixels deploy host runs exactly
`npm ci && npm run build:ts`, so this blocked deployment.

Regenerated on Yggdrasil (Linux, the cultlib-quic-native-dev container),
not on the operator's workstation, via `npm install`. Verified from a
clean clone on the same host: `npm ci && npm run build:ts` both succeed
against the regenerated lockfile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:06:46 +02:00
Meta Vens
d9a406f9ff Fix hygiene: connect() rejection tests no longer leak a resolved transport
Found running the M1 mutation (bypasses the certificate-accept check) on
Yggdrasil under cgroup-limited CPU: three tests correctly failed their
assertion (`assert.rejects(connector.connect(...))` / `manager.connect(...)`
observed a resolve instead of a reject), but realtime-quic-consumer.test.js
then hung instead of exiting — "Promise resolution is still pending but the
event loop has already resolved". Each of those assertions discarded the
connect() call's return value on the assumption it would always reject, so
the transport that unexpectedly connected under the mutant was never
captured, never disposed: a live native connection, an active
onConnectionEvent registration and an outstanding runtime reference kept
the process alive after node:test had already recorded the failures. This
is the exact scenario the batch names by history ("Soul saw M1 and M3 fail
their assertion and then hang the file").

Added `assertRejectsAndDisposes()`, a drop-in replacement for
`assert.rejects()` that disposes whatever the promise resolves to before
failing the assertion, and used it at all eight call sites in this file
that assert a connect() call rejects (two direct connector.connect() calls,
six through the session manager). Verified on Yggdrasil: M1 re-run now
fails the same three assertions and the file exits on its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:59:48 +02:00
Meta Vens
e943159a47 Fix an orphaned pendingSends race, add Cut 5 runtime-release and eviction tests
Real bug found while writing the "dispose with a pending send in flight"
regression test, not a pre-scoped fix: sendOnStream() registered a
pendingSends entry unconditionally. If the reliable-send microtask queued
by sendFrame() (chained onto reliableSendTail) ran after cleanup() had
already executed — cleanup()'s own reject-sweep runs once and is guarded
against re-entry, and offConnectionEvent means no later native event can
resolve the entry either — that entry's promise then never settles,
leaking the pending send forever. Confirmed by a hanging test that
reproduced intermittently (~20-25% of runs) when a peer transport's
dispose() raced an in-flight reliable-ordered sendFrame() on the same
transport; not reproducible in isolation outside the full node:test file
(over 100 standalone cycles clean), which points at the race window being
narrow enough that only real inter-test timing variance opens it.
sendOnStream() now checks `this.disposed` before registering, closing the
gap: a send that loses the race rejects immediately instead of
registering an entry nothing will ever settle.

Also gives that regression test its own `{ timeout: 10_000 }` so a
recurrence fails that one test loudly instead of hanging the whole file
for the harness's 60s+ default; 60 consecutive full-file runs were clean
with both changes in place (25 with the fix alone still showed the hang
occasionally, so the fix closes a real but not the only contributing
factor — recorded here rather than claimed as a full proof).

New coverage for the Cut 5 provider's runtime-release-on-every-path
requirement:
- a reliable-ordered send failure evicts the peer without broadcast()
  throwing to the caller;
- the runtime reference is released when the listener itself fails to
  open (a real PKCS12 credential, but a port already bound by another
  listener, so listen()'s own `catch { await runtime.release(); throw
  error; }` actually runs) — split out from a same-named test that had
  only exercised an earlier failure point (extractLeafCertificateDer(),
  before CultMeshQuicNativeRuntime.open() is ever called), which is kept
  as its own, more honestly named test;
- dispose() with a pending send in flight releases the runtime reference
  (the test whose flakiness led to the fix above).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:36:41 +02:00
Meta Vens
992d82e095 Add the release() use-after-free regression test (fix 2)
Ports Soul's uaf.js probe (scenario a: bare open/release cycles,
including a fresh open overlapping the previous runtime's close) into a
committed test. UV_THREADPOOL_SIZE and MALLOC_PERTURB_ must be set before
Node starts, so the test spawns a child `node` process running
test/support/release-uaf-probe.js (plain JS, not compiled, requiring
dist/ directly like the package's own compiled tests) once per pool size
(1, 2, 4) and asserts each exits 0.

Trimmed from the probe's original 300 cycles to 20: at pool size 1 with
the crypto.pbkdf2 saturation load, 300 cycles took over two minutes on
this workstation, and the ordering bug this test defends does not need
that many iterations to reach the race — 20 keeps the whole test under
about 25 seconds while still exercising many open/release cycles with a
fresh open racing a still-closing runtime.

Verified by hand: reverting release()'s order in
realtime-quic-native.ts (runtimeCloseFn before awaiting pumpLoop, the
pre-fix order) did not make this test fail on Windows — MALLOC_PERTURB_
is a glibc feature the Windows CRT ignores, so corruption here is silent
rather than a deterministic crash. The revert-and-confirm check that
actually demonstrates the bug runs on Yggdrasil's Linux container, where
glibc honors MALLOC_PERTURB_; see the verify report for that run's
output. The fix was restored immediately either way, confirmed by a clean
git diff before this commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:14:42 +02:00
Meta Vens
9cf0634f74 Add fix-1 handler-isolation tests for both fault paths
Two scenarios the batch specifically calls for: a throwing handler plus a
garbage frame (the native fault path, through faultConnection()'s now
try/catch-guarded onFault) and a throwing handler plus a peer shutdown
(the transport's own cleanup(), now releasing before running handlers).
Both assert the second onDisposed handler still runs despite the first
throwing, and the peer-shutdown case also asserts
CultMeshQuicNativeRuntime.refCount returns to baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:06:16 +02:00
Meta Vens
932d1cc8ef Add tests for M4b, M10, F7 and strengthen F6kinds
M4b: the mirror image of the existing M4 test — a latest-only stream
carrying a frame whose own delivery byte says reliable-ordered must fault
only that connection, not the pump.

M10: an equal-generation frame is dropped by the receive-side filter.
Sequenced so it is observable (Soul's note): the first frame is consumed
before the duplicate generation arrives, so the coalescer no longer holds
a pending value the duplicate could silently overwrite with itself.
Without this sequencing, M10's `<=0` -> `<0` mutation is invisible: the
coalescer's overwrite semantics make an unconsumed duplicate look
identical to a dropped one.

F7: split into two tests, because a genuine end-to-end
SEND_CANCELED-through-sendFrame() scenario proved unreliable on this
host. The native-layer test (realtime-quic-native.test.ts) is
deterministic: it round-trips two 64-bit codes above 2^53 through
CONNECTION_SHUTDOWN, exercising the exact decode line F7 mutates
(`code: toBigInt64(decoded.code)`, shared by every native event type,
including STREAM_SEND_COMPLETE). The consumer-level attempt — prime a
reliable stream, start a second send, shut the connection down from the
peer mid-flight — is recorded as a comment: MsQuic here appears to
locally complete a `StreamSend` (as an uncanceled SEND_COMPLETE) faster
than any JS-observable window can land a peer shutdown ahead of it,
regardless of payload size (tried up to 8 MiB) or an explicit delay
before the shutdown call. Recorded as not yet reached at the consumer
layer, not as unreachable.

F6kinds: the existing "stream shutdown prunes streamKinds" test only
proved a second frame still worked afterward, which passes whether or not
the map is actually pruned (a stream id is never reused within one
connection, so a leaked entry has no other behavioural symptom — exactly
why the mutation survived). Strengthened to assert
CultMeshQuicRealtimeTransport.trackedStreamCount (added in the previous
commit) directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:04:52 +02:00
Meta Vens
40c065c092 Add tests for dial cancellation, F5abort and F5loser
Covers the session-manager fixes in the previous commit:
- dispose() aborts a dial in flight instead of letting the connector's own
  timeout run out before the transport is discarded (asserted by
  wall-clock bound, not just by eventual rejection).
- disconnect() during a dial does not let that dial cache a session once
  it lands; the manager stays usable afterward.
- F5abort: a losing race candidate is told to abort promptly, proven by
  recording when spyConnector's own abort listener actually fires rather
  than only asserting eventual disposal (which the mutation's removed
  controller.abort() call does not affect, since the cleanup loop that
  disposes settled losers is untouched by that mutation).
- F5loser: a losing candidate that connects anyway, past the point abort
  would have helped (spyConnector gains an ignoreAbort option to model
  this), is disposed rather than left connected.

Replaces the old "fix 4: a connect() that completes after dispose()"
test, which pinned the pre-fix behaviour of waiting out the connector's
full delay before discarding the late transport; that delay is exactly
what dispose() aborting the dial now removes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:53:34 +02:00
Meta Vens
1c6b946efc Fix handler isolation and connect()/dial cancellation, subtract dead ceremony
Fix 1 (consumer half): CultMeshQuicRealtimeTransport.cleanup() ran
onDisposed handlers before releasing the runtime reference, unguarded. A
throwing handler skipped every handler queued after it (the throw exits
the for-of) and skipped the release entirely: on the peer-shutdown path
the refcount stuck and the process never exited; on the fault path the
throw escaped cleanup() into dispatch()'s catch in the native runtime,
which is itself now guarded (previous commit), but cleanup()'s own
contract should not depend on that. release() now runs first, and each
handler runs in its own try/catch.

Low fixes:
- receiveFrame(signal) with an already-aborted signal now rejects
  immediately instead of registering an "abort" listener that already
  fired and will never fire again, which hung the call forever.
- CultMeshQuicRealtimeConnector.connect() merged its four failure paths
  (already-aborted, handshake timeout, later abort, handshake-time error)
  into one fail() closure. The handshake-time error path was the one that
  mattered: it dropped the runtime reference and rejected but never called
  connectionShutdown on the native connection, leaking it at the native
  level on every rejected handshake past the certificate stage.
- CultMeshQuicRealtimeSessionManager.dispose() now aborts every dial still
  in flight instead of only marking sessions disposed and hoping a live
  dial notices later; disconnect() aborts the one in-flight dial for its
  target so a race with an in-progress connect() cannot cache the session
  disconnect() meant to discard. Threaded through dialAsync/
  raceFirstSuccessAsync/connectCandidateAsync as an AbortSignal that
  combines with the per-race internal controller.

Subtraction (~-70 net in this file despite the fixes above and new
trackedStreamCount/dialAborts surface):
- SimpleGate replaced by a plain promise-chain field on the transport
  (reliableSendTail); the class only ever wrapped one method.
- The two dead streamKinds.set() writes on outbound (self-opened) streams
  removed: the map is only ever read for streams that deliver an inbound
  STREAM_FRAME event, which a stream this side opened never does.
- The write-only authorityRoute field on CultMeshRealtimeCandidate
  deleted; nothing read it.
- The one-use TimeoutError class deleted in favour of Error.
- routeCertificateView moved to cultnet-ts (previous commit); the local
  copy deleted.

Also exposes CultMeshQuicRealtimeTransport.trackedStreamCount (streamKinds
map size) for the F6kinds regression test: nothing else could observe
whether STREAM_SHUTDOWN actually prunes the map, since a stream id is
never reused within one connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:53:22 +02:00
Meta Vens
ae24cb3633 Guard the native connection's fault handler, collapse the bindings wrapper
Fix 1 (native half): faultConnection() called the caller-supplied onFault
handler unguarded. A throwing handler escaped faultConnection(), then
dispatch(), then the pump's async loop as an unhandled rejection nobody
awaits, crashing the process on a single bad connection. Wrap the call and
report instead of propagating, matching the try/catch dispatch() already
has around the listener call that reaches faultConnection() in the first
place.

Subtraction: fold the private NativeBindings interface and the
bindingsCache wrapper object into the runtime class's own methods, which
already typed the same twelve calls one door down. The wrapper only
restated that typing a second time. loadBindings() now returns the raw
koffi handles (untyped, koffi has no useful types) and the class methods
do the alloc/decode work directly, the way half of them already did for
their out-parameters. Net -101 lines despite the added refCount getter and
a trimmed module doc comment (no longer narrating the koffi probe process
twice).

Also exposes CultMeshQuicNativeRuntime.refCount as a public static getter
(renaming the backing field to refCountValue): the batch's regression
tests assert it returns to baseline after a fault, a peer shutdown, and
release(); nothing else needed to reach into a private static before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:52:59 +02:00
Meta Vens
da0935b658 Share the route-certificate view between cultmesh-ts and cultmesh-browser
Both packages mapped an advertised route's wire-shaped certificate
(providerKeyId/providerPublicKeyX/providerPublicKeyY/...) to
CultMeshAuthorityRouteCertificate independently. Move the mapping into
cultnet-ts's cultmesh-authority module, next to verifyAuthorityRoute, and
have both consumers call it instead of duplicating the object literal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:52:46 +02:00
Meta Vens
fd2a701bb2 Pair the golden sequence and hex lines instead of matching independently
Found by a second real failure, this time on Windows: `dotnet test --logger
"console;verbosity=detailed"` prints a passed test's TestContext.Out lines
twice (live-streamed, then again under "Standard Output Messages:"), and
matching CULTMESH_GOLDEN_SEQUENCE and CULTMESH_GOLDEN_HEX with two
independent regexes let the harness pair a sequence from one copy with a hex
from the other, producing a spurious mismatch even though the C# and
TypeScript sides agreed. One regex now requires them adjacent, exactly as
the C# test's two back-to-back WriteLine calls print them. All four QUIC
interop lanes now pass on Windows (real dotnet test run, native connector
lane included).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:24:21 +02:00
Meta Vens
92f2566d2c Fix the golden-bytes lane to match by sequence, not assume frame 1
Found by a real failure on the Linux verification container: latest-only
coalesces on both the provider's outbox and the managed connector's own
receive-side inbox, so a single ReceiveAsync() call is not guaranteed to
land on the first frame broadcast — on a slower host it landed on sequence 3.
The C# test now also prints CULTMESH_GOLDEN_SEQUENCE, and the harness looks
up the TypeScript peer's logged encoding of that same sequence instead of
hardcoding sent === 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:21:03 +02:00
Meta Vens
a35c46afff Fix the interop lanes' dotnet-test success assertion to match real output
Verified live on Windows: a passed NUnit test under `dotnet test --logger
"console;verbosity=detailed"` prints "Test Run Successful." and "Passed: N",
never the literal "Passed!" the assertion was checking for. Both the native
and managed connector lanes now match the real strings; both were confirmed
to pass for real against a live TypeScript provider (lane 1: 137ms, lane 2:
145ms with the golden-bytes hex line matching the TypeScript peer's own
encoding byte for byte).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:18:37 +02:00
Meta Vens
d526286d60 Add Cut 5's four QUIC realtime provider interop lanes
TypeScript provider vs C# native connector (the Unity path, gated to
Windows since the native connector under test is); TypeScript provider vs
C# managed connector, with a golden-bytes check comparing the C# test's
re-encoded frame against the TypeScript peer's own encoding of the same
frame; latest-only under a stalled consumer (two dials against one 200-frame
provider, one paused mid-stream via the peer script's new --pause-after),
asserting the provider's broadcasts stay bounded regardless of the pause and
both consumers converge on sequence 200.

Both dotnet-test lanes run `dotnet test --filter FullyQualifiedName~<test>
--logger "console;verbosity=detailed"`: detailed verbosity is what surfaces
a passed test's own TestContext.Out lines, which the golden-bytes lane reads
back out of stdout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:10:44 +02:00
Meta Vens
fa8a9f4e31 Let the TS interop peer prove convergence and golden bytes for lane 3/4
dial gains --expect-sequence (stop once a frame with that sequence arrives,
rather than counting frames received — the number of frames a stalled
latest-only reader sees is not the interesting value, what it converges on
is) and --pause-after/--pause-ms, which simulate the stalled-consumer test
hook at the only level this subprocess can reach: not calling receiveFrame
for a while. The shared native pump keeps dispatching and coalescing into
this transport's own inbox regardless of whether the app reads, so not
reading is externally equivalent to not polling.

serve now also logs each broadcast frame's own TypeScript encoding as hex, so
the harness can compare it against the same frame's C#-side re-encoding
without importing the codec itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:09:37 +02:00
Meta Vens
962162e7c6 Rename and add the C# sides of Cut 5's external-provider lane
NativeConnectorReceivesFromExternalManagedProvider -> NativeConnectorReceivesFromExternalProvider:
CULTMESH_NATIVE_EXTERNAL_ENDPOINT's peer is no longer necessarily the C#
managed provider now that Cut 5 adds a TypeScript one on the same lane; the
env var name stays, since it names the endpoint, not the peer.

Add ManagedConnectorReceivesFromExternalProvider, gated by the same env var:
dials an external provider with the plain managed connector and no
ValidateProviderCertificate (so acceptance runs on the endpoint's own
cert-sha256 pin alone, like an ordinary consumer), and prints the received
frame's CultMeshRealtimeWireProtocol re-encoding as hex for the harness's
golden-bytes comparison against the TypeScript peer's own encoding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:06:15 +02:00
Meta Vens
25d749067e Add a serve mode to the TS QUIC interop peer for the provider lane
cultmesh-quic-peer.ts serve opens a CultMeshQuicRealtimeProvider on the same
test fixture credential the unit tests use, prints {status, endpoint} in the
shape spawnServeProcess already expects, waits for a first peer before
broadcasting (a provider's broadcast to zero peers silently sends nothing,
same reasoning as the C# quic-realtime-serve peer's
WaitForFirstConnectionAsync), then broadcasts --frames frames at --delivery
with --interval-ms between them, stopping early on stdin EOF like the C#
peer's WatchStdinCloseAsync.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:06:05 +02:00
Meta Vens
3f546fc5cd Add unit tests for the QUIC realtime provider
Reliable-ordered ordering in both directions, latest-only coalescing (a
synchronous back-to-back publish is deterministically coalesced, not by
network timing luck), a stalled peer's outbox holding at most one pending
frame per key across 200 broadcasts, a non-blocking broadcast, the uppercase
cert-sha256 pin, unreliable failing closed, and eviction on peer shutdown.
Runs against the real native bridge and Cut 4's real consumer connector, no
mocks; skips when CULTMESH_QUIC_NATIVE_DIR is unset, matching every other
QUIC test file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:05:56 +02:00
Meta Vens
e938cb9e59 Add the TypeScript QUIC realtime provider and its coalescing outbox
CultMeshQuicRealtimeProvider (packages/cultmesh-ts/src/realtime-quic.ts) is
the StreamPixels role: listen() opens a native listener, computes an
uppercase cert-sha256 advertisedEndpoint (the Unity connector requires the
pin), and accepts peers into a Set of the same CultMeshQuicRealtimeTransport
Cut 4 already built for consumers. broadcast() mirrors
CultMeshQuicRealtimeServer.BroadcastAsync: reliable-ordered awaits every
peer's send_complete via transport.sendFrame directly (Task.WhenAll,
evict-on-failure); latest-only publishes into a new per-peer
CultMeshQuicRealtimeProviderOutbox and returns without waiting on any peer's
I/O. The outbox pumps sendFrame's existing fresh-stream-per-frame path one
frame at a time, so a stalled peer holds at most one pending frame per
(channel, body) key.

Extracted CultMeshRealtimeLatestOnlyCoalescer from the inbound path's
inline Map+FIFO bookkeeping (unchanged behaviour) so the new outbox reuses
the same coalescing idiom instead of a second inline copy.

Accepting a connection buffers native events behind a synchronous listener
registration (the pump dispatches strictly sequentially, so registering
before any await is what makes the async runtime-reference acquisition that
follows safe) and replays them once the peer's CultMeshQuicRealtimeTransport
exists; a handshake timer evicts a peer that never completes CONNECTED.

Wired into the CultMesh facade as createQuicRealtimeProvider.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:05:47 +02:00
Meta Vens
f1ac8f301b Record QUIC Cut 4's Soul gate, the interop cause, and fix batch 1
The Soul gate refused Cut 4. A malformed peer frame killed the process,
release() was a use-after-free, a peer shutdown leaked, the session
manager never went offline, the race waited on the slowest candidate,
growth was unbounded, and eight spec rules had no test.

The interop failure turned out to be test infrastructure, not the
network: the harness gave the peer /dev/null for stdin, the peer
broadcast before any client connected, and .NET could not find
libmsquic.

Fix batch 1 passes 125/125, and the C#-provider-to-TS-consumer lane
passes. The cut grew instead of shrinking. A confirming Soul pass and
Cut 5 are running in parallel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:48:44 +02:00
Meta Vens
8e32bff135 Add a provenance note for the QUIC test fixtures
The PKCS12/DER pair had no record of how it was made or what it is. Documents
the CN, the openssl recipe that reproduces an equivalent pair, and that
nothing depends on today's exact bytes past what the tests read back out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:46:25 +02:00
Meta Vens
72da8862b3 Fix the spawnServeProcess stdio type after making stdin optional
A non-literal first stdio element loses spawn()'s typed-tuple overload
entirely (verified on Yggdrasil: tsc failed on the two other, unrelated
serve helpers' stdout/stderr going nullable), not just for stdin's varying
element. Asserted at the one call site instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:42:18 +02:00
Meta Vens
e4e81d1628 Install libmsquic system-wide in the QUIC dev image so .NET can find it
Finding 8(c): the dev image builds the Node bridge's own libmsquic copy into
a build-local directory (scripts/build-quic-native.sh), which is never on the
loader's search path. .NET's System.Net.Quic resolves "libmsquic" through
that path independently and failed with PlatformNotSupportedException before
this. The smallest honest fix: dpkg -i the same pinned 2.5.9 deb (same URL,
same SHA-256 as the build script) system-wide and run ldconfig, so both the
Node bridge's own copy and .NET's independent resolution find a libmsquic.

Changes the image; the new tag is cultlib-quic-native-dev:75cb89e73850
(first 12 hex of this file's sha256), built fresh on Yggdrasil.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:39:05 +02:00
Meta Vens
499315afc5 Keep stdin piped for the QUIC interop peer, and close it at teardown
Finding 8(a): every spawnServeProcess call used stdio ["ignore", ...], which
hands the C# quic-realtime-serve peer an already-closed stdin. Its own
WatchStdinCloseAsync (Program.cs:1261-1269) treats that as EOF and tears the
serve loop down at once, before it can send anything. spawnServeProcess gains
an opt-in `stdin: "pipe"` (default stays "ignore" for every other serve mode,
which never reads stdin), used only by the "CultMesh QUIC realtime" test; its
teardown ends that pipe explicitly before killing the process.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:38:58 +02:00
Meta Vens
fbbc6dcfc1 Wait for a client before quic-realtime-serve broadcasts
Finding 8(b): the serve mode broadcast immediately after printing its ready
line, before the harness's dial could reach it, so frames went to an empty
peer set and were silently dropped. WaitForFirstConnectionAsync polls
ConnectionCount, bounded at 30s so a peer that never gets dialed still exits
instead of hanging the interop lane forever.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:38:51 +02:00
Meta Vens
047c55c453 Add tests pinning fixes 1-6, M2, M3, M6-M9, and dedupe the two nativeBridgeAvailable helpers
Each rule below fails under its own mutation of the code fixed in the previous
two commits; verified by hand and restored, per the fix-batch brief, not
committed as a mutation harness (the operator retired that pattern).

- realtime-quic-native.test.ts: release() refuses a call with no outstanding
  reference (fix 2).
- realtime-quic-consumer.test.ts: a stream-kind/delivery mismatch faults only
  its connection while a second, independent connection through the same
  pump keeps working (fix 1 / M4); a peer-initiated shutdown rejects a
  pending receive and cleans up exactly once, so a further dispose is a
  no-op and a fresh receive rejects instead of hanging (fix 3); a slow
  latest-only reader accumulates at most one pending frame per key, and an
  equal generation is dropped outright (fix 6, M10); a real connected
  transport's isVerifiedFor rejects a generation other than the one it
  connected with, and the session manager disposes a real (not stubbed)
  transport whose generation no longer matches a freshly verified route
  (M3); the advertised certificate pin matches case-insensitively (M2).
- realtime-quic-session-manager.test.ts (new): eviction of a dead cached
  session and redial on the next connect, single-flight sharing one dial
  across concurrent callers, refusing a transport that lands after dispose()
  (all fix 4); the race returning on first success without waiting out a
  slow candidate (fix 5); the maxRacedCandidates cap (M6); the protocolIds
  and authorityRuntimeId route filters (M7, M8); the session key including
  authorityRuntimeId (M9). Trust verification is skipped via
  mode: "local-development" over loopback endpoints, since these tests are
  about the session manager's own binding/racing/caching rules, not route
  trust (already pinned by realtime-quic-consumer.test.ts's trust
  negatives).

Subtraction: `nativeBridgeAvailable` was defined twice, disagreeing on
whether to check the dependency library too; both test files now import one
definition from test/support/native-bridge.ts (the stricter of the two).

Gap, reported rather than papered over: the stream-shutdown pruning half of
fix 6 (streamKinds) has no direct test, because the map is private and
nothing here exposes it; the committed test only shows a second frame still
arrives after the pruned stream's shutdown, which does not by itself prove
the entry was removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:38:46 +02:00
Meta Vens
04858d332a Fix the QUIC consumer's peer-shutdown leak, and give the session manager eviction, single-flight and first-success racing
Soul's fix batch, findings 3, 4, 5 and 6:

- A peer-initiated CONNECTION_SHUTDOWN only set `disposed = true`, so
  `dispose()`'s early-return skipped `release()` and `offConnectionEvent`: the
  runtime reference leaked forever and a pending `receiveFrame` hung forever.
  `disposed` and "already cleaned up" are now the same flag, run through one
  `cleanup()` reached from `dispose()`, a peer shutdown, and the new `fault()`
  (wired from fix 1's `onFault`). Every path releases the runtime reference
  exactly once and rejects every pending and future receive/send with the real
  reason instead of a generic "disposed" message.
- `CultMeshQuicRealtimeSessionManager.connect()` cached a transport forever,
  even a dead one, and raced candidates with `Promise.allSettled`, so one dead
  route delayed every connect by the full handshake timeout. It now: evicts a
  session once its transport reports itself disposed (`onDisposed`, mirroring
  `CultMeshSessions.InvalidateRealtimeSession`); shares one in-flight dial
  across concurrent callers for the same target (mirrors C#'s `Lazy`); refuses
  a transport that lands after `dispose()` instead of caching it; and races
  each tier for the first verified success (mirrors `Task.WhenAny`), aborting
  and disposing the losers via an `AbortSignal` threaded into
  `connector.connect`, which now also honours a signal already aborted at
  entry.
- `streamKinds` never pruned a finished stream (`STREAM_SHUTDOWN` fell into
  `default`); it now removes the entry. The receive side queued every
  latest-only frame in an unbounded array; it now mirrors the C# reference's
  `CultMeshRealtimeInbox`: at most one pending frame per `(channel, body)`,
  coalesced on arrival, while reliable-ordered frames still queue directly
  (unbounded, matching the C# reference's own unbounded channel).

Subtraction: `trimmedOrEmpty` duplicated `cultnet-ts`'s exported `trimCSharp`;
callers now use the shared one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:38:34 +02:00
Meta Vens
245a9fdf52 Isolate the QUIC native pump from a faulting connection, fix release()'s UAF ordering, and make 64-bit koffi decodes honest
Soul's fix batch, findings 1, 2 and 7:

- The pump's dispatch had no fault boundary: a malformed frame, a stream-kind
  mismatch, or a throwing caller callback (certificate validator, waiter) threw
  out of `dispatch` and killed the pump, taking the whole process with it.
  `dispatch` now catches per-connection, shuts that connection down at the
  native level, removes its listener, and hands the error to an optional
  `onFault` callback registered alongside the listener. Every other connection,
  and the pump itself, keep running.
- `release()` called `runtimeClose` before awaiting the pump's exit, so a
  queued-but-not-yet-started `nextEvent.async` call could begin after close and
  run against freed memory. It now marks closed, awaits the pump loop (it wakes
  within its 250 ms timeout), and only then closes. It also refuses a release
  with no outstanding reference instead of silently flooring at zero.
- koffi decodes a 64-bit `uint64_t`/`void *` as a plain `number` when it fits a
  safe integer, not `bigint` (probed on the pinned 3.3.0 build against the real
  bridge). Every declared type here promises `bigint`; `toBigInt64` is now the
  single boundary conversion, applied to every 64-bit decode, so the mismatch
  that made `event.code === BigInt(CULTMESH_QUIC_SEND_CANCELED)` never true is
  fixed at its source instead of patched at that one comparison.

Subtraction: deleted the unused `lastStatus` export (wired but never called),
and collapsed the stacked eslint-disable-next-line pair over one line into one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:38:22 +02:00
Meta Vens
4247a978ae Record QUIC Cut 3 closed: Windows release leg and F5 on both platforms
The Windows release configuration passes against the pinned OpenSSL
MsQuic. The DLL hash matches the pin, so the wrong-DLL load did not
happen again.

F5 was an operator fork. The operator allowed load on Starfire with
warning. The first Windows run under 16 burners failed 5 of 5. The
cause was the closerace fixture: its poll timeout (5 s) was shorter
than its own settle budget (30 s). The bridge was not at fault. After
the fixture fix at 44ab376, closerace passes 5 of 5 under load on both
Windows and Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:08:10 +02:00
Meta Vens
44ab3767ce Make closerace's poll timeout outlast its own settle budget
Under CPU starvation (16 burners on 8 logical CPUs), all 5 rounds failed
at iteration 0 or 1 with "256 of 256 pollers returned before the close
began, so they were never in the race". The fixture contradicted itself:
the asserts-on settle waits up to 30s (plus a 50ms remainder) for every
poller to be counted inside the library, but each poller's own
next_event wait used a 5s timeout, so the first pollers timed out for
real while the settle was still waiting on the last ones. The race never
ran; the fixture just gave up on itself.

kPollTimeoutMs is now derived from the settle budget it must outlast in
every build configuration (asserts-on's counted deadline plus its fixed
remainder, and the no-seam #else's longer fixed window), named side by
side so they cannot drift apart again. A long poll timeout costs nothing
on a passing run, since the close is what actually ends each wait.

Also give the fixture's own failure to settle a distinct report. If
counted_deadline expires before every poller is counted, CloseRaceOnce
now says "fixture could not settle: N of M pollers counted inside after
30000 ms" instead of proceeding to a close that blames the pollers for a
race the fixture never set up.

holdclose, latecall and holdtimeout use the same counted-deadline idiom
but are not the same defect: their pollers are parked behind
cultmesh_quic_debug_hold_calls, so they cannot time out for real while
the settle waits, and were left alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 13:40:42 +02:00
Meta Vens
c57b613582 Move QuicRealtimeServeConfig to the file's type-declaration tail
Program.cs is a top-level-statements file: every type declaration (the
existing ServeConfig, DialConfig and friends) lives after every top-level
statement and local function, all clustered at the end. The new record landed
between two local functions instead, which is CS8803: "top-level statements
must precede namespace and type declarations." Moved beside DialConfig, where
its neighbors already live.

Found by a direct `dotnet build` run on Yggdrasil after the interop test's own
error message was truncated by Node's console printing before reaching the
actual compiler diagnostic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 13:18:20 +02:00
Meta Vens
b4ddf49cc7 Run .NET globalization-invariant in the QUIC dev image
The interop lane's dotnet build FailFasts on startup in this minimal Debian
image: no ICU package, and .NET's globalization stack aborts without one
rather than degrading. The interop peer already parses every argument through
CultureInfo.InvariantCulture, so invariant mode costs it nothing; it is also
Microsoft's own documented fix for a minimal container, cheaper than pinning
and installing an ICU package by name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 13:14:44 +02:00
Meta Vens
26c026a8c6 Fix the runtime leak that hung the previous ygg-verify job
Self killed a verify run after it sat idle for 25+ minutes past its last
printed result and root-caused it: SIGUSR2 diagnostics on the stuck process
showed an empty JS stack, every libuv handle unreferenced, and MsQuic's own
worker threads parked in epoll_wait — a native runtime that was opened but
never closed, so its registration and threads never tore down and the process
never exited on its own.

The cause was mundane and compounds badly: `RawQuicListener.open()` in
realtime-quic-consumer.test.ts opened a native runtime, then read a test
fixture whose path was wrong (see below) and threw — with no try/catch, so
the runtime it had just opened was never released. That test-file pattern ran
on seven separate tests in one process, each leaking its own reference to the
one process-wide shared runtime; by the time the file's tests finished, the
refcount had never reached zero and the live registration held the process
open indefinitely. `RawQuicListener.open()` now releases the runtime on any
setup failure before rethrowing.

Two more real bugs the same run's fixture-path failures exposed:

- Both new test files resolved fixtures at `join(__dirname, "fixtures", ...)`.
  `__dirname` at runtime is `dist-test/test`, and binary fixtures are never
  compiled or copied there — only `.ts` sources under `test/` and `src/` are.
  Both now read fixtures from their real location, two directories up.
- The negative-grep test looked for a literal `.async(` call. The bugfix in
  the previous commit references `nextEventFn.async` as a value handed to
  `promisify`, never written with a trailing `(` at the reference site, so the
  grep found nothing to check and failed by construction. It now matches
  `.async` as a property reference, not only a call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 13:13:24 +02:00
Meta Vens
7b8b948d1d Rebuild the koffi out-parameter marshalling on probed behaviour, not a guess
A `node -e` probe against the real bridge (loaded through koffi 3.3.0 on
Yggdrasil) showed the `_Out_` prototype marker does not auto-allocate storage
or reshape the return value: `openFn(null, null)` returned the plain scalar
`-1`, because the null "out" pointer is itself a bad call under the header's
own contract, and koffi never populated anything because there was nothing to
write into. `openFn(null, koffi.alloc("void *", 1))` returns the plain
`int32_t` status and the pointer decodes correctly with
`koffi.decode(buf, "void *")`, as a `bigint`.

Every out parameter is now `koffi.alloc(type, 1)` before the call and
`koffi.decode` after, inside `loadBindings`; the rest of the file is
unaffected, since it already treated ids as `bigint`. The runtime handle gets
its own `CultMeshQuicNativeHandle` alias (`bigint`) so its type is named
rather than smuggled through as `unknown`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:43:35 +02:00
Meta Vens
148bd69c06 Give the raw promisified nextEvent its own 6-argument type
The wrapper cast the raw promisified koffi call to the same 4-argument public
NativeBindings#nextEventAsync type it was implementing, then called it with
six arguments (the two _Out_ placeholders included) — a type checked itself
into a contradiction tsc caught on a clean build. The raw call gets its own
type matching what koffi actually expects; the public wrapper still exposes
four.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:39:52 +02:00
Meta Vens
92dc3a74d6 Fix three real bugs the second ygg-verify pass found
- koffi's `_Out_` sugar still counts toward the call's declared arity in this
  version (an arity check rejected the shorter call); pass `null` at each such
  slot, since the value comes back through the named field on the result
  object regardless of what was passed.
- The negative-grep test read `dist-test/src/...`, which does not exist:
  `__dirname` at runtime is `dist-test/test`, and only compiled output is
  mirrored there, so the original source is two levels up in `src/`, not one.
- Two generated Odin test roots shared one hardcoded key id
  ("odin-root-1"), so trust negative (c) had the trust policy's root looked
  up successfully under the signer's own id and failed on signature mismatch
  instead of "not trusted" — proving the wrong rule. Each generated root now
  gets a distinct key id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:38:53 +02:00
Meta Vens
a44505fab8 Fix the koffi type friction found by the first ygg-verify pass
koffi's own .d.ts resolves as an ECMAScript module under Node16 resolution,
which a CommonJS `typeof import("koffi")` reference and a `koffi.promisify`
property access (which does not exist) both choke on. Type the loaded module
as `any` and promisify `nextEvent.async` with `node:util` directly instead of
guessing at a koffi-native promise helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:37:10 +02:00
Meta Vens
67d49b567d Give the Linux QUIC dev image Node 24 and the .NET 10 SDK
Cut 4's own npm tests and the C#-to-TS interop lane need to run in the same
container that builds the native bridge, and the image had neither runtime.
Smallest change: install Node 24 from NodeSource and the .NET 10 SDK from
dotnet-install.sh, both pinned by channel/major version the way the rest of
this image pins its toolchain by digest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:33:12 +02:00
Meta Vens
7e08c5cfd2 Interop: C# managed QUIC provider serving a TypeScript native consumer
tests/GameCult.Networking.InteropPeer gains mode quic-realtime-serve
(--port, --frames, --delivery, --interval-ms): creates a self-signed
certificate the same way CultMeshQuicRealtimeTransportTests.cs does, starts
CultMeshQuicRealtimeServer, and prints one ready line carrying the advertised
cultmesh-state+quic:// endpoint with its cert-sha256 pin, then broadcasts N
frames with increasing sequence, exiting after the loop or on stdin close.
GameCult.Networking.InteropPeer.csproj references GameCult.Mesh.Quic.

Discrepancy, kept: the cut map's worked line for the ready signal is
`{ endpoint: "..." }` alone; this repo's spawnServeProcess harness
(cultnet-interop.test.ts:2491) waits for a line whose parsed `status` is
"ready", the same convention every other serve mode here already uses. The
printed line is `{ status: "ready", endpoint: "..." }` — the map's shape plus
the field its own named mechanism requires — rather than inventing a second
readiness convention for one peer.

packages/cultmesh-ts/test/interop/cultmesh-quic-peer.ts: the TypeScript half,
`dial --endpoint --expect N`, connecting with CultMeshQuicRealtimeConnector
and printing the received frames as one JSON line (fields plus payload hex).
It reaches the connector through this package's own src/, never importing
cultmesh-ts itself, keeping the dependency direction the map states
(cultmesh-ts depends on cultnet-ts, never the reverse).

packages/cultnet-ts/test/interop/cultnet-interop.test.ts gains
"CultMesh QUIC realtime: C# managed provider and TypeScript consumer",
spawning the C# peer with spawnServeProcess and the TypeScript peer with
runJsonCommand, both established patterns in this file. Skips when
CULTMESH_QUIC_NATIVE_DIR is unset, the same convention the unit tests use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:33:02 +02:00
Meta Vens
39a8a5f45e Test the Node QUIC binding, consumer and the five trust negatives
packages/cultmesh-ts/test/realtime-quic-native.test.ts: the FFI smoke against
CultMeshQuicNativeRuntime (open/close, a listener with a real PKCS12
credential, a full loopback connection delivering one raw frame end to end),
plus the negative grep that .async( appears only on nextEvent and no
koffi.register exists.

packages/cultmesh-ts/test/realtime-quic-consumer.test.ts: the TypeScript
consumer against a raw listener opened through the native runtime (connect
with the correct pin; refuse a wrong pin; refuse a missing pin with no
validator; a caller validator observes the target and the DER bytes; a late
older generation is dropped), mirroring
CultMeshNativeQuicRealtimeTransportTests.cs and
CultMeshQuicRealtimeTransportTests.cs. Then the five trust negatives through
CultMeshQuicRealtimeSessionManager with a static lookup source and routes
signed with node:crypto (P-256, IEEE P1363 via dsaEncoding): (a) expired
certificate, (b) unsigned remote route, (c) untrusted Odin root, (d) a route
that verifies but pins the wrong TLS certificate, refused by the connector
before isVerifiedFor is ever asked, and (e) a stale/replayed generation,
proven with a stub connector whose transport fails isVerifiedFor, asserting
the session manager disposes it and rejects the connect. There is no
session_open nonce on the QUIC plane (route generation plus the TLS handshake
is its freshness), so (e) is this shape rather than a literal nonce replay,
as the cut map records.

test/fixtures/quic-test.p12 and quic-test-cert.der: a self-signed ECDSA P-256
PKCS12 test credential (OpenSSL, empty password, 1088 bytes — the same shape
as the native bridge's own embedded test PKCS12) and its DER certificate, used
to open real raw listeners and compute the expected cert-sha256 pin at test
time rather than hand-transcribing it.

Discrepancy kept, reported: the C# reference's receive-side inbox also
coalesces a backlog of not-yet-drained latest-only frames of the same key to
just the newest; this transport applies only the generation-drop filter the
cut map names (":538-560") and queues every frame that passes it. The interop
lane's frames are far enough apart that this never matters in practice, but a
consumer that fell behind would see more history here than the C# reference
delivers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:32:45 +02:00
Meta Vens
6e5ba63904 Add the Node QUIC realtime consumer, connector and session manager
packages/cultmesh-ts/src/realtime-quic.ts adds, over the native binding:

- parseQuicRealtimeEndpoint, mirroring
  CultMeshQuicRealtimeTransportConnector.TryParseEndpoint (:98-109).
- CultMeshQuicRealtimeTransport: sendFrame/receiveFrame/dispose/isVerifiedFor,
  the stream-kind-vs-delivery consistency check (:519-524) and the
  per-(channel,body) generation filter that drops older (producerEpoch,
  sequence) (:538-560), both pinned against CultMeshQuicRealtimeTransport.cs.
  Reliable sends reuse one persistent outbound stream behind a gate;
  latest-only opens a fresh stream per send; unreliable fails closed. The
  bridge owns wire framing both ways, so this file never length-prefixes
  anything by hand.
- CultMeshQuicRealtimeConnector: the certificate decision on event 3 (a
  caller validator, else SHA-256-of-DER against the endpoint's cert-sha256,
  else reject), mirroring MatchesAdvertisedCertificatePin (:111-119).
- CultMeshQuicRealtimeSessionManager and CultMeshStaticRealtimeLookupSource:
  binds candidates from authorityRoutes carrying cultmesh.realtime_state.v1,
  verifies each with cultnet-ts's verifyAuthorityRoute before any connector is
  asked, orders by connector then route priority, races at most
  maxRacedCandidates per tier, requires isVerifiedFor after connect and
  disposes a transport that fails it — the realtime path of
  CultMeshSessions.cs's ConnectRealtimeAsync (:833-896), including its
  session key verseId + "\u001f" + authorityRuntimeId (:46).

No Odin WebSocket rendezvous lives here (Q7, ruled A): only the
ICultMeshRealtimeLookupSource port and a static source.

index.ts re-exports the module and gains three CultMesh statics beside the
RUDP ones: parseQuicRealtimeEndpoint, createQuicRealtimeConnector,
createQuicRealtimeSessionManager.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:32:27 +02:00
Meta Vens
c5e49290c7 Bind the CultMesh QUIC native ABI v2 from Node with koffi
Adds packages/cultmesh-ts/src/realtime-quic-native.ts: a lazy koffi binding
over the twelve v2 queue exports plus cultmesh_quic_last_status
(native/GameCult.Mesh.Quic.Native/include/cultmesh_quic_native.h). koffi is
required only inside loadBindings(), so a consumer that never touches QUIC
loads no native module. CultMeshQuicNativeRuntime is a process-wide,
reference-counted singleton running one event pump (a loop awaiting
nextEvent.async with a 250ms timeout, no koffi callbacks) that dispatches
decoded events to registered listener/connection handlers by id.

Also: add the koffi 3.3.0 dependency to cultmesh-ts's package.json, and switch
its tsconfig from classic Node resolution to Node16 so `cultnet-ts/contracts`
(a package.json `exports` subpath) actually resolves — a gap Cut 1 recorded
and left for this cut ("until Cut 4 changes it").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:32:08 +02:00
Meta Vens
dbee044294 Merge CultNet selection Cut 1
Four Soul gates. The first three refused it, each on something real: the row
identity ruling implemented as two different rules that answered opposite ways
on the shape it was written for; both runtimes faulting on an attacker-supplied
cursor through mirror-image overflows; a parity promise neither built nor
listed as outstanding; and a guard deleted on my ruling that took a legal
document shape into an untyped exception.

What lands is one rule for reference resolution in both runtimes, derived from
the wire rather than from preference, with the shared-key vector that never
existed. Malformed cursors reach the wire typed. Every comparator component
that decides output dies under mutation in both runtimes, and the one that
could not be distinguished by any input was deleted rather than recorded as
equivalent. The snapshot_response_raw.v1 envelope and its nested record types
are byte-identical across runtimes at populated and absent optionals, asserted
independently on each side.

The claim is exactly that, and no larger: byte parity for those types and
semantic parity over the named field subset. Whole-message assembly through a
single rows-to-bytes path is its own cut.

Follow-ups stay open and recorded: the C# Stryker survivor list untriaged,
contracts.rs and schema_discovery.rs unmeasured, the --since crash unrooted,
and three Caching tests that need a non-root container to prove and must not
be made to pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:22:25 +02:00
Meta Vens
63a677bea9 Correct one comment still naming the deleted To.SchemaId tiebreak
Soul found this while confirming batch 8. The Survivor-5 header in the same
file was updated when the component was deleted and this one was not, so it
described a comparator that no longer exists.

Self edited this directly rather than dispatching Hands for a single stale
sentence. Naming that because Self editing code is a boundary worth saying
out loud when it is crossed, even for a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:21:52 +02:00
Meta Vens
97d7589afc Record batch 8: the Edge vector mirrored, the dead tiebreak deleted
Both runtimes now assert all four cells of the wire vector, and the new C#
constant fails when a single byte is flipped, so it is not decoration.

R-BA came back as a deletion with a better argument than my ruling carried.
Both resolvers are pure functions of the citer's schema, the role and the
record key against a fixed candidate index, so once those tie in the
comparator the resolved row including its schema is forced identical. That is
structural rather than semantic: it does not rest on schema-authoring
discipline, which is what my version rested on and what has misled this cut
twice.

All four pre-merge items are closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:15:55 +02:00
Meta Vens
492dd0e754 R-BA: delete the unreachable to.schemaId tiebreak from both edge comparators
Soul could not construct a selection where To.SchemaId decides the
EdgesFor/page_edges order once From, Role and To.RecordKey already
tie, but flagged that as a claim, not a proof - the same shape of
argument that was wrong twice before on this comparator (R-AT).

Traced instead of asserted: TryResolveReferenceTarget (C#) and
resolve_reference_target (Rust) resolve a reference purely from the
citer's schema, the role, and the record key, against the evaluation's
own fixed candidate index (byKey/by_key) - no edge- or caller-specific
input steers which candidate wins. Once From and Role tie (pinning the
citer's schema and the declared target type/leaves) and To.RecordKey
also ties, the resolved row - schema included - is forced identical by
construction, in both runtimes, every time. No mutation can distinguish
it because no input can, so it was deleted rather than recorded as
"equivalent": a component nothing can distinguish is a line every
future reader must reason about for no behavioural gain.

Deletes the To.Descriptor.SchemaId/to.schema_id() ThenBy/then_with from
CultNetSelectionEvaluator.EdgesFor and selection.rs's page_edges sort,
and updates the comments (in-code and the Survivor 5 test block) that
described the old five-key chain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 09:08:55 +02:00
Meta Vens
cda28fa2ae R-AZ: mirror the all-absent Edge vector into the C# parity test
The 246-byte all-absent Edge vector (SM-10) existed only on the Rust
side (selection_wire_parity.rs's CSHARP_EDGE_HEX). The all-absent
header gets incidental cover because it is nested inside the 376-byte
envelope test; the edge has no such cover, so a C# edge encoder that
resumed omitting a null payload/payloadEncoding would still pass every
other test here - exactly the asymmetry R-AP existed to kill.

Adds EdgeAllAbsentMatchesTheConstantCommittedIntoTheRustTest, asserting
the same constant against MessagePackSerializer.Serialize. Flipping one
hex digit of RustEdgeAllAbsentHex fails the new test only, confirming
it is not vacuous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 09:08:42 +02:00
Meta Vens
ed39263078 Strike R-AC and its paragraph where they were written, not beside them
Soul found the false equivalence argument still standing at its original site
after I deleted the copy batch 6 had restated. Both read as live instructions
and were obeyed for two passes: nobody chased those mutants because the map
told them not to.

Struck rather than edited, so the record shows what was believed and for how
long. The closing line of R-AC survives on its own merits: a survivor
correctly identified does beat a test written to move a number, which is
precisely why identifying one takes a construction attempt rather than an
argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:03:31 +02:00
Meta Vens
9e9269d4f8 Record the fourth gate: it opens, and four items close before the merge
Every comparator component that decides output now dies under mutation in both
runtimes, and the new fixtures are killers rather than coincidences. The
headline claim is two-sided: each runtime serializes and asserts
independently, and every C# vector fails when a single hex digit of its
expected constant is flipped.

Soul found one real hole. The all-absent Edge vector exists only in Rust,
because the all-absent header is covered incidentally by the envelope and the
edge is not. A C# encoder that resumed omitting a null payload would pass
everything. Five lines closes it, so it closes before the merge rather than
after: shipping over a known asymmetry with a note promising to return is the
thing this cut exists to prevent.

Two misses are mine. R-AT asserted to.schemaId dies alone, which Hands never
claimed and which is not true; it is now construct a killer or delete the
component, because a line no input can distinguish still has to be reasoned
about. And I deleted the false equivalence argument where batch 6 restated it
while leaving the original standing as a live instruction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:02:16 +02:00
Meta Vens
f6d7f203d6 Record batch 7: every merge condition met, and a third coincidence found
All three surviving comparator components now die in both runtimes, the claim
finally has the home the map said it had, and the whole-message vector is
green at 376 bytes. The thin evidence is no longer thin: a populated tags
array and a populated payload both come out byte-identical, which was the
named risk and did not materialise.

Hands also found that Soul's counterexample, taken verbatim, does not kill the
mutation it was built for. It survives in both runtimes because select's push
order happens to match alphabetical sort for that naming, and the two runtimes
push in opposite orders. Not an unreachable branch: a coincidence, the third
of these in three days, and this one was caught while executing rather than by
a later pass.

My byte counts in the landing note were wrong, 86 and 99 against the real 228
and 246. The committed constants matched the real bytes all along; only the
prose beside them was wrong, which is the same shape as the two corrections
before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 08:45:06 +02:00
Meta Vens
35a450e32f R-AX: land the whole-message parity vector, and extend it past all-absent
Soul had this green in the rig at 376 bytes and called it "blocked on
unbuilt page-assembly ownership" - the wrong thing: only the two
*evaluators* producing the same page from the same rows needed that move
(a separate claim, R-AR); the two *encoders* producing identical bytes for
the same page value never did. Land the whole
cultnet.snapshot_response_raw.v1 envelope, byte-identical between Rust's
encode_cultnet_message_to_vec and C#'s CultNetSnapshotResponseRawV1Message,
on both runtimes.

The existing header/edge vectors also proved nothing about tags
(string[] vs Vec<String>) or payload (byte[] vs serde_bytes): both were
absent at every value pinned. Add a header with tags populated and an
edge with a payload populated, captured the same way against a real
MessagePack C# 3.1.7 run on Yggdrasil. Neither diverged - the two
mappings agree byte-for-byte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:40:52 +02:00
Meta Vens
edfe9db59d R-AV: give the typed-selection parity claim a home in runtime-parity-scope.md
Section 9 of docs/cultnet-selection-cut.md pointed here for the claim;
this file never mentioned typed selection at all. Add the row, in the
words from fix batch 7's R-AY ruling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:40:31 +02:00
Meta Vens
d784dbca7e D10b: note the breaking registration change in the error message
A shape that used to register and work - a data member aliased Owner
beside a reference member named Owner - is now refused by D10b (R-AL).
Soul's merge-gate third pass flagged this as a breaking change with no
note; the error message and its owning comment now say so.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:39:51 +02:00
Meta Vens
22d0722d5a R-AT: fix the C# self-citer fixture's naming so it actually defeats push-order luck
The zebra/alpha pairing that kills Rust's drop-both-to-components mutant
does not kill it in C#: EvaluateAll concatenates citesEdges before
citedEdges (the opposite of cultnet_rs's single cited-then-cites Vec), so
that pairing's push order already matched the correct sort order here too
- just for the opposite reason. Swap to alpha (self) / zebra (target),
the pairing that actually disagrees with this runtime's own push order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:38:27 +02:00
Meta Vens
0bdcf6fd55 R-AT: add missing MessagePack using the C# self-citer fixture needs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:35:37 +02:00
Meta Vens
c9ba53cfe1 R-AT: pin the edge comparator's tied-from-and-role survivor in C#
The same live mutant Soul found in Rust (dropping to.schemaId/to.recordKey
from the edge comparator) survives in C# too - both runtimes share the
comparator's shape, not a rule proving it. Land the C# mirror of Soul's
self-citing-peer counterexample from the rig, renamed so the self edge's
push order and the correct sort order actually disagree (the rig's own
"hub"/"other" naming left the drop-both mutation surviving by coincidence
of push order matching alphabetical order - see the Rust commit on this
branch for the construction).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:34:44 +02:00
Meta Vens
7581f38db1 R-AT: pin the row and edge comparators' recordKey/to survivors in Rust
Soul's merge-gate third pass (SM-7) found three live mutants: dropping
to.schemaId/to.recordKey from the edge comparator (both runtimes) and
dropping recordKey from Rust's row comparator (named a pass ago, never
touched). Hands' earlier argument that no selection ties from+role while
leaving to free to vary was false - a selection carrying both cites and
cited mixes their edges into one bucket, and a self-citing peer produces
exactly that tie. Take the counterexample from Soul's rig rather than
rebuilding it, and add the missing row-comparator recordKey tiebreak test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:32:06 +02:00
Meta Vens
22067888e1 Record the third merge gate, and delete a false argument rather than amend it
Soul built the counterexample. The claim that no construction gives two edges
with tied From and Role and differing To reasoned about each hop separately,
and a selection carrying both hops mixes their edges into one bucket. A row
that self-references through the same role it uses to cite a peer produces
exactly that pair. It runs in both runtimes. Three comparator survivors the
last gate named are still alive, and batch 6 added no Rust row-order test at
all.

Two of my own words were wrong and both are corrected here. Nested wire-type
byte parity overstated evidence gathered at a single all-optionals-absent
value. And calling whole-message parity blocked on an ownership move ran two
independent things together: the encoders agreeing on bytes needs no ownership
move, and Soul wrote it this afternoon at 376 bytes identical. Calling it
blocked made the cheapest available evidence look expensive, which is how it
slipped six passes while sitting there for the taking.

The claim also had no home. The file section 9 names as carrying it contains
no mention of typed selection and never did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 08:14:52 +02:00
Meta Vens
811e53895b Record batch 6, and the verification gap that was mine
The brief named ygg-verify.sh without its path where every earlier brief gave
it in full. Hands could not find the script, verified the whole batch on the
workstation instead, said so plainly rather than claiming the bar was met, and
left the branch unpushed so nothing looked merge-ready. This batch still owes
its Yggdrasil run.

The regression is backed out and the real fix went to the owner: registration
now refuses a data member whose bare name collides with a reference member's
alias, which is the collision D10 never checked. Writing R-AM's test found a
second defect, the backstop logging DynamicInvoke's wrapper instead of the
fault it was meant to surface.

R-AP's encoding turned out to be C#'s existing behaviour, with the whole gap
on the Rust side omitting keys for absent optionals. Two nested types are now
byte-identical against real C# hex. The half that would move page assembly
into the evaluator was not built, and Hands stopped and explained why rather
than reporting the ruling closed. That half is its own cut, because it changes
two public signatures, every caller and the harness.

So the cut's honest claim is nested wire-type byte parity plus semantic parity
over the named subset. Whether that is enough to merge is the operator's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 07:39:52 +02:00
Meta Vens
94707ed8ed R-AP: nil-not-omitted for Edge/RawDocumentHeader/SelectionDocumentRecord/SelectionPage
Decided the payload shape: a map with camelCase keys, fields in declared
order, every optional field always present and nil when absent - never
omitted. C#'s [MessagePackObject]/[Key("name")] map formatter already does
this by default (confirmed byte-for-byte below); Rust's `to_vec_named`
encoding for these four types used `skip_serializing_if = "Option::is_none"`,
which omits the key instead - the gap the map cited ("C# writes nine fields
with nils, Rust omits absent ones").

Removes skip_serializing_if from Edge, RawDocumentHeader,
SelectionDocumentRecord and SelectionPage, keeping #[serde(default)] for
decode-side robustness. Selection (the request type) is untouched - out of
this ruling's scope, and the map already confirmed the request side agrees
between runtimes.

Adds tests/selection_wire_parity.rs: RawDocumentHeader and Edge, each
carrying several absent optionals, encoded via to_vec_named and compared
byte-for-byte against hex captured from a real MessagePack C# 3.1.7 run
(same pattern as error_message.rs's csharp_hex vectors) - both now agree
exactly.

What is NOT done, and why: R-AP also called for "Select gains projection,
so the thing the vectors test is the thing that decides." In C#, projection
(header vs document, ToRawRecord/ToEdge/FromRecord) is owned entirely by
CultNetDocumentRegistry.SelectPage/SelectAll (GameCult.Networking), not by
CultNetSelectionEvaluator.Select/Page - the evaluator returns raw Rows/
EdgeMatches and knows nothing about a document's payload serializer, which
only the registry's bindings own. Moving page assembly into the evaluator
without threading a caller-supplied serializer (the same pattern ordinalOf/
shardIdOf already use) would give a schema-generic evaluator per-binding
wire knowledge it should not own; threading a serializer callback in is
buildable but changes SelectPage/SelectAll's own signature and every
caller, plus the parity-vector harness's construction - a materially larger
change than this pass's remaining scope. No vector yet exercises the real
CultNetSnapshotResponseRawV1Message/select_page output byte-for-byte; only
the two nested wire types in isolation. Flagged for the operator per the
brief's own stop condition.

R-AQ, updated: type-level byte parity for the nested wire shapes (Edge,
RawDocumentHeader) is now real and vector-tested. Whole-message byte parity
for cultnet.snapshot_response_raw.v1 is still not - the smaller claim (this
cut's page bytes agree only where R-AH's fixed-shape rule reaches, and
projection ownership is still split between the evaluator and the registry)
is the true one until "Select gains projection" actually lands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:33:02 +02:00
Meta Vens
62ebba4ab5 R-AO: shared vector for two-in-target shared-key resolution
Adds citer-both-in-target to the shared fixture, the first row in file (and
row-order) to cite "dup-key" through Design - both leaf_a and leaf_b at that
key are in Design's declared target, unlike citer_narrow's narrow_ref above
it (only ever one in-target candidate). Adds the matching vector case to
both runtimes' case lists and regenerates both written vector files
(CULTNET_WRITE_VECTORS=1), so cross-runtime agreement on this shape is
pinned by the parity check, not just asserted per-runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:18:11 +02:00
Meta Vens
4323e2280a R-AN/R-AO: Rust edge-order and shared-key regression tests
R-AN: the existing edge-order test ties From and To across its two edges,
so it only pins from.record_key as sole discriminator - dropping role
entirely still survives (Soul's SM-4). Two new tests isolate the other two
components of EdgesFor's comparator: role (same citer, same target, two
roles - Citation.role: None matches any reference) and from.schema_id (two
different-schema citers sharing one record key and role). Both confirmed to
kill their own dropped-clause mutation by hand (reverted after).

R-AO: shared_key_with_two_in_target_candidates_resolves_to_whichever_the_-
caller_listed_first pins "first row in the caller's order wins" - R-AF's
existing vector only covered one-in/one-out. Confirmed to kill a
last-wins mutation by hand (reverted after). Also extends the shared vector
case list with shared_key_design_resolves_the_first_in_target_candidate_-
in_row_order (paired with the fixture/vector commit).

To.SchemaId/To.Key were re-checked per Self's ruling: within any one
page_position bucket, `cites` always ties every edge's To to the query's
own single target, and `cited` always ties it to the anchor row's own
identity - the only way for To to vary at a tied page_position is a mixed
cites+cited hop on one row, which always ties or untangles via From first.
No construction was found where To is the sole discriminator; the earlier
"equivalent within one hop direction" claim holds. No code change, no
artificial test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:17:59 +02:00
Meta Vens
f01c0b6112 R-AL/R-AM/R-AO: C# regression tests for the fix batch
- R-AL: DataMemberNameCollidingWithAReferenceMembersAliasIsRefusedAtRegistration
  pins D10b's registration refusal. ReferenceMembersExcludesADataMember...
  pins the restored evaluator guard directly (hand-built descriptor via
  reflection, since D10b now closes the registry door this shape used to
  slip through).
- R-AM: CultNetDatabaseServer_SnapshotV1Handler_CatchesAnUntypedFault... and
  RudpCultNetSchemaServer_DispatchBackstop_LogsEveryUntypedHandlerFault pin
  the handler's catch and the backstop's logging, each against a real
  untyped fault.
- R-AO: Evaluator_CitedWithTwoInTargetCandidates_ResolvesToWhicheverThe-
  CallerListedFirst pins "first row in the caller's order wins" when two
  in-target rows share a key, in both row orders.

Every test in this commit was checked against a hand-reverted mutation of
its own production fix and confirmed to fail before being confirmed to pass
against the real fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:17:21 +02:00
Meta Vens
77cd9649ec R-AM: snapshot handler and dispatch backstop answer/log untyped faults
HandleSnapshotRequestV1Async only caught the three typed selection
exceptions, unlike its siblings HandleShardLogRequestAsync/HandlePutAsync,
which catch Exception and answer a CultNetErrorMessage. An untyped fault
(e.g. a null Selection) escaped into the RUDP dispatch backstop, which
swallowed it with no log - the same backstop that hid SM-6's
ArgumentNullException regression for a whole batch.

- HandleSnapshotRequestV1Async now catches Exception like its siblings and
  answers a CultNetErrorMessage.
- RudpCultNetSchemaServer gets a Logger (ILogger/NullLogger, same pattern as
  Server/Client), and DispatchAsync's backstop always logs the fault it
  swallows, unwrapping DynamicInvoke's TargetInvocationException wrapper so
  the logged message is the handler's real exception, not the generic
  invocation-wrapper text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:15:43 +02:00
Meta Vens
375b6177ae R-AL: restore ReferenceMembers' IsReference guard and fix D10's blind spot
CultNetSelectionEvaluator.ReferenceMembers dropped the `!member.IsReference`
guard on the premise that a data member's role string could never coincide
with a reference member's alias - false. CultCache's D10 only rejects a
collision between two *aliased* members; an unaliased data member's bare
name was never checked against another member's alias. A data member named
`Owner` plus a reference member aliased `Owner` registered fine, then faulted
every hop through it: ReferenceMembers yielded the data member, ReferencesOf
matched it to the reference member's edges by role string, and
`member.TargetType!` was null.

Two independent fixes, both regression-tested:
- Restore the guard, delete the false comment. Pinned directly by hand-
  building the colliding descriptor via reflection (bypassing the registry,
  since D10b below now refuses that shape at the only production door).
- D10b: registration now refuses a plain member whose bare name collides
  with a reference member's alias in the reference-role namespace
  (IndexAlias ?? MemberName), not just alias-vs-alias.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:15:33 +02:00
Meta Vens
69d736def5 Record the second merge gate: my redundancy ruling shipped a regression
R-AK was mine. I called a guard duplicate authority and ordered it deleted;
Hands verified, got it wrong, and left a comment claiming an alias uniqueness
property that the source contradicts. A legal document shape now takes every
hop path into an untyped exception, the peer gets no typed refusal, and the
batch's own new dispatch backstop swallows it without a log. Restoring the
guard leaves the suite green, which is the other half of the problem.

That is the second redundancy ruling of mine to be wrong. The first was caught
by Hands; this one was not. A deletion argued from a property has to be probed
at the shape that would break it.

R-AF is genuinely settled and Soul confirmed it harder than the probe did: the
reference type has nowhere to put a schema id, so no formatter or version can
introduce one. Both runtimes now agree on every shape that split them,
including the role-filter reordering no ruling covered.

R-AH has slipped five passes and cannot be built as I ruled it, because the
ruling never fixed map encoding, field order, or whether an absent optional is
nil or omitted, and because C# has no page type at the layer the vectors
test. This batch decides all four. Until it lands, the map now claims semantic
parity over a named field subset rather than byte parity, because byte parity
is not true today and the smaller honest claim is worth more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 06:20:15 +02:00
Meta Vens
a621566f38 Record R-AI, and that concurrent mutation verdicts here are not reproducible
canonical_number went from 29 missed to 4, all four hand-traced as equivalent
rather than assumed. is_canonical, the Q-J door whose every branch mutation
was surviving, had never been called directly by any test; it is now.

The finding that outlives the batch is infrastructural. The same commit gave
different missed and timeout sets across three -j4 runs, while every -j1 run
reproduced the same result, most likely the shared cargo target directory
under concurrent mutant builds. That makes every concurrent mutation figure in
this campaign provisional, Stryker's --concurrency 4 runs included. Killed
mutants stay killed; survivor lists from concurrent runs cannot be trusted as
complete, so the rulings they informed rest on the hand-confirmed evidence
beside them rather than on the counts.

A third test in two days passed by coincidence: render_f32's sign-bit mutant
survived because 1.0 and 2.0 happen to have bit 0 matching their sign bit.

contracts.rs and schema_discovery.rs remain unmeasured. A partial run showed
real misses across the validate family before it was killed; that signal is
recorded so the next pass starts from it. Queued rather than run, because the
merge gate is a Soul pass on batch 5, not a sweep of an adjacent file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 05:21:13 +02:00
Meta Vens
fc51e9a408 R-AI: pin render_f64's sign-bit extraction on a negative value too
Same gap as render_f32's sign-bit test (previous commit): render_f64's
sign extraction has the identical shape and the identical coincidence
(-1.0_f64/-2.0_f64's bit 0 happens to be 0, matching their real sign
bit's positive-looking value under a wrong-direction shift).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 05:18:49 +02:00
Meta Vens
82895ec866 R-AI: pin render_f32's sign-bit extraction on a negative value
Every existing render_f32 test (including the two added in the previous
commit) used a positive value, so a mutant that shifts the sign-bit read
the wrong direction (bits << 31 instead of bits >> 31, reading bit 0
instead of bit 31) went unnoticed: 1.0_f32 and 2.0_f32 both happen to
have bit 0 equal to 0, same as their real (positive) sign bit, so the
mutant computed the same "not negative" answer by coincidence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 05:18:49 +02:00
Meta Vens
a24beda0e3 R-AI: pin render_f32's subnormal branch and implicit leading bit directly
The first canonical_number mutation pass (previous commit) closed every
survivor except four in render_f32 (biased_exponent == 0 flipped to !=,
the subnormal exponent's -149 losing its sign, and the normal path's
implicit-bit | flipped to ^/&). The existing round-trip tests parse the
rendered string back through f32::parse and compare bits, which can
still agree with a wrong bit-extraction when the wrong value happens to
parse back to the bits it was carelessly derived from; none of them
assert on the rendered string's shape or magnitude directly.

Add a subnormal-shape test (2^-149 is "0." plus 149 fraction digits, not
an astronomical integer and not one exponent off) and a normal-path test
where the fraction bits are identical to zero's (1.0 and 2.0) so only
the implicit leading bit the | folds in carries the value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 05:18:49 +02:00
Meta Vens
68af08a5b2 R-AI: pin canonical_number's grammar and bignum helpers directly
cargo-mutants over selection.rs found every branch mutation in
is_canonical surviving (the Q-J door was reachable only through the
handful of spellings validation_refuses_non_canonical_number_spellings
happened to cover), plus decompose/pad_right/canonicalize_decimal_digits/
big_mul_small/big_to_most_significant_first each replaceable with a
trivial stub while the suite stayed green.

Add tests that call is_canonical, decompose, pad_right, compare,
canonicalize_decimal_digits, big_mul_small and big_to_most_significant_first
directly (the nested test module already reaches private helpers via
super::), pinning exact output at the inputs that exercise each branch,
comparison and carry step named in the mutant list rather than only the
few spellings the field-validation door happened to already cover.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 05:18:49 +02:00
Meta Vens
6773b8e437 Record batch 5: the wire settled S-1, and a degenerate test that passed by luck
R-AF was decided by dumping real bytes rather than by preferring an
implementation. A reference serializes as a bare msgpack string with no schema
id anywhere, so C#'s rule was right and Rust adopted it. The shared-key vector
that never existed now covers both directions, including that row order does
not change the answer.

Two findings came out of the work itself. Rust resolved and target-checked
every declared reference before filtering by the requested role, where C#
filters first: a live divergence no ruling had named. And the existing
row-tiebreak test was degenerate, passing with the tiebreak reverted and
swapped, purely because of which real schema hash happened to sort first. It
now derives its keys from the ids discovered at runtime.

Both cursor overflows are fixed, each runtime fed the other's proven input,
with a backstop catch so nothing untyped reaches the poll loop.

Two things are owed rather than done. The diff-scoped Stryker run failed on a
tooling bug that was worked around instead of root-caused, so 47 untriaged
survivors are inherited. The dispatch backstop has no integration test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 04:55:00 +02:00
Meta Vens
3c4f83b038 R-AG/R-AK: checked C# cursor-length arithmetic, a dispatch backstop, and the redundant guard
R-AG. ReadLengthPrefixedFields tested pos + length > bytes.Length in int
math; a length prefix near int.MaxValue overflowed the unchecked add,
slipped past the guard, and Encoding.UTF8.GetString threw
ArgumentOutOfRangeException instead of this method returning null the
way every other malformed shape already does. Promotes to long before
the add, which cannot itself overflow (both operands stay far under
long's range), so the guard sees the true sum.

CultNetRudpSchemaServer.DispatchAsync gains a catch around the handler
invocation: HandleSnapshotRequestV1Async already answers
cursor_invalid/selection_invalid/reference_outside_target for its three
typed exceptions, but nothing stopped an untyped one from an
unanticipated bug reaching DynamicInvoke's caller and stalling the poll
loop for every other peer. Answers nothing on the wire (an exception
this generic carries no typed refusal to report) and drops the one
malformed dispatch, the same as an unregistered handler already does.

Hostile-cursor tests (CultNetSelectionFixBatchTests.cs) feed
CultNetSelectionCursor.Parse a length prefix near int.MaxValue (C#'s own
proven overflow), near usize::MAX (Rust's proven overflow, fed here too
per the ruling), wider than any integer type, and negative-looking -
each asserted to throw the typed CultNetSelectionCursorException.

R-AK. Deletes ReferenceMembers's `!member.IsReference` guard.
CultCache.ReferencesOf (CultCache.cs:262-278) already searches only its
own reference members for a role match and returns empty for anything
else; index aliases are unique per descriptor, so a data member's own
role string can never coincide with a reference member's. Verified
before deleting: traced ReferencesOf's role lookup directly rather than
trusting the ruling's premise. A second copy of the same exclusion here
was duplicate authority, not a second decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:44:09 +02:00
Meta Vens
dd08be4f32 R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests
R-AF: FixtureRow's references (and every fixture/hand-built row that
constructs them) carry a bare target key now, matching Row::references's
new contract. Adds citer_narrow() (the shared fixture's real schema id,
distinct from this file's own local NARROW_CITER) to the fixture
machinery, and two direct behavioural tests
(shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order,
shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target)
pinning both shapes from Soul's table at the select() level, plus a
shared-fixture vector case for the accept shape and an
evaluation_refusal_cases entry (expected_evaluation_refusal, read by the
matching C# addition) for the refuse shape.

Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target
(renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role):
its original shape queried one role while expecting refusal from an
edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15)
test gives (same role, two elements) - found because it broke once
matches_citation's role-filter order was fixed to match C#. The new
version mirrors C#'s actual test shape.

R-AG: hostile cursor tests feeding Cursor::parse a length prefix near
usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven
overflow, fed here too per the ruling - neither side's tests are
evidence about the other), a prefix wider than any integer type, and a
negative-looking prefix - all under std::panic::catch_unwind, asserting
a typed CursorInvalid and never a panic.

R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor
(a both-hops selection with a hub/other citer pair whose keys and roles
are deliberately anti-correlated, so a Role-first mutant and the real
rule disagree) and
row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal
(the same anti-correlation trick applied to order_rows, computed against
the real leaf_a/leaf_b ids at runtime).

FixtureRow gains #[derive(Debug)] - needed for an assert message on the
new evaluation-refusal-case writer, harmless elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
Meta Vens
b9db8702d2 R-AF/R-AJ: C# parity vectors and the ordering-composition tests they needed
R-AF: registers ParityCiterNarrow (citer_narrow's C# type, [CultIndex
"narrow_ref"] so its role matches the fixture/Rust spelling), adds
Keys-scoped vectors pinning both shapes from Soul's table - a shared key
where the only candidate is out of target refuses evaluation-time
(expectedEvaluationRefusal, a narrow addition to the vector schema
distinct from the door-only expectedRefusalField - not R-Z/R-AH's
page-bytes harness), and one where an in-target row shares a key with an
out-of-target row and must still resolve.

Landing these exposed a real, pre-existing cross-runtime scope
difference in R-W unrelated to R-AF: Rust checked every declared
reference on a row regardless of the citation's requested role;
C#'s ReferenceMembers(descriptor, citation.Role) narrows by role first.
Fixed on the Rust side (previous commit); hop_by_role_cites_exact_id and
cites_target_by_name_alias keep an explicit Keys scope here anyway as
belt-and-suspenders.

Also corrects S-12's last stale comment: cites_target_by_name_alias's
"Confirmed cross-runtime defect" note was already false - the alias port
fixed it - and still called it confirmed.

R-AJ: CultNetSelectionSurvivorTests.cs's row-tiebreak fixture
(Evaluator_BreaksAnOrdinalTieBySchemaIdThenRecordKeyInBothDirections) was
degenerate - probed by hand-reverting the source and swapping the
tiebreak's two ThenBy calls, it still passed, because whether the old
3-row fixture discriminated the swap depended on which of SelLeafA's/
SelLeafB's real schema ids happened to sort first, and on this repo's
ids it didn't. Rewritten to assign row keys relative to the schema ids
discovered at runtime (low-sorting schema gets the high-sorting key and
vice versa) so the two candidate orderings are always the exact reverse
of each other; reprobed the same mutant afterward and it now fails.

Adds a direct EdgesFor test (constructing FullEvaluation/EdgeMatch by
hand) pinning "From.Key before Role" in the five-key edge tiebreak - the
two existing EdgesFor tests each tie one of those two components, so
neither could tell a Role-first mutant from the rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:25 +02:00
Meta Vens
9bc50830c1 R-AF/R-AJ: shared parity fixture rows and regenerated vector files
The fixture had zero duplicate record keys across its ten rows and no
ordinal tied across two different schemas - both shapes this fix batch's
rulings needed and neither runtime's parity vectors ever covered.

R-AF adds a "citer_narrow" schema (real SHA-256 id, computed the same way
CultDocumentRegistry does - a probe run against a type matching the
eventual C# type's exact namespace/nesting confirmed it) whose one
reference targets leaf_a only, plus rows for both shapes from Soul's
table: a lone out-of-target row at a key (refuses), and two rows sharing
a key where only one is in-target (accepts, resolving the in-target one
regardless of row order).

Also drops "targetSchema" from every reference entry: it was never
anything but documentation (SelectionParityVectorTests.BuildDocument and
cultnet-rs's build_row both already read only targetKey) and keeping a
field the wire does not actually have would contradict the rule R-AF just
established.

R-AJ adds a tied-ordinal pair across leaf_a and leaf_b, with keys chosen
opposite to the real schema ids (probed) on purpose, so a schemaId-first
order and a recordKey-first order are forced to disagree regardless of
which real id happens to sort first - see the row-tiebreak test this
enables.

selection-vectors.cs-written.json and selection-vectors.rs-written.json
are regenerated from the new fixture (CULTNET_WRITE_VECTORS=1 on both
sides) and cross-checked in both directions locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:02 +02:00
Meta Vens
323ec3867c R-AF/R-AG: Rust resolves references by the wire's actual rule; checked cursor arithmetic
R-AF. Probed the real wire bytes a stored CultRecordRef<T> produces
(GameCult.Caching.MessagePack.CultRecordRefFormatter<T>.Serialize writes
only value.Key.Value - a probe against a real document confirmed the
serialized "Design" field is the bare msgpack string "a-eq", nothing
else). The wire carries no schema for a reference; C#'s rule was right
and Rust's exact-(schema,key) lookup assumed a schema id the edge never
actually carries.

Row::references() now returns (role, target_record_key, payload) - a
bare key, matching C#'s ReferencesOf. matches_citation/build_incoming_index
resolve through the new resolve_reference_target, which mirrors C#'s
TryResolveReferenceTarget: search every row sharing the key (by_key is
now keyed by record_key alone, grouping rows across schemas) for the one
whose schema falls inside the declared leaf set, refusing deterministically
(by schema id, code-point order) when none do. Folds D9's leaf-set check
into resolution itself rather than a second ensure_within_declared_target
pass - deleted, since it also dropped the old bypass-on-empty-leaves
behaviour C# never had (empty leaves always refuses when a candidate
exists).

Also moved the citation's own role filter before resolution in
matches_citation (mirroring C#'s ReferenceMembers(descriptor,
citation.Role), which narrows to that role's members before R-W's
per-edge check ever runs) - found while landing R-AF's vectors: Rust
was checking every declared reference a row carries regardless of the
requested role, a stronger and different guarantee than the C#
reference actually gives.

R-AG. read_length_prefixed_fields used unchecked usize addition
(pos + len) to bound a length prefix parsed from the wire; a hostile
prefix near usize::MAX overflowed and panicked. Uses checked_add now,
returning None (which Cursor::parse already turns into a typed
CursorInvalid) the same way every other malformed shape here already
does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:42:41 +02:00
Meta Vens
9c588f87af Record the merge gate: Cut 1 may not merge, on three counts
S-1 is not closed. R-V became two different rules: C# resolves an edge's
target by record key plus the declared leaf set, Rust by an exact schema-and-
key lookup. They give opposite answers on the exact shared-key shape the
ruling was written for, and no vector covers it because the fixture has no
duplicate keys at all. That is why it survived four passes.

Both runtimes fault on an attacker-supplied cursor, on mirror-image inputs.
C# overflows an int and lets an untyped exception reach the server's poll
loop; Rust overflows a usize and panics. Each holds exactly the hole the other
closed, which is what happens when a rule is hardened twice independently and
neither run is fed garbage.

R-Z was neither done nor reported as outstanding. I recorded batch 4 as
closing its rulings without checking that one, so the tracking failure is
mine. It also cannot be built as written: the two runtimes have no common
wire type for a page, so the comparison needs a shape decision first. I have
ruled one and flagged it for review rather than letting the vectors harden
around an unexamined choice.

Two further findings worth the pass. The Rust vocabulary has 59 live mutants
nobody has looked at, and about 35 of them sit on canonical_number, whose door
survives every branch mutation: the byte-for-byte parity rule is pinned by
nothing in Rust while the C# side was blocked at 66% over the same class. And
Soul settled the out-of-bounds read I had ruled most urgent: it is not a
defect, the mutant was a coverage gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 03:39:35 +02:00
Meta Vens
ad58423d09 Record the Rust half of batch 4, and the duplicate authority R-AB bought
R-AB worked and cost two fixes: the named include_str! escape, then a second
one in the fixture loader that kept the crate unmutable even after the first.
cultnet-rs can now be mutated at all for the first time. Scoped to the diff:
23 tested, 23 caught, nothing missed.

The price is that twenty-two schemas now exist twice, guarded by a drift test
rather than by structure. It drifted within hours: the rebase onto the C#
side's R-X changed a schema and the vendored copy did not see it. The drift
test caught it. The cleaner shape, a build script staging into OUT_DIR, fails
for exactly the reason the include_str! did, because cargo-mutants copies the
package tree. Publishing the contracts as their own crate would fix it and is
out of this cut. So the duplicate is accepted on the record, in the follow-ups
rather than the win column.

S-2 turned out to be C#-only. Rust already checked the declared target before
filtering by key, so the divergence was one-sided.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 02:43:16 +02:00
Meta Vens
d7f73af3b6 Rust R-AB: re-sync the vendored cultnet.error.schema.json after rebase
Rebasing onto the C# side's landed R-X work (fix batch 4) picked up
changes to contracts/cultnet/cultnet.error.schema.json that predate the
vendored copy this branch added. The drift test added for R-AB caught
it immediately: re-copied, no other vendored schema or the vendored
fixture differed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 02:41:59 +02:00
Meta Vens
97bc2076b1 Rust R-AB: vendor the parity fixture too, so cargo-mutants' baseline
actually runs

The schema_discovery.rs include_str! escape was not the only one:
tests/selection.rs's fixture() read
../../contracts/cultnet/interop/selection-vectors.fixture.json, the
same three-levels-up path shape, and 37 of 45 tests panicked on it
under cargo-mutants' isolated copy - confirmed on Yggdrasil after the
schema_discovery.rs fix alone still landed "cargo test failed in an
unmutated tree, so no mutants were tested".

selection-vectors.fixture.json is a checked-in input (not a generated
artifact), so it gets the same treatment as the 22 schemas: a
byte-identical vendored copy under
packages/cultnet-rs/contracts/cultnet/interop/, a drift test
(fixture_contract_matches_the_canonical_source, same skip-when-absent
shape as schema_discovery_contracts.rs), and the loader reads the
vendored copy unconditionally.

selection-vectors.cs-written.json is different in kind: it is the C#
reference's own generated output, produced only by a real cross-runtime
checkout running SelectionParityVectorTests with
CULTNET_WRITE_VECTORS=1. There is nothing to vendor - an isolated
Rust-only copy can never satisfy that test's premise regardless of what
this crate does with its own files. That one test now skips (with an
explanation) when the canonical contracts/ tree is entirely absent,
rather than panicking and failing the whole suite over a cross-runtime
artifact the isolated copy cannot produce by construction.

Verified locally by hiding contracts/ entirely and rerunning the full
suite (217 tests across every binary): unchanged pass count, same as
with it present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 02:29:24 +02:00
Meta Vens
4914e560d5 S-12: correct the shared fixture's alias prose, now that both runtimes
share one alias rule

selection-vectors.fixture.json's "//aliases" comment still called the
two alias forms a cross-runtime defect ("no candidate string that
resolves through both"). That was true before the Rust alias port; it
is not true now. Both runtimes resolve a schema alias the same one way
(strip a trailing .vN, compare the remainder against the descriptor's
declared SchemaName): nameAlias resolves through both, and hashAlias -
kept in the fixture on purpose - is refused as unmatched by both, since
neither runtime's production call sites produce or accept it.

Prose-only change to this shared fixture file, as ruled: no vector
values, schema ids, or row data touched.

Also recorded for Self's map: cultnet-rs's alias port (R-E) does not
carry SchemaVersion or CompatibleSchemaIds - no Rust call site needs
either, so schema_alias::matches folds them away and agrees with the C#
rule only on the subset it actually uses (candidate/id/name comparison,
not version or compatibility-list matching).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 02:29:24 +02:00
Meta Vens
1abd453115 Rust R-AB: make cultnet-rs self-contained enough for cargo-mutants
schema_discovery.rs embedded the 22 contract schemas via
include_str!("../../../contracts/cultnet/*.schema.json"), three levels
above src/ and outside packages/cultnet-rs entirely. There is no
workspace Cargo.toml tying this crate to its siblings, so cargo-mutants
treats this package's own directory as the whole tree it copies - every
one of those paths escaped it, and the unmutated baseline build failed
on all 22 with "No such file or directory". There was no mutation
evidence for the Rust half of this cut at all.

Fix: vendor byte-identical copies of the 22 schemas under
packages/cultnet-rs/contracts/cultnet/, git-tracked so they travel with
the package, and repoint every include_str! at the vendored copy. A
build script staging files into OUT_DIR was the other option named for
this rule, but it does not actually solve the problem: the script would
still have to read from outside the package to find its source, and
would fail the same way inside cargo-mutants' copy. Vendoring is
"another route that keeps every source path inside the package."

Vendoring duplicates files this repo treats as canonical
(contracts/cultnet/*.schema.json, shared across every runtime per
docs/cultnet-selection-cut.md section 1), so a new test,
schema_discovery_contracts.rs, compares every vendored file against its
repo-root original byte for byte and fails loudly on drift. It skips
(not fails) when the canonical contracts/ tree is not present beside
the package - the isolated-copy case R-AB exists to survive - so the
check cannot reintroduce the same escape it guards against.

cargo-mutants run on the crate, at least covering this cut's diff:
report to follow with every survivor triaged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 02:29:24 +02:00
Meta Vens
d9bc9eb974 Rust R-Y: the cursor's HMAC covers its own body, not only the selection
Cursor::compute_digest hashed the selection alone (cursor/limit cleared),
so a caller holding one valid cursor could splice its genuine digest onto
a body naming a different asOf/ordinal/schemaId/recordKey and reuse it -
the digest never bound the cursor to the position it names.

compute_digest now takes (asOf, ordinal, schemaId, recordKey, selection,
key) and folds the body into the same length-prefixed canonical string
before hashing; mint and verify_digest pass their own body through.

Tests: an in-module unit test (cursor_tests, next to Cursor, where the
private digest field and the length-prefix helpers are reachable) splices
a genuine digest onto a rewritten ordinal and a rewritten record key and
confirms verification fails on both; an integration test confirms two
cursors minted for different positions under the same selection and key
never share a digest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 02:29:24 +02:00
Meta Vens
ccea1098ce Rust R-V and R-W: row identity is (schemaId, recordKey), and each hop
edge is anchored by its own direction

R-V (S-1): select()'s row index and the cited incoming index were both
keyed by record key alone, so two schemas sharing one key collided -
which row a `cites` edge resolved to, and whether a `cited` selection
counted a row as cited, depended on row order. Both indexes are now
keyed by (schemaId, recordKey), matching CultCache's per-schema key
uniqueness.

R-W (S-3), Rust half: a selection carrying both `cites` and `cited`
computed one `cited_direction` flag and applied it to every edge in the
batch, so the `cites` edges vanished whenever `cited` was also present.
Each `EdgeMatch` now carries its own `EdgeAnchor` (Citer/Citee) set at
the point it is produced, and the page filter/sort anchor each edge by
its own hop instead of one shared direction.

Tests: two schemas at one record key, in both row orders, agree on
which row a citation resolves to and which row counts as cited; a
selection with both hops returns edges from both directions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 02:29:24 +02:00
Meta Vens
9e44a2e6e8 Record R-AA landing, and two corrections it made to Soul's triage
Score 66% to 77.62%, every named survivor group killed, no production code
touched. The score is the least interesting part.

Two of the mutants Soul called live are equivalent: within one hop direction
the anchor and role already determine the target, so the pair cannot diverge.
They are recorded as equivalent so nobody pays for them a third time. And a
sixth mutant nobody had named survived in the same method on R-B's primary
ordering key, which means the rule R-B exists to protect had its first
component unguarded and the triage missed it.

The digest needed a golden value rather than a comparison. A same-versus-
different test cannot catch a mutant that swaps which case gets which flag,
because the two digests still differ, just mislabelled. The golden was
reconstructed from the method's documented contract rather than its own
helpers, which is what makes it evidence.

Three suspected real defects were flagged rather than quietly fixed, and get
behaviour tests first. The reachable one is an out-of-bounds read from an
attacker-supplied cursor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:53:34 +02:00
Meta Vens
86752c3e2d Correct the outstanding list, which contradicted the section above it
The list still said Windows owed its release configuration hours after the
Windows leg landed and closed it. A map that disagrees with itself is worse
than one that is merely behind, because both halves look current.

What is actually outstanding is narrower: the burner stress, which is the
operator's fork, and whether earlier Windows numbers were measured against
the pinned MsQuic at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:50:53 +02:00
Meta Vens
99e366a36d R-AA follow-up: kill ComputeDigest's flag-swap survivors, pin EdgesFor's primary key
The first Stryker pass (72.18%, up from 66%) showed the named dozen mostly
dead but surfaced two related gaps the "differs" style of assertion cannot
reach on its own:

- ComputeDigest's field-number and cites.role presence flags each had an
  equality-mutation survivor (field.Number != null / selection.Cites.Role
  != null) that a same-vs-different digest comparison cannot catch: the
  mutant swaps which of two cases gets which flag value, so both cases
  still digest differently, just under swapped labels. A handful of plain
  AppendString/sb.Append deletions survived alongside it for the same
  reason - removing content that a same-vs-different pair didn't happen
  to isolate. One independently reconstructed golden HMAC value, built
  from the method's own doc comments rather than its private helpers,
  pins the whole wire shape at once: any single append or flag polarity
  wrong breaks the exact match.
- EdgesFor's OrderBy(pageRowIndex) - the primary "page-row order" key R-B
  names before "(from, role, to)" - had no test forcing page position and
  from/role/to order to disagree, so it survived alongside (and separate
  from) the five ThenBy mutants already named.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:48:10 +02:00
Meta Vens
5520d292eb Record the Windows leg, and that it was passing against the wrong MsQuic
The bridge imports msquic.dll by name and nothing ever copied the pinned
library beside the test binary. Linux failed loudly on this. Windows did not
fail at all: DLL search fell through to PATH and loaded an unrelated msquic
from the Windows Performance Toolkit, a tenth the size of the pinned build,
and every scenario passed against it with no sign anything was wrong. The
green result was what hid the defect.

Fixed by mirroring the Linux POST_BUILD copy on the Windows side. Every result
recorded here is from a rerun afterwards, and F1 is now closed on both
platforms.

The open question is how far back this reaches. Every earlier Windows number
in this campaign was measured on this machine, where the copy has never
existed. Those figures are unconfirmed until someone establishes whether the
deleted harness placed the library itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:44:47 +02:00
Meta Vens
cc71f17b7b Say what the cell-mass test proves, and what it does not
Soul: a constant erf passes it. It is one necessary property, not correctness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:43:52 +02:00
Meta Vens
f3aae704e9 Put msquic.dll where the QUIC native test build's loader needs it (Windows)
Running QUIC Cut 3's Windows leg exposed the same class of defect Linux
had at 5533c34: CULTMESH_QUIC_BUILD_TESTS has never had its own packaging
step on Windows, and build-quic-native.ps1's copy of msquic.dll only
lands in the final packaged output directory, never beside the test
binary in the CMake build tree.

Unlike the Linux failure, this one did not fail loudly. gamecult_mesh_quic_native.dll
imports msquic.dll by name, and Windows DLL search falls through to PATH
when the importing binary's own directory doesn't have it. This
workstation has an unrelated msquic.dll on PATH from the Windows
Performance Toolkit (529,480 bytes, unversioned) that is not the pinned
Microsoft.Native.Quic.MsQuic.OpenSSL 2.5.9 build (4,181,856 bytes). Every
asserts-build scenario ran clean against that wrong binary before this
fix, with no error to say so.

Mirrors the Linux POST_BUILD fix: CMake already receives MSQUIC_ROOT, so
the test target now copies msquic.dll from MSQUIC_ROOT/bin/x64 beside
itself instead of leaving the loader to guess.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:43:21 +02:00
Meta Vens
8ee1ebaf67 R-AA: behavioural tests for the dozen real Stryker survivors
Soul's triage on 52082ec named ~12 live survivors across
CultNetSelection.cs/CultNetSelectionEvaluator.cs, each sitting on a rule
this cut exists to enforce, plus 75 uncovered mutants behind them. Each
new test drives the public selection/evaluator surface with a fixture
built to make the rule's failure observable in the answer, not in the
code's spelling - several needed new fixtures (a mixed numeric/string
alias, an index alias differing from its member name, two reference
members resolving to one target) because their absence is why the
mutant survived in the first place.

Two of the five EdgesFor order-flip mutants (To.SchemaId/To.Key) are not
addressed: within one hop direction an edge's (From, Role) already
determines its To deterministically, so no legal selection can tie
anchor+From+Role while To differs. Recorded as a likely equivalence,
left for Stryker's own verdict rather than forcing a contrived test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:37:38 +02:00
Meta Vens
6e8362b7dc List erf/erfinv in the README's math vocabulary
They shipped without ever being added to the public surface list.
Scalar-only is noted explicitly: every other scalar in math.cs has
float2/3/4 overloads, erf/erfinv do not, and that stays a separate
scope decision since the only consumer (Aetheria fire control) is
scalar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:30:07 +02:00
Meta Vens
4cdc02b200 Give erf/erfinv real accuracy, domain and monotonicity tests
The round-trip test (ErfinvInvertsErf) was the only defense of
erfinv's accuracy, and it is blind by construction: it can only prove
erf and erfinv agree with each other, not that either is correct. A
hand mutation of a single tail coefficient degraded erfinv's worst
error 300-400x (5.0e-7 to 2.0e-4) and every committed test still
passed. Added ErfinvMatchesReferenceValues, pinned against reference
values from bisecting the platform's double-precision erf (Python's
math.erf, glibc-backed) rather than this package's own erf, out to
|y| = 0.9999, at the same 2e-6 absolute bar erf's own reference test
carries. Expected values are computed for the actual float32-rounded
input, not the decimal literal, because erfinv's derivative diverges
near +-1 and that rounding alone moves the true answer by more than
the error bar if ignored.

Verified against three hand mutations, each restored byte-exactly
afterward:
- erfinv tail leading coefficient 2.83297682f -> 2.83317682f: killed
  (4 assertions failed).
- erfinv tail second coefficient 1.00167406f -> 1.00187406f: killed
  (4 assertions failed).
- erf's `p` coefficient 0.3275911f -> 0.3275930f: survives. Soul
  measured this one 2.3x worse but still under the 2e-6 bar the spec
  pins; this is not a fixture gap, it is the mutant staying inside the
  accuracy envelope the tests are supposed to allow. Recorded here
  rather than tightening the bar past what the spec asked for.

Added ErfinvAtDomainEdgesAndBeyond, pinning the domain and edge
behavior from the matching source comment: erfinv(1) = +infinity,
erfinv(-1) = -infinity, and NaN outside [-1, 1] including +-infinity
and NaN input.

Deleted ErfinvIsMonotonicallyIncreasing: it asserted strict
monotonicity, which erfinv does not have. Scanning every representable
float32 in [0, 0.999999] finds 9662 backward steps, first at
y = 0.00022214651, worst drop 5.960464e-8 (one ULP at that scale) --
invisible only because the old fixture stepped by 0.001. erf has the
same shape: 100 backward steps over [0, 4.5], worst drop 1.192093e-7.
Replaced it with two tests: ErfinvIsMonotonicWithinFloatingPointTolerance,
pinning monotonicity to within 1e-6 (comfortably above the measured
noise floor), and ErfCellMassesStayNonNegativeAcrossRealisticGeometries,
the consumer-level property that actually matters for fire control:
differences of adjacent erf values (cell probability mass) stay
non-negative across 16/64/256/1024/4096 cells over +-4 sigma, plus a
tight 1 sigma spread and a very tight 0.25 sigma spread.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:30:07 +02:00
Meta Vens
fc9ce970b8 Fix erfinv's sign inversion at the domain edges
math.erfinv(1.0f) returned -infinity and erfinv(-1.0f) returned
+infinity: at value = +-1, w = -log((1-value)(1+value)) = -log(0) =
+infinity, the tail branch's leading coefficient is negative, and
`return p * value` delivered the inverted infinity. erf saturates to
exactly 1.0f at 5.543 sigma, so any interval edge beyond that reaches
this bug in ordinary use.

Guard both edges explicitly before the polynomial runs: erfinv(1) is
now +infinity, erfinv(-1) is -infinity, and anything strictly outside
[-1, 1] (including +-infinity) is NaN, which is the correct answer and
was already the fallthrough behavior. Documented the domain and the
edge behavior in a comment, matching the house style `sign` already
uses at math.cs:84. The approximation itself is unchanged.

Also recorded the true measured accuracy beside erf and erfinv: worst
absolute error 6.621e-7 and 5.066e-7 respectively, both inside the
2e-6 bar this package's tests pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:30:07 +02:00
Meta Vens
6a2b42f650 Pin mtp test-runner for CultMath and document mutation testing
CultMath.Tests is on xunit.v3. Stryker's default vstest runner cannot
capture coverage against xunit.v3's test host and hangs retrying test
sessions indefinitely (observed: 2096 mutants queued, zero completed after
~9 minutes of retries). Stryker's Microsoft.Testing.Platform runner
(-t mtp, still preview) works correctly against xunit.v3 and completed the
same 1351-mutant run in ~4.5 minutes. Pin it in config so a plain
`dotnet stryker` works without a CLI flag.

Add docs/mutation-testing.md: how to run it, the --since form for a single
cut, the triage rules, a known coverage-capture caveat in the mtp runner
(confirmed by hand-mutation against static readonly fields), and the
packages still needing their own config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:24:05 +02:00
Meta Vens
a4d28c7148 Record the QUIC README fix: the documented path failed at its first command
Two independent breaks, not one. The loader failure is fixed in CMake rather
than in prose, using an argument the build already required. The second break
fires earlier: relative cache paths resolve against the CMakeLists.txt, not
the shell, so the build died on a missing header before the loader was ever
reached.

That example was published in the same commit that deleted the working path,
and nobody ran it. Running the documentation is the check that would have
caught it, and running it is what caught it now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:20:16 +02:00
Meta Vens
bfb1ea338e Record fix batch 4's C# half, and the schemas its new refusal exposed
Three commits on cultnet/selection-cut1 close R-V, R-W, R-X, R-Y, S-4, S-8,
S-9, S-10 and S-11. R-V is proven in both row orders, which is the point: the
old behaviour depended on which row was enumerated first.

Making the validator refuse keywords it does not implement immediately found
twelve uses across three committed schemas that nothing had ever validated.
Those three are formally unvalidatable now. Nothing broke because nothing ran
them, which is the finding rather than a reassurance.

Hands named two gaps instead of papering over them: S-11 is fixed by
inspection with no dedicated test, and the three schemas need a decision.
Caching's three failures remain unproven rather than passing, since the
stopgap's container runs as root and they expect a write to fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:16:24 +02:00
Meta Vens
5533c34783 Make the QUIC native test build put libmsquic.so.2 where the loader needs it
The README's Linux test build has never worked: both the bridge and the
test executable carry INSTALL_RPATH "$ORIGIN" and expect libmsquic.so.2
beside them, but only scripts/build-quic-native.sh's packaged output
directory ever got that copy. A test build produced a binary that died
at once with "cannot open shared object file". Fix it where the
knowledge already lives instead of adding a README workaround: CMake
already receives MSQUIC_LIB_DIR, so the test target now copies
libmsquic.so.2 beside itself in a POST_BUILD step.

While verifying the README's own worked example end to end, found a
second, independent break in the same docker block: MSQUIC_INCLUDE_DIR
and MSQUIC_LIB_DIR were passed as paths relative to the repository
root, but CMake resolves a relative cache-variable path against the
CMakeLists.txt that consumes it, not the invocation directory, so the
compile failed on a missing msquic.h before it ever reached the
loader. Made both paths absolute with $(pwd) and documented why.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:15:45 +02:00
Meta Vens
7fd16ec45a Put the campaign maps on main, where the agents reading them actually look
A Hands agent working the selection cut reported that the map had no fix
batch 4 section. It did not, on the branch it could see: the live copy was on
my own working branch, main was a day behind, and the selection work branch
carried a copy from 2026-09-17. It worked from its brief instead and said so,
which is the only reason this surfaced rather than producing a cut built on
five-day-old design.

That is my failure, not the agent's. Self commits the maps and I had been
committing them wherever I happened to be standing. A map is only current if
it is current on the branch the reader has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:15:21 +02:00
Meta Vens
b7d77b708d Fix batch 4 (R-X, S-4/S-10): the error schema matches the wire, and the validator refuses a keyword it does not implement
CultNetErrorMessage is a MessagePack-CSharp map-mode object, so every
declared key is written on the wire, nil included - the same fact R-S
already established for the raw document header. cultnet.error.schema.json
was missing routingHint entirely and typed code/details as always-present
non-null values; Soul's P7 found real bytes failing it on exactly this.
routingHint, code and details are now `required`, with nullable types
matching CultNetErrorMessage/CultNetErrorDetails. A new test decodes real
bytes for all four refusal codes (selection_invalid, cursor_stale,
cursor_invalid, reference_outside_target) against the schema.

MiniJsonSchemaValidator:
- implements minimum, maximum, maxLength and maxItems, which were declared
  in committed schemas (fieldPredicate's number pattern, selection.limit,
  ...) but silently never checked;
- anchors `pattern` to a full match (Index==0 && Length==value.Length)
  instead of Regex.IsMatch's "search anywhere", so .NET's `$` (which,
  unlike ECMA-262's, matches immediately before one trailing newline even
  without RegexOptions.Multiline) can no longer let e.g. "5\n" pass a
  schema author wrote as "exactly one digit";
- refuses a schema node that uses a keyword this validator gives no
  constraint semantics to, instead of silently ignoring it. Running every
  committed contracts/cultnet/*.schema.json schema through this found 12
  such uses, none reached by any existing test: `allOf`/`if`/`then`/`else`
  in cultnet.database-change-raw.schema.json, and `uniqueItems` (six
  places) plus one `anyOf` in the cultmesh.verse-catalog-*.schema.json
  pair. contentEncoding, $schema/$id/$defs/title/description/default and
  the x-cult-runtime-type vendor extension are treated as metadata with no
  constraint semantics to implement, not refused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:23 +02:00
Meta Vens
b3a8f6d195 Fix batch 4 (S-9/S-11): shard-scoped asOf, and the v1 subscribe door runs before the hop refusal
S-9: CurrentAsOf() returned the maximum shard-log sequence across every
shard the database holds, but that sequence is a per-shard counter -
mixing shards' counters together could report a page as exact "as of" a
sequence a different, untouched shard advanced to while the page's own
shard sat still. CultNetDatabase gains CurrentAsOf(shardId), the watermark
of one shard's own log, and CultNetDocumentRegistry.SelectPage now scopes
asOf to the single shard EnsureSingleShard already resolves the matched
rows to, falling back to the caller-supplied value only when the selection
matches no rows.

S-11: CultNetDatabaseServer.HandleSubscribeV1Async refused a hop-bearing
selection before running the door (Validate), so a selection invalid for
an unrelated reason (an empty keys list, an undeclared index, ...) got a
hop refusal that had nothing to do with why it was actually rejected. The
door now runs first, unconditionally. The hop refusal also always named
the field "cites" even when the selection carried Cited instead; it now
names whichever hop is actually set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:12:07 +02:00
Meta Vens
1bf581d68b Record the QUIC Linux leg green, and a README path that never worked
All ten scenarios pass under asserts and all four required under release, on
Yggdrasil, in the committed debian:13 image. Worst overshoot anywhere was 4 ms
against a 400 ms tolerance. F1 is closed on Linux: the build that ships now
runs the timeout rule.

Running the README's own worked example is what found the second thing. It
cannot work: the test binary carries an $ORIGIN rpath and nothing puts
libmsquic beside it for a test build. That example was written in the same
commit that deleted the harness, so the working path was removed and the
broken one published together.

Two things stay open. Windows still owes its release configuration for the
same reason Linux did. And F5's stress wants sixteen CPU burners on the
platform where burners are now forbidden, which is the operator's call rather
than mine to route around.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:12:05 +02:00
Meta Vens
e99bf46520 Fix batch 4 (R-V/R-W/S-8/R-Y): row identity is (schemaId, recordKey), and the cursor digest covers its own body
R-V (S-1): byKey and BuildIncomingIndex keyed the hop's row resolution by a
bare record key. Two schemas sharing one key collapsed into a single
dictionary slot (last writer wins), so which row a reference/citation
resolved to - and therefore whether a selection matched or refused
reference_outside_target - depended on row insertion order. byKey is now a
bare-key -> row-list index, and a shared TryResolveReferenceTarget searches
each edge's declared leaf set to resolve the one row that actually belongs
to it, deterministically regardless of order (Soul's P2, tested both ways).
EdgesFor's page-row anchor index is keyed the same way for the same reason.

R-W (S-2): EnsureWithinDeclaredTarget's equivalent check (now folded into
TryResolveReferenceTarget) now runs for every edge a reference carries,
before a citation's own key/schema filters that edge out - not only for the
edge the citation happens to ask about. A many-reference edge into a schema
outside its declared target refuses the selection even when the caller's
key filter would otherwise skip past it without ever inspecting it (Soul's
P3).

S-8: the dead `targetType ?? typeof(object)` fallback is deleted. D11
already refuses every reference shape whose target the cache cannot infer
before it can reach the evaluator, so the branch had no reachable fixture.

R-Y: the cursor's HMAC now covers its own body (asOf, ordinal, schemaId,
recordKey) as well as the selection. Before this, a forged cursor could
reuse a legitimately minted digest while rewriting the ordinal or record
key underneath it, since nothing about the body was ever authenticated
(Soul's P4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:11:54 +02:00
Meta Vens
cbff486b1e Record where Soul's selection probes live, and that losing them is acceptable
The pass ran against 52082ec. Its nine hand probes, including the Rust
both-hops probe that proved S-3 on a matched pair, sit unpushed on soul-probe
in a scratch clone. Probes are throwaway by doctrine, so the pointer says both
where they are and that the findings recorded in this map are the durable
copy. Fix batch 4's tests have to reproduce the behaviour independently
anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:00:23 +02:00
Meta Vens
c5c4c073b4 Correct the record: QUIC batch 8 landed before the retirement
My previous commit rewrote the batch-8 rulings as if the batch were still
owed. It was not: it landed at 5646ff7 and the harness went at 796433f, both
on hands/cultmath-erf. The map was behind the code, not the other way round.

What that changes: the release configuration and the seven mutation entries
landed inside the harness and left with it two commits later, so the release
build is owed again as a plain build. The scenario runner and every seam
survived untouched and now run as direct arguments to the binary.

Cut 3's only outstanding work is the Linux leg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:55:37 +02:00
Meta Vens
7889515993 Rewrite QUIC batch 8 under the harness retirement
Four of the six rulings for the eighth fix batch named harness entries. The
operator retired the hand-written mutation harness the same day, with no
fallback, so those clauses are dead text. What each ruling protected is not:
the release configuration, the seam wrapping the whole duration expression,
pollhammer against a runtime holding live objects, the zero and negative
timeout bounds, and closerace surviving starvation all stand as behaviour.

The bridge is C++ and no ecosystem mutation tool is wired here, so it gets no
committed mutation evidence at all. Its rules are pinned by scenarios at the
layer that decides them; a mutant is something Soul tries by hand and throws
away.

F5's 16 burners now run on Yggdrasil. Those burners are what froze the
operator's workstation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:54:00 +02:00
Meta Vens
c244003339 Record the Stryker triage: a dozen live survivors on this cut's own rules
The triage landed on the fifth attempt. Most of the 85 survivors are message
strings; about a dozen are real, and they sit on the rules the cut exists to
enforce: the alias-numeric refusal degrades to "only if every schema agrees",
a half-blank cites.target stops being refused, the index-alias role naming is
untested at all four sites, the row tiebreak and EdgesFor's ordering are
pinned by nothing, and ComputeDigest's hop branches have no coverage at all.
The equivalent mutants are named so nobody chases them twice.

R-AA's triage half is done; what it now owes is a behavioural test per live
survivor.

Soul also withdrew its own claim that C#'s cursor digest does not sort its
lists. It does, at CultNetSelectionEvaluator.cs:637.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:52:20 +02:00
Meta Vens
e92413d6eb Add Stryker config for CultMath and ignore its output
Scope mutation testing to CultMath.Tests only, matching Aetheria's
stryker-config.json shape: Standard level, progress/html/json reporters,
break threshold 0 (score is not a gate). Mutate only CultMath's own
sources, excluding the generated Swizzles.g.cs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:50:10 +02:00
Meta Vens
a9bb08f9db Record Soul's corrections: the job was not wedged, and the Stryker triage is owed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:49:08 +02:00
Meta Vens
3e13c14f06 Adopt Stryker.NET as CultLib's mutation-testing tool
Pin dotnet-stryker 5.0.0 as a repo-local tool, matching Aetheria's Cut 11
adoption (F:\Projects\Aetheria dotnet-tools.json) and the current latest
release on nuget.org.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:49:05 +02:00
Meta Vens
618b7b562b Record the whole-cut Soul pass: the hop is keyed wrong and the runtimes disagree
Cut 1 may not merge. Row identity becomes (schemaId, recordKey), the hop
behaves identically in both runtimes, the error message obeys its own schema,
and cultnet-rs becomes mutable by cargo-mutants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:44:40 +02:00
Meta Vens
b688cb2091 Cut 12.0: test erf and erfinv behaviour
Covers the pinned accuracy bar against tabulated erf values (0, +-0.5,
+-1, +-2, +-3, within 2e-6), odd symmetry, the +-1 limits, erfinv's
round trip through erf over y in [-0.999, 0.999], and erfinv monotonicity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:42:57 +02:00
Meta Vens
87b95aefa5 Cut 12.0: add erf and erfinv to CultMath
Fire control's Gaussian scatter needs erf for a cell's probability mass
and erfinv to draw the lateral offset, both called per shot. Abramowitz-
Stegun 7.1.26 for erf (stated max abs error 1.5e-7), Giles 2010 (GPU
Computing Gems) for erfinv. Neither is an HLSL intrinsic, so there is no
shader-parity obligation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:42:57 +02:00
Meta Vens
1bcc19298f Record C# pass B, and correct Self's cursor-parity premise
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:23:38 +02:00
Meta Vens
52082ec720 Add "not" support to MiniJsonSchemaValidator
fieldPredicate's oneOf branches use `not: {required: [...]}` to make
values/number exclusive; the validator had no "not" handling at all, so it
silently ignored the keyword and FieldPredicate_ValuesAndNumberTogether_FailsItsSchema
found nothing wrong. A schema matches "not" when validating against the
subschema produces zero errors, in which case the instance must not match it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:13:53 +02:00
Meta Vens
f47060d5c2 Land fix batch 3's R-S: the schemas describe the bytes C# writes
New contracts/cultnet/cultnet.raw-document-header.schema.json for
CultNetRawDocumentHeader - it has no payload/payloadEncoding members at all,
so headers must not $ref the record schema (which required both). The
response page's own schema now lists every property CultNetSnapshotResponseRawV1Message
declares as `required` (present on the wire, nil included) rather than only
the four that happened to be non-nullable - MessagePack-CSharp's map-mode
formatter writes every declared key regardless of null, so `headers`/`documents`
are always both present, one an array and the other an explicit null by
projection. The old `oneOf: [{required:[headers]},{required:[documents]}]`
matched both branches on every real page (both keys present) and so never
validated anything; it is now `oneOf` over each key's type (array vs null),
which is mutually exclusive. cultnet.raw-document-record.schema.json gained
schemaName/schemaVersion/schemaContentHash (present on every real record, not
previously in the schema at all - any real page would have failed
additionalProperties:false on them). fieldPredicate's two oneOf branches each
gained `not: {required: [the other]}`, so `values` and `number` cannot both
be present (previously only `required` was checked, which does not exclude
the sibling key).

Added MiniJsonSchemaValidator, a deliberately small draft-2020-12 subset
validator (type/required/additionalProperties/properties/items/const/enum/
pattern/minLength/minItems/oneOf/$ref, same-document and cross-document) -
no schema-validation dependency existed in this repo, and this only needs to
cover what contracts/cultnet/*.schema.json actually use. CultNetSchemaContractTests
decodes real wire bytes from a live CultNetDatabaseServer (header projection,
document projection, and a hop with edges) through MessagePackSerializer.ConvertToJson
and checks them against the committed schema files, plus three
fieldPredicate-shape tests pinning the values/number exclusivity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:12:58 +02:00
Meta Vens
753c750e37 Fix the cursor body's length prefix to count UTF-8 bytes, not UTF-16 chars
AppendString (CultNetSelectionEvaluator.cs) wrote value.Length, .NET's UTF-16
code-unit count, but Cursor.Parse's ReadLengthPrefixedFields slices that many
bytes out of the UTF-8-encoded body. An astral character (a surrogate pair:
2 UTF-16 units, 4 UTF-8 bytes) made the two counts disagree, so a cursor
minted for a row keyed with one (Evaluator_CursorPositionUsesCodePointOrderMatchingTheRowTiebreak's
astral key) failed to parse back - caught by that existing test on Yggdrasil,
not a new one. The prefix is now Encoding.UTF8.GetByteCount(value); the
digest's canonical string never round-trips through the byte-sliced reader
(it is hashed whole), so this only had to be right for the cursor body, but
one counting rule for both is what keeps AppendString actually unambiguous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:06:16 +02:00
Meta Vens
d08fe0449f Drop the duplicate ResolveShard added in the previous commit
CultNetDatabase already had a public ResolveShard(schemaId, key) wrapping
ResolveShardInternal; the previous commit added a second one with the same
signature. CultNetDatabaseServer.CreateSelectionResponse's shardIdOf callback
uses the pre-existing one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:05:10 +02:00
Meta Vens
a4cf54f4a9 Land fix batch 3's R-O and R-Q: keyed length-prefixed cursors, replica-apply sequence gap
R-O: CultNetSelectionCursorKey is a per-process (per-CultNetDatabase-instance) 32-byte
random key. CultNetSelectionCursor.ComputeDigest is now an HMAC-SHA256 under that key
instead of an unkeyed SHA-256, so a cursor cannot be forged without the key and does not
verify across a restart (a fresh CultNetDatabase mints a fresh key). The cursor body
(asOf/ordinal/schemaId/recordKey/digest) is length-prefixed (<byte length>:<value>, no
delimiter) instead of joined with a bare "\x01"-separated string, so a record key
carrying any character, U+241F included, round-trips. CultNetSelectionEvaluator.Select/
Page/ComputeDigest take an optional CultNetSelectionCursorKey, defaulting to a
process-wide key generated once so existing callers that never wire up a database still
get a keyed digest. CultNetDatabase gets a CursorKey property (random unless
CultNetDatabaseOptions.CursorKey is supplied) and passes it through
CultNetDatabaseServer.CreateSelectionResponse.

Rust already landed R-O (5aac120) under its own algorithm, deliberately not byte-matching
the reference: its own doc comment says a cursor is never handed across runtimes, so only
collision-freedom and keying are load-bearing, not the wire bytes. That is a real
divergence from this pass's instruction to match Rust's wire encoding exactly - reported
in full in the batch report.

R-Q: the two replica-apply paths (ApplyCommittedPutAsync, ApplyCommittedDelete) recorded
the mutation-log entry but never set _lastWriteSequence, so a row's order/cursor position
looked like "never written" until it was written again locally. Both now set it from the
entry's own sequence, matching AppendMutationLogEntry's local-commit path.
InitializeLogSequencesFromStore rebuilt only _nextLogSequences at startup; it now also
rebuilds _lastWriteSequence by replaying the durable log's Put/Delete entries in sequence
order. CultNetDatabase.ResolveShard is a new public wrapper over the existing shard
resolution; CultNetDocumentRegistry.SelectPage/CreateSelectionResponse take an optional
shardIdOf callback and refuse (reusing CultNetSelectionInvalidException/selection_invalid,
R-N) when a selection's matched rows resolve to more than one shard's log, because asOf is
a single watermark and is not exact for a shard-spanning page.
CultNetDatabaseServer.CreateSelectionResponse wires shardIdOf/cursorKey through from the
database.

Also fixes a defect the Rust Hands found while matching wire bytes:
CultNetErrorMessage.ForReferenceOutsideTarget double-prefixed "reference_outside_target: "
- the exception's own message already carries it. Now prefixed once, matching Rust's
ForReferenceOutsideTarget.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:04:22 +02:00
Meta Vens
f1aa105472 Record R-N in both runtimes, and the MessagePack key-order trap
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:02:49 +02:00
Meta Vens
eff62e59fb Land R-N in Rust: cultnet.error.v0 code/details, matched byte-for-byte
CultNetMessage::Error gains typed code (CultNetErrorCode) and details
(CultNetErrorDetails) fields mirroring the landed C# reference
(CultNetErrorMessage/CultNetErrorDetails, CultNetSchemaMessages.cs). The
message now encodes/decodes through the raw cultnet.schema.v0 path
(encode_raw_cultnet_schema_message/parse_raw_cultnet_schema_message)
instead of the generic serde_json::Value round trip every other
CultNetMessage variant uses, because that path's BTreeMap-backed
serde_json::Value would sort keys alphabetically and break the C#
reference's fixed schemaVersion/error/routingHint/code/details map
order - every optional key is written nil, never omitted, matching
MessagePack C#'s always-five-key formatter.

Adds the SelectionRefusal -> CultNetMessage mapping the fix-batch-3
comment in selection.rs called out as pending on the C# shape landing,
covering all four codes (selection_invalid, cursor_stale, cursor_invalid,
reference_outside_target).

cultmesh-rs's one CultNetMessage::Error match gets an added `..` so it
still compiles against the wider variant; it does not read code/details.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:00:37 +02:00
Meta Vens
e8a3decbab Fix batch 3 tests: R-N wire decode, R-P wire-id authorization, R-T removal/cites
Four CultNetErrorMessage wire round-trip tests (decode the bytes a peer actually
receives) proving code+details for selection_invalid, cursor_stale, cursor_invalid,
reference_outside_target. A subscription-server round trip proving snapshot and
first live change are authorized by the same wire id (R-P). A subscription-server
round trip proving a removal under a field-predicate selection is delivered with
no exception (R-T(a)). An evaluator test proving an implicitly-typed many
reference still refuses an out-of-target edge (R-T(b)).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:46:32 +02:00
Meta Vens
66b5e60b43 Fix batch 3: R-N wire refusal codes, R-P one authorizer id, R-T removal/cites gaps
R-N: cultnet.error.v0 gains additive code/details fields (contracts/cultnet).
CultNetErrorMessage grows ForSelectionInvalid/ForCursor/ForReferenceOutsideTarget
factories; both C# servers (CultNetDatabaseServer for snapshot+live,
CultNetDatabaseSubscriptionServer) use them at every existing catch site, plus
two sites that previously let the exception escape uncaught instead of reaching
the peer (CultNetDatabaseServer's v1 subscribe snapshot branch,
CultNetDatabaseSubscriptionServer's Subscribe around CreateProjectedSnapshot).
CultNetSelectionCursorException now carries AsOf/Current for cursor_stale.

R-P: CultNetDatabaseSubscriptionServer.CreateMatchedRecord (the live fast path)
authorized against descriptor.SchemaId; the snapshot path (CreateProjectedSnapshot)
already authorized against the wire id. Both now call the same
CultNetDocumentRegistry.WireSchemaId (made internal, was private) so one id
reaches the authorizer on the snapshot, live and reconcile paths alike.

R-T(a): CultNetSelectionEvaluator.MatchesSchemaKeysFields no longer reads a
Removed change's (always-null) document against Fields - it matches on
keys/schemas alone and treats a field predicate as unknown instead of throwing
TryGetIndexValue(null).

R-T(b): EnsureWithinDeclaredTarget no longer skips the out-of-target check when
TargetType is null; it checks against every registered leaf (unconstrained is a
real, checked target set, not a bypass). CultCache's ResolveReferenceTarget also
now infers the target from a many reference's CultRecordRef<T> element when no
explicit [CultReference(typeof(...))] is declared, which was the one remaining
source of a null TargetType reaching that check on a real, testable path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:46:32 +02:00
Meta Vens
806c05a700 Record fix batch 3's landed pieces and what is still pending
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:37:35 +02:00
Meta Vens
c606eb38f2 Back out the guessed R-N wire shape; R-N is pending on this branch
Self's sequencing note (2026-09-22): the C# R-N pass (code/details on
cultnet.error.v0) is running separately in C:\ws3 and will report the
exact field names and code strings once it lands. Do not guess them.

Reverts the earlier Error{code,details} fields on CultNetMessage, the
SelectionRefusal::wire_code/wire_details/to_wire_message mapping, and
their five tests. R-O, R-T, R-U's hand-probes and R-R are unaffected -
none of them depend on the wire error shape. When the C# R-N commit
lands, pull it, read its exact field names and codes, and implement the
Rust mapping against those instead of a guess.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:35:57 +02:00
Meta Vens
a312f6072c CultNet selection cut1 fix batch 3, Rust half: R-R, the v0 lowering matches the C# pre-cut server
docs/cultnet-selection-cut.md, Self's rulings for fix batch 3 (2026-09-22),
R-R: "the Rust v0 lowering must match the C# pre-cut v0 behaviour" -
recordKeys: [] answers empty, and the requested key order is kept. The
parallel C# pass landed the same correction on the reference server at
5e33d81/1afbaed/f8ba2d0 (pulled via rebase); this is the Rust side of it.

lower_v0_list (packages/cultnet-rs/src/snapshot_query.rs) used to collapse
a present-but-empty-after-trimming schemas/recordKeys list down to `None`
("no filter"), turning `recordKeys: []` into "every row" instead of the
pre-cut server's actual answer of nothing. It now returns `Some(vec![])`
for that case; the shared evaluator's matches_schema_keys_fields already
treats a present-but-empty list as "match nothing" (R-E), so no further
door/short-circuit logic was needed here - unlike the C# reference,
serve_read_only_raw_snapshot never calls validate, so there is no v1 door
for this shape to hit (the R-R fixup at 1afbaed doesn't apply to Rust).

serve_read_only_raw_snapshot also now restores the caller's requested
record-key order on the response (a stable sort by position in
record_keys), matching the pre-cut server's HashSet<string>
insertion-order iteration instead of leaking plain snapshot-source order.

Rewrote the stale test that pinned the old (wrong) "empty/blank list is no
filter" behaviour into
serve_read_only_raw_snapshot_omitted_v0_schema_list_is_no_filter_present_empty_or_blank_answers_empty,
and added serve_read_only_raw_snapshot_keeps_the_requested_record_key_order.
Hand-verified both by reverting each fix locally and confirming its own
test fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:32:02 +02:00
Meta Vens
5aac120a58 CultNet selection cut1 fix batch 3, Rust half: R-N/R-O/R-T source and tests
The source and test changes for the previous commit's message - split out
because the harness-deletion staged from an earlier `git rm` committed
alone before these paths were added. Same ruling, same scope: R-N's wire
code/details, R-O's keyed length-prefixed cursor, R-T's every-edge cites
check, and their behavioural tests (including the hand-verified mutants:
reverting each of the three kills its own new test).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:26:56 +02:00
Meta Vens
5cf31d1e87 CultNet selection cut1 fix batch 3, Rust half: R-N/R-O/R-T, retire the Rust mutation harness
docs/cultnet-selection-cut.md, Self's rulings for fix batch 3 (2026-09-22),
the Rust-owned share:

- R-N: cultnet.error.v0 gains additive code/details fields. SelectionRefusal
  now maps to a wire code (selection_invalid, cursor_stale, cursor_invalid,
  reference_outside_target) and a flat string-map details payload via
  to_wire_message(); the legacy array wire (gamecult.networking.v0) has no
  slot for either and never carries them, matching v0 peers ignoring them.
- R-O: the snapshot cursor is keyed. CursorKey is a random 32-byte
  per-process key; Cursor::mint/compute_digest now take it and use
  HMAC-SHA256 instead of plain SHA-256, so a cursor forged without the key,
  or minted under a different process's key, is refused cursor_invalid.
  Cursor::parse stays public and decodes as_of without the key (Huginn's
  dependency); only verify_digest (called from select()) needs it. The
  cursor body itself is now length-prefixed fields, not a delimiter-joined
  string, so a record key carrying any character - including the pre-fix
  C# delimiter U+241F - round-trips.
- R-T: matches_citation (the `cites` path) now checks every one of a
  citer's edges against its declared target before applying the
  citation's own role/key filters, closing F15: a corrupt edge used to
  hide behind an unrelated filter and never get validated.
- The retired Rust mutation harness entries file
  (scripts/mutate-cultnet-selection-rust.entries.ps1) is deleted; tests
  pin the behaviour directly.

Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json
from the shared fixture; it is byte-identical to what was committed, so
R-T's fix does not change any existing fixture vector's outcome. The Rust
reader still evaluates every vector in the committed cs-written.json
(15/15) unchanged - that file predates the parallel C# Hands' R-N/R-S
work and was not touched here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:26:56 +02:00
Meta Vens
f3667913ec Regenerate cs-written.json with the two new cites-refusal vectors
Ran CULTNET_WRITE_VECTORS=1 dotnet test tests/GameCult.Networking.Tests -c
Release --filter FullyQualifiedName~WriteSelectionVectors on Yggdrasil
against b1932d9 and committed its output verbatim. 17 vectors total (up from
15): the 4 door-refusal vectors now include
refuses_unmatched_cites_target_schema and
refuses_cites_target_by_hash_shaped_alias.

Per the brief, this is the fix-batch-3 report's parity-vector table baseline.
The C# reader (SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyIn
TheReference) now passes all 16 of rs-written.json's vectors, up from 14/16 -
rs-written.json already carried both refusal vectors; only this file and the
door check that makes it agree were missing on the C# side. rs-written.json
itself was not touched (Rust-owned).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:24:20 +02:00
Meta Vens
b1932d95ed Add the two pending cites-refusal parity vectors, now that C# refuses them
Item 1 of the fix-batch-3 brief: check whether CultNetSelection.Validate
refuses a cites.target.schemaId that CultNetSchemaAliasMatching cannot
resolve. It already does (CultNetSelection.cs:389-394), landed by an earlier
Hands pass on this branch (the "C# matrix Hands", after the parity table in
the spec was captured) - so no production code changed here.

What was still missing: SelectionParityVectorTests.RefusalCases() had a
comment explaining why refuses_unmatched_cites_target_schema stayed
"Rust-only", dated before that door check landed. The comment was stale and
the vector was never added. Added it, plus
refuses_cites_target_by_hash_shaped_alias (Self's ruling 2026-09-22: "the C#
reference's alias rule is the rule" - C# only resolves a name-form alias, so
a hash-shaped cites.target.schemaId is exactly the same unresolved-target
case and must refuse at the same field). RefusalCases() now takes the
fixture's schemas dictionary to reach schemas["leaf_a"].HashAlias.

Verified on Yggdrasil: dotnet test tests/GameCult.Networking.Tests -c Release
--filter FullyQualifiedName~SelectionParityVectorTests (the existing
cross-runtime reader against the committed rs-written.json still passes, so
this change does not depend on anything the Rust Hands owns). Regenerated
contracts/cultnet/interop/selection-vectors.cs-written.json with
CULTNET_WRITE_VECTORS=1: both new refusal vectors are present and both
refuse at "cites.target.schemaId" as expected before being committed as
vectors (WriteSelectionVectors's own Assert.Throws gate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:22:18 +02:00
Meta Vens
f8ba2d0aca R-R vs R-F discrepancy: CultMesh's typed API refuses empty keys, doesn't answer empty
Discrepancy between spec and Body, reported per Hands' brief ("make the
smallest change that keeps the spec's intent, and report the discrepancy").

R-R says "recordKeys: [] answers empty" for v0 lowering. That holds for the
raw v0 wire protocol server (CultNetDocumentRegistry.CreateRawSnapshotResponse,
CultNetDatabase.CreateShardSnapshotResponse - fixed two commits back). It does
not hold for CultMesh's typed convenience API: R-F, landed earlier in this
cut, made CultMesh.EnsureV0Compatible run CultNetSelectionValidation.
ValidateShape client-side before a request is ever sent, specifically so that
"Keys=[]/[\"\"] is refused rather than silently lowered to 'every key'"
(CultMeshSnapshots.cs:836-841). Once R-R made ResolveDefaultSelection's
Clean(recordKeys) produce Keys=[] instead of Keys=null for an explicit empty
list, every FetchDocumentsAsync(recordKeys: []) call now trips that existing
R-F door and throws, instead of reaching the wire at all.

Kept both rulings as each one's own owner decided them: the raw wire protocol
answers empty (R-R); CultMesh's typed wrapper refuses before sending (R-F).
Rewrote FetchDocumentsAsync_WithExplicitEmptyRecordKeys_AnswersEmpty, added
two commits ago on the wrong assumption that CultMesh's typed API also
answers empty, to FetchDocumentsAsync_WithExplicitEmptyRecordKeys_
IsRefusedClientSide, asserting the actual (and, on inspection, correct) R-F
behaviour instead.

Verified on Yggdrasil: dotnet test tests/GameCult.Mesh.Tests -c Release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:18:00 +02:00
Meta Vens
1afbaed775 R-R fixup: v0's empty-answers-empty must not reach the v1 door
The first attempt at R-R built a CultNetSelection with an empty (non-null)
Keys/Schemas array straight from the lowered v0 filter and handed it to the
shared evaluator. CultNetSelectionValidation.ValidateShape refuses exactly
that shape ("selection.keys is present and empty"), because for the v1 typed
door an empty list is a caller mistake. v0 and v1 read the same emptiness
differently on purpose (v0: answer nothing; v1: refuse), so v0 must decide
its answer before the selection ever reaches Validate.

CultNetDocumentRegistry.CreateRawSnapshotResponse and
CultNetDatabase.CreateShardSnapshotResponse now short-circuit to an empty
document set when the lowered schemas or keys list is present-and-empty,
without constructing a CultNetSelection or calling into the evaluator at all.

Caught by the test added in the previous commit
(CultNetDocumentRegistry_RawSnapshot_ExplicitEmptyRecordKeys_AnswersEmpty),
which failed with CultNetSelectionInvalidException before this fix. Verified
on Yggdrasil: dotnet test tests/GameCult.Networking.Tests -c Release, all
green (217 passed, 2 skipped by design).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:15:22 +02:00
Meta Vens
5e33d81e25 R-Q3, R-R: v0 lowering preserves v0's pre-cut answers exactly
CultNetV0SelectionLowering.Lower collapsed a non-null-but-empty filter list to
null ("no filter"). That corrupted two pre-cut v0 behaviours the same
function feeds:

- CultNetDatabase.CreateShardSnapshotResponse and
  CultNetDocumentRegistry.CreateRawSnapshotResponse (b3d9cf7) both test
  `filter?.RecordKeys != null`, not a length, so `recordKeys: []` always
  answered empty. Lowering the empty list to null made it answer every row
  instead. This corrects the ruling from the commit-2 fix batch that lowered
  an empty v0 list to "no filter".
- CultMesh.ResolveDefaultSelection used the same collapse to decide whether an
  explicit empty recordKeys should still fall back to the document type's own
  schema default; under the corrected Lower it must not.

Lower(null) still means "no filter"; Lower(non-null) is always a real
(possibly empty) array. The evaluator already treats a non-null empty Keys
array as "match nothing" (CultNetSelectionEvaluator.MatchesSchemaKeysFields);
only the v0 lowering was erasing that signal before selections reached it.

Also restores the pre-cut requested-key order for
CultNetDocumentRegistry.CreateRawSnapshotResponse: pre-cut, requested record
keys were iterated in the order the caller sent them (a HashSet<string> built
from the array, which enumerates in insertion order); the new shared
evaluator's constant ordinal fell back to schema-and-key sort order instead.
ordinalOf now ranks rows by their position in the requested key list when one
was given.

Rewrote CultMeshStreamingTests.FetchDocumentsAsync_WithExplicitEmptyRecordKeys
(renamed ...AnswersEmpty) to assert the corrected behaviour instead of the
ruling it used to pin. Added
CultNetDocumentRegistry_RawSnapshot_ExplicitEmptyRecordKeys_AnswersEmpty,
CultNetDocumentRegistry_RawSnapshot_KeepsRequestedRecordKeyOrder and
CultNetV0SelectionLowering_Lower_DistinguishesNullFromExplicitEmpty to
NetworkingTests.cs.

Not done in this commit: CultNetDatabase.CreateShardSnapshotResponse (the
shard-replication snapshot path) still orders by the evaluator's default
schema-and-key tiebreak. Its own pre-cut order was raw cache iteration order,
which the new evaluator has no equivalent for and shard replication does not
appear to depend on row order; flagged as a discrepancy rather than changed
without a test that would pin the "right" order. The side-by-side b3d9cf7 vs
HEAD comparison run the spec asks for was not executed in this pass; the
pre-cut behaviour above comes from reading CultNetDatabase.cs and
CultNetDocumentRegistry.cs at b3d9cf7 directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:13:31 +02:00
Meta Vens
8d7bee9346 Delete the retired hand-written mutation harness from this branch
Operator ruling, 2026-09-22: "Better to have nothing than a harness that
punishes refactoring." Removes scripts/mutate-dotnet.ps1 and every
scripts/*.entries.ps1 mutation-entry file for CultNet selection (networking,
mesh, caching, fix-batch-2). Also drops the two live code comments in
tests/GameCult.Mesh.Tests/CultMeshStreamingTests.cs that cited entry ids
(MESH-KEY-Loosening, MESH-DEFAULT-Loosening), rewording them to describe the
rule instead of the retired harness. Mutation testing now uses Stryker.NET on
the cut's diff, run by Soul.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:13:12 +02:00
Meta Vens
796433f2d4 Delete the mutation harness; keep the QUIC scenarios runnable directly
Better to have nothing than a harness that punishes refactoring
(operator ruling, 2026-09-22). Remove scripts/mutate-cultmesh.mjs
(1451 lines) and its Node dependency from the Linux QUIC dev image.

The behavioural coverage stays: native/GameCult.Mesh.Quic.Native's
scenario runner and its seams (waitseam, pollhammer, holdtimeout,
zerotimeout, closerace, and the rest) are untouched. The README and
the dev Dockerfile now document building with
-DCULTMESH_QUIC_BUILD_TESTS=ON -DCULTMESH_QUIC_DEBUG_ASSERTS=ON and
running each scenario as a direct argument to the built binary,
instead of through the harness.

docs/cultnet-selection-cut.md and docs/typescript-quic-realtime-cut.md
still name the harness; they are Self's campaign history and stay as
written.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:41:28 +02:00
Meta Vens
d0480a7d83 Record the operator's acceptance: Q-BP2 A, the title rule, R-D, the Q-J door
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:55:04 +02:00
Meta Vens
0714492858 CultNet selection cut1 fix batch, second Hands pass: R-D mutation entries and Mesh R-F/R-L coverage
- Repairs two stale mutation entries in
  scripts/mutate-cultnet-selection-caching.entries.ps1
  (CACHE-QJ-ExponentExpansion-Revert, CACHE-QJ-NaNInfinity-Revert): both
  anchored on ExpandScientificNotation/the pre-R-D switch arm, which the
  first fix-batch pass's R-D commit (7903853) deleted outright in favour of
  RenderExactFloat/RenderExactDouble. Replaces the exponent-expansion entry
  with CACHE-RD-ExactExpansion-Revert (reverts to .NET's shortest
  round-trip ToString(), the exact defect R-D fixes) and
  CACHE-RD-FractionSplit-Loosening (an off-by-one digit-split bug that
  still looks "exact"), and re-anchors the NaN/Infinity entry on the
  current RenderExactDouble call.
- Repairs one stale mutation entry in
  scripts/mutate-cultnet-selection-mesh.entries.ps1 (MESH-FOREIGN-Revert):
  anchored on the pre-R-L inline TryReadSchemaVersion call, which R-L's
  commit (c01bc0a) replaced with CultNetDocumentRegistry.PayloadMatchesSchema.
  Re-anchors it and adds MESH-RF-Door-Revert (R-F: EnsureV0Compatible runs
  CultNetSelectionValidation.ValidateShape before its own v0 transport
  refusals), with a new tests/GameCult.Mesh.Tests/CultMeshSnapshotOptionsTests.cs
  proving FetchSnapshotAsync refuses Keys=[]/[""] before ever touching a
  client.
- All entries verified: Caching 11/11 killed, Mesh 7/7 killed (pending final
  confirmation on this run), restores byte-exact.

R-L (docs/cultnet-selection-cut.md): the Python refusal of vN>0 already has
a real regression test (packages/cultnet-py/tests/test_cultnet.py:1958-1978,
unsupported_schema_version) but no mutation entry - mutate-dotnet.ps1 is a
dotnet/NUnit harness only and this repo has no Python mutation harness. R-L's
C#-adjacent piece (TryReadSchemaVersion going internal behind
CultNetDocumentRegistry.PayloadMatchesSchema) is covered by
MESH-FOREIGN-Revert above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:11:21 +02:00
Meta Vens
d94174d597 CultNet selection cut1 fix batch, second Hands pass: mutation entries and S9
- Adds mutation entries for R-A, R-B, R-C, R-E, R-F, R-G, R-H, R-J
  (scripts/mutate-cultnet-selection-networking.entries.ps1), each a revert
  and a loosening where the rule supports one, targeting
  CultNetSelectionEvaluator.cs, CultNetSelection.cs, CultNetDatabaseServer.cs
  and CultNetDatabaseSubscriptionServer.cs.
- Adds S9 (docs/cultnet-selection-cut.md section 10): a new RUDP round-trip
  test proving a hop-bearing v1 subscription reconciles the whole matched set
  when the citer (not the page row itself) changes, plus its own mutation
  entry (NET-S9-HopFastPath-Revert) reverting D6's Watch routing.
- Adds tests/GameCult.Networking.Tests/CultNetSelectionFixBatchTests.cs:
  regression coverage for R-B (edge anchor/order), R-C (code-point order in
  both the row tiebreak and the cursor position compare), R-E (alias
  matcher; door refusal), R-F (Select validates unconditionally), R-H
  (digest collision), R-J (numeric any_of canonical rendering) - none of
  these rules had a test that could fail independently of the fix-batch
  regression tests already in place.
- Fixes a real gap R-E named but the first fix-batch pass never wrote:
  CultNetSelection.Validate now refuses a cites.target.schemaId that
  matches no declared schema, at the door, instead of MatchesCitation
  silently treating an unresolved target as "not found" and answering an
  empty page. Resolved through CultNetSchemaAliasMatching, the reference
  rule, against every declared schema - not only the selection's own
  Schemas-filtered reachable set, which a second entry
  (NET-RE-DoorTargetReachableOnly-Loosening) proves: a cites target is a
  separate structural reference, not itself constrained by Schemas.
  Coordinator-assigned item; the shared parity fixture's
  refuses_unmatched_cites_target_schema vector (Rust side, another Hands)
  is not yet in selection-vectors.rs-written.json, so
  SelectionParityVectorTests.SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
  does not yet exercise it - it will once that vector lands, unmodified
  by this commit.
- NetworkingTests.cs gains CultNetDatabaseServer_AnswersV1SnapshotRequestRespectingItsFields
  (R-A's loosening) and DatabaseSubscriptionServer_SubscribesV1AndReconcilesAHopBearingSelectionWhenTheCiterChanges
  (S9 / R-A's subscribe-listener revert).

Discrepancy: R-A's snapshot-request v1 listener registration
(CultNetDatabaseServer's _server.OnCultNet(_snapshotHandlerV1)) has no
mutation entry. CultNetDatabaseServer pairs only with Server (the
LiteNetLib/TCP-secured production construction site), and no existing test
in this suite - before or after this pass - drives Server's real
client/listener dispatch for CultNetDatabaseServer; every test calls
CreateSelectionResponse/CreateSnapshotResponse directly. That revert is
recorded as not yet reached in the entries file, not swapped for an easier
mutant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:11:21 +02:00
Meta Vens
5646ff7351 Land the QUIC Cut 3 eighth fix batch: release config, whole-duration seam, pollhammer live objects, zero-timeout bound, closerace starvation fix
Self's ruling of 2026-09-22 named six findings against Soul's eighth pass.
This lands the code side of all six:

- The wait-record seam (CULTMESH_QUIC_DEBUG_RECORD_WAIT) now wraps the whole
  duration handed to `wait_for`, not the raw `timeout_ms` before it is
  converted to milliseconds, so arithmetic applied after that conversion can
  no longer live outside what the seam records. Its "unset" sentinel moves to
  a separate `debug_wait_recorded` flag so a recorded -1 could never be
  confused with nothing recorded.
- `pollhammer` also runs against a runtime holding a live connection to an
  unroutable address (RFC 5737 TEST-NET-1), a best-effort stream on it, and a
  real (never-connected) listener, closing the gap where the wait's predicate
  was only ever exercised on an empty runtime.
- A new `zerotimeout` scenario pins that `timeout_ms <= 0` returns within
  50 ms, probing 0, -1 and INT32_MIN.
- `closerace`'s settle no longer sleeps a guessed duration: where the
  development seam exists it waits for `debug_peak_calls` to reach the poller
  count (reset before any poller thread exists, so nothing can zero a count a
  poller already raised) before a short fixed margin closes the last gap
  between being counted and being inside the condition wait.
- `kGenerousLateToleranceMs` narrows from 800 ms to 400 ms so the wall-clock
  scenarios can catch arithmetic applied outside the seam's own argument,
  which `waitseam`'s equality check structurally cannot see.

scripts/mutate-cultmesh.mjs gains a plain `release` configuration on both
platforms (CULTMESH_QUIC_DEBUG_ASSERTS=OFF, CMAKE_BUILD_TYPE=Release) running
`polltimeout`, `pollbusy`, `pollhammer` and `zerotimeout`, and seven new
mutation entries: R1 (the release macro divides the duration by 10), W1-W3
(600 ms added, a 500 ms floor, and a 1000/909 scale, each applied outside the
seam's own argument), P1 (the idle predicate also ends on a live connection),
and Z1-Z2 (a 500 ms sleep and a 300 ms spin on a non-positive timeout). Two
stale comments are corrected: the scenario list above `assertScenarios` now
names `pollhammer` among the clock-sensitive entries, and a historical "1300"
probe value that no longer matches the test file is removed. The header's
section 7 intro now says the seam serves both the quiesce and the wait
recording, not quiesce alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:27:56 +02:00
Meta Vens
a1309c638c Record the Rust alias port: row-for-row with the reference
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:21:08 +02:00
Meta Vens
a31993d518 CultNet selection: update mutation entries for the R-E rewrite
The alias-matching call sites changed shape (schema_alias::matches/
matches_any now take (candidate, schema_id, schema_name)), so the entries
that edit them needed their Old/New anchors and Test names updated:
RS-E-Alias-Revert (now targets the ported name-comparison line, not the
deleted hash-stripping compare), RS-E-CitesTarget-Revert, and
RS-E-EmptySchemasNeverMatchesEverything-Revert.

New entries for this cut's rule, per spec:
- RS-E-Alias-RevertToHashStripping: the revert - go back to hash stripping
  (compare schema_id against schema_id's own inferred name, the deleted
  matcher's behaviour).
- RS-E-Alias-CaseInsensitive-Loosening: the loosening - compare
  case-insensitively instead of Ordinal.
- RS-E-Alias-AcceptAnyVersionSuffix-Loosening: a second loosening - accept
  any ".vN" suffix without comparing the name.
- RS-E-InferSchemaName-StripAnyLastDotSuffix-Loosening: a function-of-input
  mutant - infer_schema_name strips any suffix after the last dot, not only
  a ".v<digits>" marker.

Also fixed the invocation: File/Command values are relative to the CultLib
repo root and carry --manifest-path packages/cultnet-rs/Cargo.toml (there is
no repo-root Cargo.toml/workspace), matching -Repo C:\wal and -Target
'packages/cultnet-rs/src/selection.rs,packages/cultnet-rs/src/snapshot_query.rs'.

Verified: M0 green (4 commands, 36+81+7+1 tests), all 40 entries killed by
their own test, every restore byte-exact (git diff clean against the
committed source after the run).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:19:12 +02:00
Meta Vens
8ef170b847 Record the early Soul pass's new findings and rule fix batch 3
Refusals become wire messages, cursors are keyed, one id reaches the
authorizer, and v0 lowering preserves v0's pre-cut answers exactly,
correcting the earlier ruling that lowered an empty list to no filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:06:11 +02:00
Meta Vens
7a9fabfa2c CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher
Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an
alias is a schema *name* with a trailing .vN suffix, resolved against the
descriptor's declared name - never against a hash id. C#'s production call
sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use
the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare
id-vs-id overload.

- Row gains schema_name() (defaults to schema_id() for rows with no separate
  name, e.g. raw wire records); RowSet gains schema_name(schema_id) ->
  Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which
  this crate has no type for (D7).
- schema_alias::matches/matches_any now take (candidate, schema_id,
  schema_name) and resolve a candidate's inferred name against the declared
  name, exactly as the C# descriptor overload does - not against schema_id's
  own inferred name, which is what the deleted hash-stripping matcher did.
- Every call site (reachable_schemas, the cites.target door check,
  matches_schema_keys_fields's D6 fast path, matches_citation) now resolves
  and passes a schema's name instead of comparing two schema-id strings.
- Deleted: the hash-stripping schema_alias::matches(candidate, schema_id)
  two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form
  either runtime's production rule ever resolves.

Behaviour verified against a standalone run of the exact C# algorithm
(CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id
match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all
convention, ordinal case sensitivity, ".v" with no trailing digits, and
multiple ".v" markers (last one wins) - every case is now a Rust unit test
in schema_alias::tests.

Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries
are now a shared negative check (neither runtime resolves them), and the
nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime
defect (schemas_by_name_alias). Regenerated
contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16
vectors): added refuses_cites_target_by_hash_shaped_alias, renamed
hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias.

cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two
failures are both the pending unmatched-cites.target door refusal
(CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the
other Hands' C# door fix covers both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
Meta Vens
9b88cb4e5c Record Soul's eighth pass: the release build never runs the rule it ships
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:43:50 +02:00
Meta Vens
722859550d Record the shared fixture and rule the alias defect: the C# reference's rule is the rule
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:34:57 +02:00
Meta Vens
defb9a110f CultNet selection cut1 Rust: build from the shared parity fixture
Companion to the C# side's shared-fixture commit. Replaces the
invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts
with lazily-loaded values read from
contracts/cultnet/interop/selection-vectors.fixture.json (a
std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/
declared_roles/is_numeric/target_leaves become runtime string
comparisons instead of const match patterns). NARROW_CITER stays a
local, unshared id - it never appears in a cross-runtime vector.

base_rows() keeps S2-S23's original 7-row set (now sourced from the
fixture's first 7 rows, unchanged in value or order, so every test
that enumerates it - orders_by_ordinal_then_identity_and_reverses,
pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor,
matched_is_the_total_count_not_the_page_count, ... - still holds
unchanged). A new all_fixture_rows() carries all ten rows (the
fix-batch float-tie and astral/BMP rows too) for the two parity-vector
functions only.

write_selection_vectors_for_the_reference gains float_tie_exact_ge,
astral_key_code_point_order, and the fixture's hash alias
(leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias
resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form
only CultNetSchemaAliasMatching's descriptor overload resolves)
probes, mirroring the C# side. selection_vectors_written_by_the_
reference_decode_and_evaluate_identically_in_rust no longer panics on
the first disagreement (evaluate_vector now returns a Result instead
of .expect()'ing); it collects every vector's mismatch and fails once
with the full list, matching the C# reader's same change.

Regenerates selection-vectors.rs-written.json from the shared fixture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
Meta Vens
3317dddf46 CultNet selection cut1: the shared C#/Rust parity fixture
Reconciles the two Hands' independently-regenerated parity vectors
(Soul's whole-cut pass, F3): the C# fixture used schema *names*
("leaf_a") as literal ids, the Rust fixture used invented fake
sha256-shaped ids - every vector carrying an id failed when read by
the other runtime.

Adds contracts/cultnet/interop/selection-vectors.fixture.json: the one
row set both writers now build from, keyed by each schema's real
SHA-256 content-hash id exactly as CultDocumentRegistry computes it
(dumped via a temporary DumpFixtureSchemaIds run). Carries the rows
either runtime needs - astral/BMP-private-use keys, a float tie,
cites/cited, a cites target reached by alias - plus two DIFFERENT
alias forms, because the two runtimes' alias matchers key off
different descriptor attributes and no single candidate resolves
through both (see the fixture's "//aliases" note and the report).

Rewrites SelectionParityVectorTests.cs to load the fixture instead of
hand-building rows (BuildFixture/BuildExtendedFixture deleted), assert
the fixture's declared schema ids still match what the registry
computes (fails loudly on drift instead of silently comparing wrong
ids), and use the real schema id (not SchemaName) as a row's
cross-runtime identity. Also fixes a real bug this file carried since
before the shared fixture: both the writer and the reader compared
`evaluation.Rows.Count` (the page size) against "matched" instead of
`evaluation.TotalMatched` (the total, per R-G) - selections with a
Limit were writing/checking the wrong number. The reader now collects
every vector's mismatch instead of stopping at the first one, so a
real parity defect is reported in full.

Regenerates selection-vectors.cs-written.json from the shared fixture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:29 +02:00
Meta Vens
e3dbb640f7 Correct the f32 3e20 example: the Rust Hands was right
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 10:27:57 +02:00
Meta Vens
a088435f9f CultNet selection cut1 Rust fix batch: mutation entries for R-B/D/E/F/G/H/I
Adds a revert (and loosening, where the rule has one) per new rule to
scripts/mutate-cultnet-selection-rust.entries.ps1, and repairs three things
the entries file's own first run against this fix batch found:

- The RS-QJ-ExponentExpansion-Revert entry targeted
  expand_scientific_notation, which R-M deleted; replaced by entries
  against R-D's real replacement (the mantissa/exponent bignum).
- RS-D-ExponentBranch-Revert's first draft ("New = if false") hung: it
  routes every value, including f64::MAX's exponent of ~971, into the
  fraction branch, whose "k = (-exponent) as usize" wraps a positive
  exponent's negation into a huge usize and spins effectively forever - the
  run had to kill that process by PID. "New = if true" is the safe
  direction (a negative exponent's "for _ in 0..exponent" is just an empty
  range, not a cast) and is pinned against a negative-exponent test
  instead.
- RS-V0Lowering-Revert (pre-existing, not from this batch) unexpectedly
  survived the first run: matches_schema_keys_fields's move to
  schema_alias::matches_any (R-E) picked up that helper's own "empty
  candidates matches everything" shortcut, masking the v0-lowering
  mutation this entry exists to catch. Fixed in the prior commit
  (matches_schema_keys_fields no longer uses matches_any's shortcut); a new
  entry, RS-E-EmptySchemasNeverMatchesEverything-Revert, pins the fix
  directly rather than only through v0's own lowering.

All commands are narrowed with "-- --skip
selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust":
that vector-file test is legitimately pending the parallel C# Hands's own
regenerated cs-written.json (this suite does not touch that file), the same
carve-out this file already gave the pre-existing
reactive_document_coalesces_direct_same_schema_alias_member_writes flake.

Full run against this batch's final state: 36/36 entries killed, M0 green
on both commands, no survivors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:11:31 +02:00
Meta Vens
4f81c255a3 CultNet selection cut1 Rust fix batch: R-M deletions
- contracts.rs: replaces the ~335-line hand-written require_*/encode_*
  codec for SelectionDocumentRecord/RawDocumentHeader/Edge/RecordRef with
  two generic helpers (require_typed/encode_typed) that go through
  rmp_serde::to_vec_named/from_slice and rmpv::Value's own Serialize -
  these four types already derive Serialize/Deserialize with the wire's
  camelCase field names. rmpv::ext::to_value/from_value was tried first and
  rejected: it serializes a struct positionally (one array slot per field
  in declaration order), which breaks the moment a skip_serializing_if
  field is actually skipped and is not the named-map encoding this wire
  always uses. Proven both ways by
  tests/selection.rs's snapshot_response_raw_v1_round_trips_* tests, which
  round-trip real headers, documents-with-payload, and edges with and
  without a payload through the real cultnet.snapshot_response_raw.v1
  message envelope.
- snapshot_query.rs: deletes reject_duplicates and its two call sites -
  dead, because lower_v0_list already dedups (via its own HashSet) before
  reject_duplicates ever runs, so its duplicate check can never fire.

(RawDocumentHeader::from_document, the Eq derives on the selection wire
types, and two dead branches the mutation harness's own first run proved -
exact_decimal_from_mantissa's redundant "if mantissa == 0" guard and
big_mul_small's unreachable multi-digit carry loop - are selection.rs
changes already covered by the prior commit in this batch.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:11:31 +02:00
Meta Vens
8b114ed353 CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I
The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's
rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs.

- R-B: hop edges now follow the hop's direction - cites keys an edge off its
  From (the citer), cited off its To (the citee), each in page-row order
  then (from, role, to) code-point order. Before this, both hop kinds keyed
  off From, so a cited selection's edges were silently empty unless the
  citer also happened to be on the page (Soul's F2). build_incoming_index
  iterates the caller's own row slice, never a HashMap, for its own order.
- R-C: code-point order was already Rust's default str::cmp (UTF-8 byte
  order); no change needed there.
- R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754
  decimal expansion from mantissa and exponent, via a small fixed-point
  bignum (~60 lines), replacing the previous shortest-round-trip-form path
  that rounded ties differently from .NET. expand_scientific_notation and
  its test are gone (R-M); the Display probe test is kept per the map.
- R-E: schema_alias (ported from CultNetSchemaAliasMatching's string
  overload) is the one schema-identity rule for `schemas` and
  `cites.target.schemaId`; an unmatched cites target is refused at the
  door. Found and fixed along the way: matches_schema_keys_fields must not
  use schema_alias::matches_any's own "empty candidates matches everything"
  shortcut (correct for the door/reachability call sites it mirrors from
  C#) - v0's snapshot server calls matches() directly, without validate(),
  so an empty-but-present schemas list must still match nothing there too.
- R-F: select() validates first and returns the typed refusal; matches()'s
  hop guard is a real assert!, not a debug_assert! a release build would
  compile out.
- R-G/P-1: Evaluation and SelectionPage both carry matched, the total match
  count, not the page count.
- R-H: Cursor::compute_digest is length-prefixed for every string and list,
  closing the collision Soul found (["a|b"] and ["a","b"] used to digest
  the same).
- R-I: select_page projects a selection into the wire's SelectionPage -
  headers or documents under Selection::projection, matched, and edges; a
  header projection carries no payload, in edges too (S20).

Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the
old fixture's schema names doubling as ids, which had silently masked the
R-E gap (vectors passed only because "leaf_a" happened to already be both
the alias and the id).

Two documented discrepancies against the map's own worked numbers, both
verified independently outside this crate (Rust bit decomposition plus a
Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the
map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact
expansion, not "0.0000001" (a stale shortest-form assertion this fix batch
did not originally touch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:11:31 +02:00
Meta Vens
0ab91d0298 Record the C# half of the selection fix batch, and what it left undone
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:42:17 +02:00
Meta Vens
3d32c67821 CultNet selection cut1 fix batch: regenerate the C#-written parity vectors
Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json
under R-B/R-C/R-D/R-E/R-I.

SelectionParityVectorTests.cs: the six original cases' fixture
(BuildFixture) is left untouched, because SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
reads the already-committed selection-vectors.rs-written.json against
it, and a new row reachable by an unfiltered or loosely-filtered case
(descending_with_limit, gt_numeric_boundary) would silently change that
case's expected page. The fix-batch rows and cases live in a new
BuildExtendedFixture/Cases set, additive over BuildFixture, used only
by WriteSelectionVectors:
- float_tie_exact_ge: a row at 2233759.25f, on the round-to-even tie
  R-D's exact BigInteger rendering fixes (its canonical decimal is
  "2233759.25" regardless of which shortest form a runtime's ToString
  would have picked).
- cites_target_by_alias: cites.target.schemaId given as "leaf_a"
  (ParityLeafA's SchemaName) against a-eq's real content-hash SchemaId,
  exercising R-E's alias-matched cites (the pre-fix code required an
  exact SchemaId match and this always missed).
- astral_key_code_point_order: two rows sharing one ordinal, keyed by
  a BMP private-use character (U+E000) and an astral character
  (U+1F602, surrogate pair 😂), pinning R-C's code-point
  tiebreak order.
- header_projection/document_projection: the same hop-bearing match set
  under each projection.

Known-expected failure: SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
fails against the currently-committed selection-vectors.rs-written.json
(incoming_exists_true's edge count) because that file predates R-B and
was not touched here - the parallel Rust Hands owns it. Re-run once it
regenerates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:40:18 +02:00
Meta Vens
c01bc0a129 CultNet selection cut1 fix batch: R-L (contract, Python, mutate-dotnet.ps1)
- cultnet.selection.schema.json: schemas/keys gain minItems: 1 (present
  and empty is refused at the door, never "every schema/key" - omit the
  field instead); limit documents that it is clamped 1..=200 by the
  answering server rather than refused outside that range.
- cultnet_py/interop_peer.py: handle_server_message refused only an
  exact ".v1" suffix, so a v2+ selection message fell through to a
  silent empty-list answer instead of a loud refusal. is_unsupported_cultnet_version
  matches any cultnet.*.vN with N > 0 instead. test_cultnet.py's R-2
  test now covers v2 and v10, not only v1.
- mutate-dotnet.ps1: an entry's File (from -Entries, often written with
  "/") is looked up against the $targets dictionary keyed by -Target's
  own spelling; a "\\" vs "/" mismatch didn't throw at the lookup -
  $targets[$file] silently returned $null - so the crash surfaced one
  call later as a null-argument error inside a .NET path API instead of
  as "unknown target". Both -Target and every File read from an entry
  now normalise through Normalize-TargetPath before indexing $targets.

Discrepancy: the ruling names "both R-2 sites" for the Python fix; only
one site with this exact narrow-match bug exists in packages/cultnet-py
and packages/cultmesh-py (cultmesh_py/server.py's handle_message already
refuses every unrecognised schemaVersion, v1+ included, through its own
default branch). Fixed the one that exists; did not invent a second.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:53 +02:00
Meta Vens
915262fee2 CultNet selection cut1 fix batch: R-I/K/D tests and R-A/R-M reflection fix
- R-I: new CultNetSelectionPageTests.cs kills Soul's two named S20
  mutants - dropping `wantDocument &&` in ToEdge (a header-projection
  edge would carry document payload bytes anyway) and always emitting
  Edges instead of `selection.HasHop ? ... : null` (a hop-less
  selection's page would carry an empty array instead of no Edges
  member) - plus a matched-vs-page-length test for R-G. Fixing
  ToEdge's mutant coverage surfaced a real bug the new tests caught:
  it serialized an edge payload through CultDocumentMessagePackSerialization,
  which requires the payload's type to be a registered CultDocument
  and throws for a plain declared value type (float, in the fixture);
  it now serializes directly through MessagePackSerializer.
- R-D: CultNetCanonicalNumberTests gains "5\n"/"5\r\n" cases - .NET's
  $ matches immediately before a single trailing newline even without
  RegexOptions.Multiline, so the door's pattern is re-anchored with \A/\z
  and would have accepted "5\n" as canonical without this. Caching's
  TryGetIndexNumberRewritesADoublesExponentNotationToPositionalDigits
  is updated for 1e-7's actual exact decimal expansion (the R-D commit's
  own effect): the old assertion pinned "0.0000001", the shortest
  round-trip form a pre-R-D render would have produced.
- R-K: every serverThread.Join() in NetworkingTests.cs that sat outside
  a try/finally now sits inside one, so a failed assertion cannot leave
  the poll thread running past the point where `using` disposes the
  socket it polls (an ObjectDisposedException on that raw background
  Thread was a host crash, not a test failure). The bodyDemand.HasConsumers
  assertions wait on the condition instead of assuming
  SubscribeAsync's completion and PublishDemand happened in an order
  a network round trip does not guarantee.
- Reflection fix: DatabaseSubscription_FiltersCanonicalWireChangesByRequestedSchemaAlias
  built a v0-shaped CultNetDatabaseSubscribeMessage to invoke
  CreateMatchedRecord by reflection; that method now takes the shared
  SubscriptionRequest (R-A), so the test builds one through the same
  reflection path instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:36 +02:00
Meta Vens
790385364c CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.

Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
  reports against (the citer under cites, the cited row under cited),
  not filtered by "From is on the page" regardless of direction; order
  is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
  string.CompareOrdinal in the row tiebreak, cursor position and edge
  order, so an astral key sorts the same way here as it does under
  Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
  its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
  never .NET's shortest round-trip form, which rounded a tie the
  opposite way from Rust at some values. The now-dead ExpandScientificNotation
  is deleted.
- R-E: MatchesCitation compares a cites target through
  CultNetSchemaAliasMatching instead of an exact SchemaId string
  compare; ToEdge and ToRawRecord now emit the same wire schema id
  (WireSchemaId) for the same row, where ToEdge previously used the
  descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
  - the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
  is gone, so no public evaluation path can skip the door. Mesh's
  EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
  (the declaration-independent half of the door) before its own v0
  transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
  SelectPage's Matched field now reports it instead of the page's row
  count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
  (v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
  which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
  projects every matched row, instead of looping SelectPage per 200-row
  page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
  (AppendString/AppendList) instead of delimiter-joined, closing the
  collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
  rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
  fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
  instead, the one binding-alias reconciler. The duplicated v0/shard
  lower-and-page loop is gone with SelectAll above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:39:04 +02:00
Meta Vens
451ecc2d5e Record the whole-cut Soul pass: Cut 1 does not merge, and rule the fix batch
The C# reference never serves v1, cited returns the wrong edges in both
runtimes, and the runtimes disagree on floats, schema aliases and ordering.
Floats render as their exact decimal expansion, flagged for the operator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 08:12:05 +02:00
Meta Vens
a704a77ec3 Correct the orphan-rule claim, and add two pre-merge items for Huginn
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:49:05 +02:00
Meta Vens
070fac062d Record the seventh fix batch: the wait is observed where it is decided
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:04:56 +02:00
Meta Vens
0a49f869f2 Record selection commit 4: parity vectors, and the hop they leave out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:57:24 +02:00
Meta Vens
00f4c02dd7 CultNet selection cut1, commit 4: the parity vectors, and the ruling this branch landed under
Two things landed on this branch while commit 3 was in flight, both addressed
here rather than left for the vectors to paper over:

**Self's ruling, 2026-09-22 (commit 2 fix batch, 568e8e3/021e3f1): empty
lists, blank entries, and v0's own cleaning.** Rust already refused an empty
schemas/keys list (inherited from before the ruling); it now also matches
the reference's three further decisions exactly, not just in spirit:

- The door's refusal field is the plain list name ("schemas"/"keys"), not an
  indexed "schemas[i]" - selection::validate's SelectionInvalid.field now
  matches CultNetSelectionInvalidException("schemas", schema, ...) literally,
  because the parity vectors compare this string across runtimes.
- snapshot_query.rs's lower_v0_list now deduplicates a mixed list the same
  way CultNetV0SelectionLowering.Lower does (Distinct(StringComparer.Ordinal)
  - order-preserving, first occurrence wins), not just blank-filters it.
  A new test (serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter,
  extended) pins the dedup case directly.
- Two mutation entries (RS-EmptySchemas-Revert, RS-EmptyKeys-Revert) added
  for the door's original empty-list refusal, which had tests but no
  mutation coverage before this commit.

**S12: the two-way parity vectors**, contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json,
eight vectors each: the six evaluated selections from commit 3 (any_of+ge
conjunction, descending+limit, gt boundary, cited exists true/false, three
rows sharing one index value) plus two door-refusal vectors added for this
ruling - `refuses_empty_schemas_list` (schemas: []) and `refuses_blank_key_entry`
(keys: ["a-eq", "   "]) - which decode their selection bytes and assert
`validate`/`Validate` refuses at exactly the named field, never evaluating.
The blank-key vector could not be generated before this commit: the door
rule it exercises did not exist in the reference until 568e8e3 landed.
`cites`-based hop vectors are deliberately absent (documented in both test
files): MatchesCitation compares Citation.Target.SchemaId against the
reference's real SHA-256 schema id exactly, with no SchemaName fallback,
and this fixture pair has no way to agree on that hash across runtimes;
`cited` (role/existence, matched by record key alone) is the hop direction
both sides can cross-check, so incoming_exists_true/incoming_negation carry
that coverage instead. Both reader tests pass:
SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference (C#)
and selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust
(Rust), each judging the other runtime's file.

**The remaining mutation matrix.** Of the coordinator's named set (S1, S2,
S6, S9-S15, S17, S20, S23): S1 was already covered (RS-EmptySchemas/Keys,
RS-BlankSchema/Key); S2 (any_of matches membership, RS-S2-Revert) and S6
(cites.role filters the hop, RS-S6-Revert) are newly covered - S6 needed a
new fixture (cites_role_excludes_a_second_reference_at_the_same_target)
because base_rows()'s citer-1 only ever carries one reference, so a mutant
dropping the role filter had nothing else to wrongly match and survived
against the original hops_one_edge_by_declared_role test; that finding is
recorded in-line as a comment, not silently swapped for an easier mutant.
S9-S15 (subscription reconcile, v0-lowering-of-subscribe) do not apply:
D7 - no Rust caller subscribes in this cut, so there is no subscription
client to mutate. S17 (inherited alias reaches every leaf) does not apply
either: Rust's toy Row/RowSet has no inheritance to begin with - both
leaves in the fixture declare "mass" independently, which is the Rust
spelling of the same guarantee, already exercised throughout the suite
without a dedicated mutant (there is no shared base-type discovery path to
revert). S20 (edges only for a hopping selection) was investigated and
found structurally vacuous in Rust as built: `edges` is only ever populated
inside the cites/cited branches, so a mutant on the has_hop() gate cannot
produce a different observable result for a non-hopping selection - no
entry added; a real gap would need the evaluator's structure to change
first. S23 (matches every row sharing an index value, never a cache's
unique-index winner) has no revertable bug surface: Rust's Row/RowSet
never had a unique-index map to begin with (D7), so this is pinned by
construction, not by a mutation-testable rule; matches_every_row_sharing_an_index_value
still exists as a positive test naming it.

**A pre-existing, unrelated flaky test, found and worked around, not
fixed.** tests/cultnet.rs's reactive_document_coalesces_direct_same_schema_alias_member_writes
fails consistently when the mutation harness runs the full tests/cultnet.rs
binary (both default-parallel and RUST_TEST_THREADS=1), but passes 5/5 in
isolation and passes in a plain `cargo test -p cultnet-rs` run - state
pollution from another test in that binary under the harness's specific
invocation path, not a race this campaign's code participates in (nothing
here touches reactive_document machinery). RS-V0Lowering-Revert's Command
is narrowed to `cargo test --test cultnet serve_read_only_raw_snapshot`
(filtering both M0 and the entry's own run to this campaign's test) so the
suite does not depend on that unrelated test's health; the flake itself is
reported here as a finding, not patched.

Tests: cultnet-rs 174 passed, 0 failed (default parallel run).
GameCult.Networking.Tests 196 passed, 1 skipped (WriteSelectionVectors,
env-gated), 0 failed.

Mutation harness: 24 entries (was 22 in commit 3; +RS-S2-Revert, +RS-S6-Revert;
RS-BlankSchema/BlankKey and RS-V0Lowering-Revert's anchors and field-name
assertions updated to match the landed fix batch's exact spelling), all
KILLED, M0 green.

Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty.

Structural delta, this commit only: 6 files changed, +108/-23 (selection.rs
+15/-9 field-name fix; snapshot_query.rs +25/-14 dedup; tests/cultnet.rs +21
dedup test; tests/selection.rs +35/-2 S6 fixture and field-name fixes;
scripts/mutate-cultnet-selection-rust.entries.ps1 +33/-4 two new entries
plus three corrected anchors/commands; SelectionParityVectorTests.cs +2/-1
field name), plus the two committed vector JSON files (~90 lines each, new).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 06:55:52 +02:00
Meta Vens
d9629a8e26 Record the selection commit 2 fix batch and Rust commit 3
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:17:54 +02:00
Meta Vens
021e3f1bba CultNet selection cut1, commit 2 fix batch: R-2, Python refuses v1 selection messages
R-2 was owed by Cut 1 and not yet done (docs/cultnet-selection-cut.md, section
9, Self's rulings 2026-09-22): a v0-only Python peer must refuse a
cultnet.selection.v1 carrier message loudly instead of silently answering
with an empty list.

- packages/cultnet-py/src/cultnet_py/interop_peer.py:
  handle_server_message's fallback now recognises any cultnet.*.v1 schema
  version and replies cultnet.error.v0 { code: "unsupported_schema_version",
  details: { schemaVersion } } via the new unsupported_schema_version_error
  helper, matching cultmesh_py's existing error shape. One test
  (test_python_interop_peer_refuses_v1_selection_messages) proves a
  snapshot_request.v1 and a database_subscribe.v1 message both get the
  refusal, not an empty list.
- packages/cultmesh-py/src/cultmesh_py/server.py's delegating path
  (_handle_connection_message -> handle_message) already refused every
  unsupported schema version generically, v1 included - no code change
  needed there. test_cultmesh_local_server_returns_errors_for_bad_requests
  gains an explicit assertion for a snapshot_request.v1 message through that
  same path, so the coverage is named rather than only implied by the
  generic case.

This is the one exception to "do not touch packages/" in this fix batch: only
these two Python packages' interop/server files and their tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 06:13:16 +02:00
Meta Vens
568e8e356f CultNet selection cut1, commit 2 fix batch: door, shard-via-evaluator, Mesh v0 refusal, dead Reconcile code, mutation harness crash handling
Soul found commit 2 didn't pass (docs/cultnet-selection-cut.md, 2026-09-22):
empty/blank schemas-keys still reached the evaluator, the shard snapshot ran
its own loop instead of the evaluator, Mesh's v0 wire silently dropped v1-only
selection terms, the Mesh one-pass read could return the wrong schema's
record at a shared key, two decode rules for a foreign schema id disagreed,
dead identity-comparison code from the _projectRecord removal was still live,
and a disposed-socket race could abort a mutation run instead of recording
NO VERDICT. This is Self's fix batch for all of it.

- CultNetSelection.Validate now refuses a blank/whitespace entry inside
  schemas or keys, not just an empty array. CultNetSelectionEvaluator no
  longer treats [] as "no filter" (the door already refuses it before
  evaluation). CultNetV0SelectionLowering is the one place that lowers an
  empty or all-blank v0 list to null; CreateRawSnapshotResponse and
  CreateShardSnapshotResponse both route through it, and CultMesh's own
  Clean() now delegates to it instead of duplicating the rule.
- CreateShardSnapshotResponse no longer carries a private loop or its own
  schema/key matching: it builds a CultNetSelection and answers through
  CultNetDocumentRegistry.SelectPage (the same evaluator path as the
  non-shard snapshot), with the shard's own membership check as a row
  filter. Binding-alias expansion now runs on both paths identically.
- ResolveDefaultSelection decides "no schema filter" from the cleaned
  recordKeys, not the raw array length. OverlaySelection stops deciding what
  a missing override means: any explicit schemaIds/recordKeys override
  replaces the default selection wholesale; no override keeps the default
  untouched.
- CultMeshSnapshotRequestOptions.Selection is now checked by
  EnsureV0Compatible before every send: fields, cites, cited, limit, cursor,
  descending, and any non-default projection are refused typed
  (CultNetSelectionInvalidException), never silently dropped from the v0
  wire message. Mesh sending v1 is FU-Mesh-v1, not this cut.
- CultMeshHotBodySubscription.Selection returns a defensive copy on every
  read so its arrays can't be mutated out from under the stored selection.
- ReadDocumentFromSnapshotResponse now searches candidates at one recordKey
  in one order - exact schema, then alias, then a foreign schema id decoded
  from the payload - instead of one OR'd condition where a case-insensitive
  or structurally-lucky candidate could win first. The foreign-id decode
  reads the payload's embedded schema version with the registry's shared
  CultNetDocumentRegistry.TryReadSchemaVersion and alias-matches it against
  the caller's own descriptor directly, rather than resolving to "whichever
  registered type owns this schema string" - that resolution is ambiguous
  when two document types alias the same schema id (MeshNoteDocument and
  MeshNoteAliasDocument in the existing fixtures), which is what broke on
  the first pass; TryResolveDescriptorByPayloadSchema stays for its existing
  caller (ResolveDescriptorForRawDocument) with a note on when not to use it.
  DecodeSnapshotDocuments and the Mesh copy of the payload-schema decode
  (RawSnapshotPayloadMatchesSchema/TryReadSchemaVersion) are deleted in
  favour of the shared one.
- CultNetDatabaseSubscriptionServer: the dead RecordKey/SchemaId identity
  comparisons in Reconcile and ApplyProjectedChange, and the duplicate-key
  throw in CreateProjectedSnapshot, are deleted - they can no longer fire
  now that a projection's dictionary key is always the record's own key.
  A new test flips authorizeRecord per key while the whole request stays
  authorized (so DemandActive never toggles) and proves both halves of the
  remaining diff: the removal of a now-unauthorized key and the add loop
  picking up a newly-authorized one.
- Every NetworkingTests.cs test that starts an IsBackground poll thread now
  joins it before its server disposes (about 20 tests; one already did).
- scripts/mutate-dotnet.ps1: a dotnet-test-host crash writing to stderr no
  longer throws the whole run away under PowerShell 5.1's
  $ErrorActionPreference = 'Stop' (2>&1 on a native command wraps stderr in
  a terminating NativeCommandError); Run-Tests catches it and the entry loop
  records NO VERDICT instead of aborting every remaining entry.
- Two probe tests: CultNetDatabase_ShardAndNonShardSnapshot_Agree{On
  UnfilteredRequest,OnBindingSchemaIdOverride} prove the shard and non-shard
  snapshot paths agree, including shard-membership exclusion of a document
  outside the shard's key prefix and a binding schema-id override matching
  on both paths.
- Mutation entries updated/added to match: the stale NET-D4-FourthMatcher
  entry (its anchor was the private loop this batch deleted, not refactored)
  is replaced by NET-D4-ShardMembership-Revert; the Mesh entries file's
  header miscount ("two of three" loosening entries when there are only
  two) is corrected and both previously-surviving loosening mutants
  (MESH-DEFAULT-Loosening, MESH-KEY-Loosening) now have killing fixtures;
  scripts/mutate-cultnet-selection-fixbatch2.entries.ps1 is new, covering
  Reconcile's remaining two mutants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 06:13:15 +02:00
Meta Vens
b8fad13867 Rewire the QUIC harness's native entries onto the wait seam
cultmesh_quic_native.cpp's wait_for callsite now reads
std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)) instead
of std::chrono::milliseconds(timeout_ms), so every existing entry anchored on
the old spelling is repointed at the new one, with the loosening's derivation
placed inside the macro's argument so the seam's record and the wait's own
duration stay the same evaluation.

Soul's seventh pass named eight derivations that were each the identity at
every wall-clock probe the sixth pass's tolerances could afford: a floor at 50
with no early check, a 16 ms and a 40 ms quantum, a wait that doubles past a
runtime's twelfth poll, a clamp at 7275, an offset of 25, a floor of 40, and a
6% scale. All eight are new entries here, killed by waitseam's exact mismatch
rather than by timing.

The seam itself gets a revert (it never records, so waitseam reads -1
forever) and a loosening (the record moves to an unconditional statement
ahead of the branch, reading the host's raw argument on every call instead of
only what the bridge hands its wait — indistinguishable at every value that
reaches a real wait, and caught only at timeout_ms <= 0, where the bridge's
own guard never waits and the honest seam must stay untouched).

S1's two predicate survivors get names and entries: K1 gives up after 64
wakes, K2 ends the wait on any wake once runtime->error is non-empty. Both
are new pollhammer targets; the existing "no predicate" revert is S1's
"returns on any wake" and needed no new entry, only its anchor fixed. S5 gets
a second hold-guard entry at the stated 1500 ms ceiling, alongside the
existing 1000 ms one.

assertScenarios gains waitseam and pollhammer.

Every added/changed entry was hand-verified against a manual win32-x64 build
before this commit: waitseam killed the seam's revert, its loosening, and a
representative +25 ms offset by exact mismatch; holdtimeout killed the S5
1500 ms guard by a 1.4 s overshoot; pollhammer killed K1, K2 and the
no-predicate revert within tens of milliseconds. Each mutation was applied by
hand, run, and hand-reverted with a diff against the working tree before the
next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:37:05 +02:00
Meta Vens
66830e92be Record the wait, not its shadow: a dev-only seam for cultmesh_quic_next_event
Self's ruling of 2026-09-22 called it after seven Soul passes: a wall-clock
probe against cultmesh_quic_next_event's timeout can never tell the host's
argument from a clamp, floor, round, scale or later-poll fault that happens to
be the identity at the probe's own value, because there is always another
function that matches. Move the observation to where the rule is decided.

Add cultmesh_quic_debug_last_wait_ms / _reset_last_wait_ms, built exactly like
the existing hold seam: gated behind CULTMESH_QUIC_DEBUG_ASSERTS, absent from a
shipped build. CULTMESH_QUIC_DEBUG_RECORD_WAIT wraps the literal timeout_ms
handed to the condition wait as a pass-through, so what is recorded and what is
actually waited on are the same evaluation and cannot diverge except by
changing what the macro wraps.

Two scenarios:
- waitseam asserts the recorded wait equals the host's own argument, exactly,
  over 0, 1, 15, 16, 39, 40, 999, 1000, 1001, 7300 and INT32_MAX (the header
  states no narrower maximum), and over 40 polls on one runtime past the point
  every prior scenario stopped asking. timeout_ms <= 0 never reaches the wait
  at all, so its probe checks the honest converse — the seam stays untouched —
  which is what catches a seam that records the raw argument on every call
  instead of only the value it actually hands its wait.
- pollhammer runs one thread hammering every gate-touching entry point that
  cannot itself queue an event, in a yield loop, against another thread's idle
  poll, once on a clean runtime and once after a refused listener_open has
  left runtime->error non-empty. The idle poll must still stay for its
  timeout.

polltimeout, pollbusy and holdtimeout stay as generous-margin proof that the
recorded wait is really waited, not as the precision instrument they used to
have to be: the fastest-of-ten mechanism, the 15 ms probe's tight tolerance,
and the stated-limits table they rested on are gone, because waitseam now
kills every clamp, floor, round, scale, offset and later-poll mutant by
equality regardless of timing. The Windows priority raise these three
scenarios carried is gone with them: a flat 800 ms late tolerance already
covers the worst overshoot the old table recorded (127 ms at normal priority
under load), so raising the process above the load bought nothing the new
margin does not already have. holdtimeout's long probe moves to 1600 ms, above
the stated 1500 ms ceiling on the hold guard, with the hold kept armed to
3000 ms so a guard that parks a naturally-timed-out call anyway overshoots by
well more than the margin.

Verified locally on win32-x64: waitseam and pollhammer pass against the
unmutated bridge; a seam-loosening mutant (recording the raw argument outside
the wait's own branch) fails waitseam's 0 ms probe; an inline +25 ms offset on
the wait's argument fails waitseam by exact mismatch; the S5 hold-guard mutant
(timeout_ms > 1500) fails holdtimeout by 1.4 s overshoot; K1 (++wakes > 64), K2
(!error.empty()) and a no-predicate revert all fail pollhammer's idle poll
within tens of milliseconds. All were hand-reverted and diffed clean against
the working tree before this commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:33:19 +02:00
Meta Vens
b090ca97f9 CultNet selection cut1, commit 3: the Rust runtime
docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs
(new) - the Rust spelling of the vocabulary at wire parity with the corrected
C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner
supplies declared values and edges through two traits.

- Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge,
  RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct
  from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/
  schemaContentHash - a pre-existing v0 gap this cut does not touch).
  SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics;
  FieldPredicate.op stays a raw wire string, never a serde-tagged union.
- canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a
  hand-rolled digit-grammar parser, no regex dependency added),
  canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then
  integer length, then digits, then padded fraction - no f64 anywhere), and
  render_i64/render_u64/render_f64/render_f32 for a Row implementer's own
  rendering. probe_f64_display_never_uses_exponent_notation empirically
  verifies the map's claim about Rust's f64 Display (confirmed true) rather
  than trusting it.
- Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number
  (canonical string, never f64), references for Row; all_schema_ids,
  declared_indexes, declared_roles, is_numeric, target_leaves for RowSet.
  A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of
  references satisfy generic bounds without an extra copy.
- validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing
  selections assert via debug_assert since Rust has no exception type to
  mirror C#'s InvalidOperationException), select (order/hop/cursor/paging,
  D9's reference_outside_target refusal, S23's per-row match - never through
  a unique-index map, because there is none here to begin with).
- Cursor: mint/parse/digest is this runtime's own internal algorithm (a
  cursor is opaque and never crosses runtimes - each server answers its own
  pages), not a byte-for-byte port of the reference's; what must match is the
  *rule* (asOf + last position + a digest bound to the selection), which it
  does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert.
- contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1,
  DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages'
  field-for-field shape. SnapshotResponseRawV1 carries binary payloads
  (documents[].payload, edges[].payload), so it is routed through the
  existing raw JSON-bypass path (parse_raw_cultnet_schema_message/
  encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/
  SnapshotResponseRaw, with its own require_*/encode_* helpers for
  SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef.
  SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the
  ordinary serde_json-intermediate path. No message-layer selection
  semantics validation here, matching the reference
  (CultNetSchemaMessageSerialization.cs only deserializes; a server calls
  selection::validate against its own RowSet before evaluating).
- snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled
  allowlist filters collapse into one call to selection::matches over a
  lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose
  ordinal/values/number/references are unreachable (the lowered selection
  never sets fields/cites/cited, so matches_schema_keys_fields never calls
  them) - documented, not hidden.

Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch,
mirrored here per the coordinator's mid-task instruction rather than C#'s
pre-fix spelling as it stood at e5348d3):
- validate refuses an empty-but-present schemas/keys list (already had this)
  and now also refuses any individual entry that is empty or whitespace
  (fields[i]-style names: schemas[i]/keys[i]).
- The evaluator never sees []; matches_schema_keys_fields does not special-case
  an empty list as "no filter" (it never did here - that C# fallback,
  CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust).
- snapshot_query.rs's v0 lowering gets its own cleaning, owned by the
  lowering and not the evaluator: lower_v0_list turns an empty or
  all-blank v0 schemaIds/recordKeys list into None before it ever reaches
  validate or select.

Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut;
+62 across lib unit tests, tests/selection.rs and tests/cultnet.rs).
cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit -
confirmed unaffected). GameCult.Networking.Tests (the new
SelectionParityVectorTests.cs file, compiled but not yet fully exercised -
see below) builds clean.

Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet-
selection-rust.entries.ps1): 22 entries against src/selection.rs and
src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and
new blank-entry refusals (schemas and keys, revert x4), order/descending
(revert), cursor staleness (revert + loosening) and digest (revert), S7's
incoming negation (revert + loosening), S16's le/ge boundaries (revert x2),
S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion
(revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's
integer-through-float64 renderer, decimal trailing-zero stripping and
exponent expansion (revert x3, the last two pinned via direct
canonical_number unit tests since render_f64 itself cannot reach exponent
notation given Rust's Display contract - verified, not assumed), and the
new v0-lowering rule (revert).

Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src`
empty.

Not included here, landing as commit 4 once ready: the committed parity
vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json).
Both directions already work for six evaluated selections and the []
door-refusal case; the blank-key-entry refusal vector cannot be generated
from the C# side yet because the parallel C# fix batch implementing that
door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit
2 fix batch") has not landed on this branch as of this commit. Rust's own
tests (validation_refuses_a_blank_entry_in_schemas_or_keys,
serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter)
already pin both new rules independently of C#.

Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new),
contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the
fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new
v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector
machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new),
tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new).
Against section 14's ~430-line estimate for selection.rs+contracts.rs: the
two together land at ~2000 lines added, well over estimate - the map's own
§14 correction already flagged commits 0/1 running roughly 2x estimate for
the same reason (the hop's incoming index, the cursor, and per-field door
messages cost more than the prose assumed), and the v1 raw-message handling
(SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers,
~400 lines) was not separately budgeted at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
Meta Vens
bc9fbb4fe3 Record Soul's seventh pass, and move the wait's observation to where it is decided
Seven passes on one rule, each killing the last Soul's functions and each
followed by another that is identity at every probe. Wall-clock observation
from outside cannot converge on an arithmetic rule. A dev-only seam records
the computed wait, and equality kills the whole family.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 05:10:25 +02:00
Meta Vens
1fbb28ae90 Record Soul on selection commit 2 and rule the fix batch
The reconcile tests lost what they proved, the shard snapshot still loops on
its own, and the public options type silently drops every term v0 cannot
carry. Empty lists are refused at the one door, Mesh refuses what it cannot
send, and the dead identity code is deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 05:02:05 +02:00
Meta Vens
e5348d3d57 CultNet selection cut1, commit 2f: the Q-J fix (canonical decimal numbers)
Corrects commit 1's Number field: docs/cultnet-selection-cut.md section 2
"Numbers" (added 2026-09-22, superseding the map's own stale float64 text)
specifies canonical decimal strings per the operator's Q-J ruling, and
commits 0/1 shipped double instead - the Rust Hands caught the conflict
before writing a line and stopped; this is the correction the map's
Progress block calls for before Rust starts.

- CultNetFieldPredicate.Number: double? -> string?. New CultNetCanonicalNumber
  (CultNetSelection.cs) is the wire's canonical grammar and comparator:
  IsCanonical validates ^-?(0|[1-9][0-9]*)(\.[0-9]*[1-9])?$ and excludes "-0"
  (the regex alone matches it); Compare orders by sign, then the integer
  part's length, then its digits, then the fraction padded on the right to
  equal length - a pure string function, no double anywhere on the path.
  ValidateField refuses a non-canonical number at the door
  (selection_invalid) rather than normalising it.
- CultNetSelectionEvaluator.CompareNumber now takes two canonical strings and
  switches on CultNetCanonicalNumber.Compare's sign instead of comparing
  doubles directly.
- CultCache.cs: CultDocumentDescriptor.TryGetIndexNumber and
  PersistedMember.NumberOf render a member's exact canonical decimal instead
  of a double. An integer renders exact (no 2^53 limit); decimal strips
  trailing fractional zeros; float/double use .NET's shortest round-trip
  ToString(), rewritten out of exponent notation to positional digits
  (ExpandScientificNotation) before the shared CanonicalizeDecimalDigits
  strips zeros and collapses a negative-zero result to "0"; NaN and infinity
  render null (TryGetIndexNumber returns false, matching "matches no
  comparison").
- contracts/cultnet/cultnet.selection.schema.json: "number" is now a string
  with the canonical pattern (a defence-in-depth (?!-0$) lookahead beyond
  what the door enforces in code), not a JSON number.
- Tests: CultNetCanonicalNumberTests (new) pins IsCanonical/Compare directly,
  including the door's five named non-canonical spellings, magnitude vs.
  lexicographic ordering, sign, integer-part length, and a long past 2^53
  against its float64 rounding. CultNetSelectionEvaluatorTests gains
  ValidationRefusesNonCanonicalNumberSpellings (+1, 1.0, 01, 1e3, -0) and its
  existing Number literals become canonical strings.
  CultDocumentSelectionSurfaceTests gains a dedicated FixtureLeafNumbers
  fixture and four tests: the 2^53+1 long rendering exactly, a decimal's
  trailing zeros stripped, a double's exponent notation rewritten to
  positional digits (1e21, 1e-7), and NaN/Infinity answering false.

Mutation entries (scripts/mutate-dotnet.ps1): four new Caching entries
(integer-through-float64, decimal trailing zeros, exponent expansion,
NaN/Infinity) and five new Networking entries (the door's regex, the door's
explicit "-0" exclusion, the comparator's integer-length check, a
lexicographic-whole-string mutant, and the comparator's sign check) - all
KILLED. The two pre-existing S16 (le/ge boundary) entries had to be
re-anchored: their Old text quoted CompareNumber's previous double-based
`left <= right`/`left >= right` spelling, which no longer exists now that
CompareNumber switches on Compare's int result: `cmp <= 0`/`cmp >= 0`.

Verification: dotnet test - GameCult.Networking.Tests 192/192 (was 183 before
these additions), GameCult.Caching.Tests 192/192 (was 188), GameCult.Mesh.Tests
252/253 (one pre-existing unrelated skip, unaffected by this commit - no Mesh
source touched). Caching mutation run: 10/10 KILLED (6 pre-existing + 4 new),
M0 and final both green. Networking mutation run: 15/15 KILLED (10
pre-existing + 5 new, across CultNetSelectionEvaluator.cs, CultNetDatabase.cs
and CultNetSelection.cs), M0 and final both green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 04:18:22 +02:00
Meta Vens
0402ab88bc Record the Mesh collapse and correct two claims in its report
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 03:55:43 +02:00
Meta Vens
4c33a62b29 CultNet selection cut1, commit 2e: mutation entries for the Mesh collapse
scripts/mutate-cultnet-selection-mesh.entries.ps1 covers the two rules moved in
commit 2d against tests/GameCult.Mesh.Tests: ResolveDefaultSelection's "no schema
filter when keys are given" default, and ReadDocumentFromSnapshotResponse's
recordKey-anchored exact read (including the foreign-schema-id decode fallback it
still honors).

M0: green, 87 passed (CultMeshStreamingTests). Verdicts:

- MESH-DEFAULT-Revert: KILLED
- MESH-DEFAULT-Loosening: SURVIVED
- MESH-KEY-Revert: KILLED
- MESH-KEY-Loosening: SURVIVED
- MESH-FOREIGN-Revert: KILLED
- Final: green, 87 passed. All restores byte-exact (working tree clean after the run).

Both SURVIVED entries are recorded as fixture gaps, not swapped for easier
mutants: no test in CultMeshStreamingTests.cs passes an explicit empty (non-null)
recordKeys array, and none varies a record key's case, so nothing distinguishes
MESH-DEFAULT-Loosening's `recordKeys != null` from the correct `recordKeys is
{ Count: > 0 }`, or MESH-KEY-Loosening's OrdinalIgnoreCase from the correct
Ordinal comparison. MESH-FOREIGN has a revert entry only; no distinct, still-wrong
"loosening" of "decode successfully as TDocument" was found that isn't already
one of the other two entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:54:36 +02:00
Meta Vens
5ba48f0ebe CultNet selection cut1, commit 2d: the CultMesh collapse
Collapses the rest of the Mesh selector surface named in docs/cultnet-selection-cut.md
section 4/11 onto CultNetSelection:

- CultMeshSnapshotRequestOptions becomes a record carrying Selection in place of
  SchemaIds/RecordKeys; its three hand-written clone functions
  (CloneSnapshotRequestOptions x2, CloneSnapshotFacadeRequestOptions) are gone in
  favor of `with`.
- CultMeshPeerSnapshotDocumentOptions becomes a record for the same reason;
  WithPublicationBindingOptions and ToSnapshotRequestOptions collapse to `with` and
  one inlined Selection literal at their single call site.
- CleanSnapshotFilter and the three-times "no schema filter when keys are given"
  rule collapse into two owners: CultMesh.ResolveDefaultSelection (the typed-document
  default) and CultMesh.OverlaySelection (explicit override over defaults).
- CultMeshHotBodySubscription carries one Selection instead of parallel
  RecordKeys/SchemaIds allowlists.
- ReadDocumentFromSnapshotResponse's four-tier fallback collapses to one
  recordKey-anchored read: exact schema/alias match, or (foreign/runtime-generated
  schema id) a payload that decodes as TDocument. The two tiers that matched by
  schema alone or recordKey alone - the ones that could return a record under the
  wrong key - are gone; DocumentHandle_ReadsRemotePeerSnapshotsAsTypedDocuments
  pins the surviving recordKey-anchored decode fallback.

Two items in section 11's table are not cut here, by finding rather than oversight:

- IsSameCultDocumentSchema (CultMesh.cs) already delegates to the one
  CultNetSchemaAliasMatching owner (no second engine); deleting the wrapper would
  duplicate its type-to-descriptor resolution at four call sites instead of
  collapsing it. Left in place.
- The selector literals in CultMeshClient.cs (574-604, 830-860, 932-962) call
  CultNetDatabaseSubscriptionClient.SubscribeAsync, which stays a v0
  (schemaIds/recordKeys) API in this cut - that client is not named in section 7's
  per-file plan. There is no Selection to move these onto without adding a v1
  subscribe path to Networking, which is out of this commit's scope. Left in place.

Tests: GameCult.Mesh.Tests 252/253 (1 pre-existing unrelated skip), 0 failed.
GameCult.Networking.Tests 161/161 excluding one pre-existing flaky test
(DatabaseSubscriptionServer_ReconcilesDeliveredProjectionAndBodyDemandWhenAuthorityChanges,
a background RUDP poll thread racing a disposed socket in code this commit does not
touch; it crashes the test host the same way on this branch before this commit too).
GameCult.Caching.Tests 188/188.

Net line count, src/GameCult.Mesh only: +143/-226, net -83. Section 11 estimated
about -326 for this cut's slice. The gap is mostly the two items above (about -49
of the estimate) plus one already-ruled keep this map's Progress block already
named (the foreign-schema-id decode fallback, -41 in the table, kept per the prior
Hands' ruling and untouched here) and the four-tier fallback surviving as a
two-tier one rather than a zero-fallback exact read (an honest test-enforced
requirement, not a padding choice).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:38:56 +02:00
Meta Vens
b7c641a681 Specify Q-J's canonical decimal, and sweep the float64 text the ruling left live
The operator ruled decimal strings on 2026-09-17 and the map recorded it in
section 16, but section 2 and D8 still said float64 and nothing specified the
canonical form or the comparison. Commit 1 built what section 2 said. The Rust
Hands stopped on the conflict before writing a line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 03:17:18 +02:00
Meta Vens
4167cc4f24 CultNet selection cut1, commit 2c: mutation entry for the fourth matcher's collapse
Self's ruling (docs/cultnet-selection-cut.md, 2026-09-22) required a mutation
entry for the fourth schema-matching copy's collapse into
CultNetSchemaAliasMatching, landed in commit 2a. Added
NET-D4-FourthMatcher-Revert, pinned by
CultNetDatabase_Creates_ShardSnapshotResponse_ForCompatibleSchemaAlias: killed.

Every pre-existing entry in this file now carries its own File, so the whole
file runs as one invocation against both CultNetSelectionEvaluator.cs and
CultNetDatabase.cs (it previously assumed a single implicit target and would
throw given two). Ran the combined file: 10/10 entries killed, M0 green, both
targets restored byte-exact (verified by git status showing no diff on
either .cs file after the run).

A loosening mutant for the same site
('(requestedSchemaIds != null && requestedSchemaIds.Length == 0) ||', i.e.
reject only an empty-but-non-null requested-schema list) was run by hand and
SURVIVED: CultNetDatabase_Creates_ShardSnapshotResponse_ForCompatibleSchemaAlias
puts exactly one document in the whole cache, so an over-permissive filter
still returns the same single row. Recorded as a comment in the entries file
per the brief ("report it, do not replace it with an easier mutant") rather
than committed as a fake kill or swapped for an easier mutant. Fixing it needs
a second document of a different schema in the shard's fixture; not added
here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:05:34 +02:00
Meta Vens
c5b03cb455 CultNet selection cut1, commit 2b: collapse IsSameCultDocumentSchema into the one alias matcher
CultMesh.IsSameCultDocumentSchema (CultMesh.cs) and the decode-time alias check
in CultMeshSnapshots.DecodeSnapshotDocuments both re-implemented schema-identity
comparison independently of CultNetSchemaAliasMatching, the one matcher landed
in commit 1. Both now call CultNetSchemaAliasMatching.Matches; the private
InferSchemaName duplicated at CultMeshSnapshots.cs is deleted (Matches already
applies the same .vN inference internally).

Spec discrepancy, reported per the Hands brief (map contradicted by the Body):
docs/cultnet-selection-cut.md section 4 lists CultMeshSnapshots.cs:1033-1047
(RawSnapshotPayloadMatchesSchema) and :1078-1088 (InferSchemaName) as deletions,
on the stated reasoning that decode no longer needs a client-side re-filter
because the server's v1 answer is exact. Deleting RawSnapshotPayloadMatchesSchema
outright breaks two existing, still-relevant Mesh tests:
SnapshotHelpers_DecodeSameSchemaAliasFromForeignSchemaId and
SnapshotEndpoint_SyncsCompatibleForeignSchemaIdIntoLocalCache
(CultMeshStreamingTests.cs). Both cover a wire schemaId this runtime cannot
resolve by any alias at all (a foreign/runtime-generated id, e.g.
"runtime.generated.mesh-note.ui.42") where the only way to decode is to trust
the payload's own embedded schemaVersion. That is a decode capability, not a
re-filter of an already-exact server answer, and the map's stated reason for
deleting it does not apply to it. Smallest change keeping the map's intent
true: kept RawSnapshotPayloadMatchesSchema and TryReadSchemaVersion, deleted
only the duplicate InferSchemaName inside it and routed the comparison through
CultNetSchemaAliasMatching.Matches.

Test counts, no regressions: Networking 162/162, Mesh 252/253 (the one
pre-existing skip), Caching 188/188.

Negative grep: MatchesRequestedSchema empty. The one remaining
"MatchesSchema(" grep hit is RawSnapshotPayloadMatchesSchema's own name and
declaration, kept for the reason above.

Still outstanding from the map's Mesh collapse (§4/§11, ~365 gross lines):
the five cloned CultMeshSnapshotRequestOptions-carrying option records, the
"no schema filter when keys are given" rule (x3), CleanSnapshotFilter, the
four-tier ReadDocumentFromSnapshotResponse fallback, and every selector
literal in CultMeshClient.cs/CultMeshBodyDemand.cs/CultMesh.cs peer-snapshot
and body-subscribe construction. Those require CultMeshSnapshotRequestOptions
(and the v0 wire builders that read it) to carry a Selection instead of
separate SchemaIds/RecordKeys — a wider, structural change across Mesh's
public surface that is not done in this commit. Recorded as the next slice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:54:53 +02:00
Meta Vens
0787608cef CultNet selection cut1, commit 2a: delete _projectRecord, collapse the fourth schema matcher
_projectRecord (CultNetDatabaseSubscriptionServer.cs) reached no production
caller and ran after authorization with no wire consumer. Removed the field,
constructor parameter, and both call sites in Watch() and
CreateProjectedSnapshot(); projection is a value on a v1 selection now, not a
delegate. The two tests that used projectRecord to simulate a record's
identity changing for the reconcile diff
(DatabaseSubscriptionServer_AuthorizesRequestAndFiltersSnapshotAndLiveRecords,
…ReconcilesDeliveredProjectionAndBodyDemandWhenAuthorityChanges) are rewritten
to drive a real rename through the database (delete the old key, put the new
one) instead, so they still prove authorization filtering and the Reconcile()
diff/body-demand behavior.

Self's ruling (docs/cultnet-selection-cut.md, 2026-09-22): the fourth
schema-matching copy at CultNetDatabase.cs, serving CreateShardSnapshotResponse
and SchemaMatchesCatalogFilter (MatchesRequestedSchema + a duplicate
InferSchemaName), collapses into CultNetSchemaAliasMatching.MatchesAny, the one
matcher landed in commit 1.

Networking tests: 162/162 (was 162/162 after commit 1; no count change, two
tests rewritten not added). Negative grep: _projectRecord|projectRecord empty;
MatchesRequestedSchema gone (one remaining substring hit is
RawSnapshotPayloadMatchesSchema in CultMeshSnapshots.cs, the Mesh copy this
commit does not touch — collapses in the next commit).

Mesh collapse (§4/§11 of the map, ~365 gross lines across CultMesh.cs,
CultMeshSnapshots.cs, CultMeshClient.cs, CultMeshBodyDemand.cs) is not in this
commit; it is large enough to build on its own and follows as commit 2b.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:44:23 +02:00
Meta Vens
3d86bfacec Record selection commits 0-1, the fourth engine, and the ledger miss
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:35:34 +02:00
Meta Vens
17d10e017f CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).

Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.

Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
  cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
  replaces the reflection entirely. v0 lowers into a Selection and walks the
  evaluator's own pages so a v0 caller still sees its whole matching set
  (D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
  ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
  MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
  v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
  CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
  (92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
  CreateProjectedSnapshot's collapse are commit 2's, per the map's own
  deletion line, and are untouched here.

Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.

Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.

Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).

Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.

scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 02:31:42 +02:00
Meta Vens
907f216456 Record the sixth fix batch, accept the Windows priority raise, dispatch Soul 7
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:21:57 +02:00
Meta Vens
ff72f16ed6 Give the hold's guard a mutant that reads the timeout, and reach it
Soul's sixth pass (N4): `(woken || timeout_ms > 1000) && HELD` survived both
targets. The two existing hold entries only rearrange the guard's syntax,
and `holdtimeout`'s single 150 ms poll sits below any threshold such a guard
would carry.

`holdtimeout` now runs two idle polls under the armed hold, 150 ms and
1300 ms, and holds for 2200 ms. The new entry is that guard. Compiled in on
both targets, the 1300 ms poll is parked until the release and returns at
about 2200 ms, and every other scenario stays green. The harness kills it
on both targets.

With the two polls running together, the first poll's exit also wakes the
second. That gives the asserts build a second multi-threaded witness for the
predicate: the N1 revert and read-what-woke mutants die here as well as on
`pollbusy`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:01:41 +02:00
Meta Vens
4ffa4c9628 Pin the wait's predicate with a host that has a second thread
Soul's sixth pass (N1): every CallScope destructor wakes every waiter, so
without the predicate a poll leaves whenever another host thread makes any
call. Every scenario had a single-threaded host, and dropping the predicate
survived the whole matrix on both targets.

`pollbusy` is a public-API scenario and needs no development seam. A second
host thread calls `cultmesh_quic_last_error` every 20 ms for twice the
poll's timeout while an idle `poll(1000)` waits. It checks the timing first.
It then checks the other thread's call count, so a poll that passed without
ever being woken reads as the fixture failing.

Three harness entries:
- revert: no predicate, `woken` taken from `cv_status`;
- loosening: the wait reads what woke it and does not wait again;
- loosening: a hand-written loop that restarts the whole timeout on every
  wake. Its lateness is a function of the input.

Each was compiled in and run through every scenario on both targets:
- revert and read-what-woke: `pollbusy` returns at 0 ms;
- re-arm: `pollbusy` returns at about 3000 ms.
The earlier scenarios all stay green under all three. The harness kills all
three on both targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:01:27 +02:00
Meta Vens
37967e8b9b Probe the host's timeout at 15, 200 and 7300 ms, and stress the tolerance
Soul's sixth pass (N2): with probes at 150, 200 and 5000 ms, `min(t,5000)`,
`min(t,4990)`, `t*11/10` and `t+70` survived both targets, `max(t,220)`
survived linux-x64, and `max(t,250)` and `t+80` died by 22 and 2 ms. "5000 is
above any plausible ceiling" was wrong: 5000 is a round ceiling.

- A 15 ms probe, asked ten times with the fastest checked against 30 ms. It
  is the only place a floor or an added constant shows. Scheduler delay only
  adds, so noise has to delay all ten attempts to fail the bridge.
- The long probe is 7300 ms, which is not a round number.
- A single poll gets 60 ms plus a sixteenth of the timeout.
- Each timed scenario prints its worst overshoot per probe.
- Three harness entries: a round clamp `min(t,5000)`, a floor
  `max(t,100)` and an offset `t+100`.

Stressed with two busy threads per logical CPU (16 on 8), five rounds of
every timed scenario on both targets. At normal priority, Windows put a
single poll up to 127 ms late and the fastest of ten 15 ms attempts up to
87 ms late, and a 45 ms tolerance failed the unmutated bridge in four rounds
of five. The timed scenarios therefore raise their own process to
HIGH_PRIORITY_CLASS on win32-x64, which needs no privilege. With that, the
worst single-poll overshoot was 15 ms on win32-x64 and 41 ms on linux-x64,
at normal priority there. The fastest 15 ms attempt was 0 ms late on both.

The limit in source is measured, not asserted. Each boundary mutant was
compiled in and run on both targets:
- `t+25` survived and `t+40` died;
- `max(t,40)` survived and `max(t,50)` died, on linux-x64 by 5 ms;
- `t*106/100` survived and `t*108/100` died;
- `min(t,7285)` survived and `min(t,7275)` died.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 01:59:37 +02:00
Meta Vens
e420410fe9 CultNet selection cut1, commit 0: the cache's public read surface
docs/cultnet-selection-cut.md section 6/7/10: the reflection loop at
CultNetDocumentRegistry.cs:325-342 exists only because nothing outside
GameCult.Caching can read a declared index value, reference, or numeric
member (Members/IndexAccessors are internal, no InternalsVisibleTo to
Networking). This adds the one public door instead: CultDocumentMemberView,
CultDocumentDescriptor.DeclaredMembers/TryGetIndexValue/TryGetIndexNumber/
ReferencesOf, and CultDocumentRegistry.ResolveTargetLeaves (D9, computed live
against the registry, never cached or persisted).

Two registration refusals land beside DiscoverMembers' existing
DuplicateSlotMessage/HiddenMemberMessage: D10 renames the ArgumentException
ToDictionary already threw for two differently-named members sharing one
index alias into a rejection that names the schema, both members and the
alias; D11 refuses a reference member whose CLR shape the cache cannot walk
(many:false needs CultRecordRef<T>, many:true needs
IEnumerable<CultRecordRef<T>> or IDictionary<CultRecordRef<T>, V>) instead of
silently answering it as unreadable later.

GameCult.Networking is untouched; the evaluator that will read this surface
does not exist yet (commit 1).

Verification: dotnet test tests/GameCult.Caching.Tests - 188 passed (176
existing + 12 new), 0 failed. New tests: TryGetIndexValue/TryGetIndexNumber
reading an inherited alias; ReferencesOf over a single/many-list/many-dictionary
reference, including two dictionary entries with different payload bytes
(S19); ResolveTargetLeaves against an abstract target with no [CultDocument]
of its own (D9), including a leaf registered after an earlier descriptor was
built; IsNumeric agreement for a nullable numeric and a decimal member (S22);
the two new registration refusals, dynamically emitted so registry discovery
by attribute scan cannot see the rejected shapes.

scripts/mutate-cultnet-selection-caching.ps1: a dedicated dotnet/NUnit
mutation runner (the shared tools/eureka-mutations.ps1 harness only speaks
cargo; scripts/mutate-cultmesh.mjs is the QUIC campaign's file and this
campaign does not edit it, per the map's section 10). Byte-exact restore
verified by SHA-256, a sidecar for crash recovery, M0 no-op control, one
anchor match required per entry. Six entries (D8/S22, D10, D11 - revert and
loosening each) all KILLED; final restore verified green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 01:57:35 +02:00
Meta Vens
b3d9cf7821 Tell an existing Windows clone how to get its build script back to LF
`*.sh text eol=lf` reaches new clones only. A clone checked out before the
rule keeps scripts/build-quic-native.sh with carriage returns after pulling,
because the blob did not change: git neither rewrites the file nor reports it
modified, and the container still dies on the interpreter `bash\r`.

The README now gives the repair where a newcomer meets the container path,
and .gitattributes points to it. Verified on clones made at 367c9bc and moved
to c25de5a, from Git Bash and from Windows PowerShell 5.1: 109 CR bytes
before, none after, status clean. The line uses `;` so it runs in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 01:03:51 +02:00
Meta Vens
c25de5a895 Record Soul's sixth pass: the wait's predicate is pinned by nothing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:50:29 +02:00
Meta Vens
7cd5a2b63f Record the fifth fix batch landing and dispatch Soul's sixth pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:23:24 +02:00
Meta Vens
fa327a0627 Name the CultMesh MCP bridge as a priority campaign
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:37:00 +02:00
Meta Vens
089859deb4 Write and enforce the semver policy, per operator ruling on 1.0.60
1.0.60 removed public API (CultInspectorAssetPath) under a patch bump; its
own changelog says "Breaking" and the version number lied. Operator ruling:
semver is policy, strictly, for every published package family.

docs/semver-policy.md states what counts as breaking per surface (managed
API, Unity inspector attributes, CultCache wire format/store compatibility,
CultNet protocol, native bridge exports), the 0.y.z pre-1.0 convention, tag
naming, and required changelog shape. Records the 1.0.60 scar as history;
does not renumber it.

scripts/check-changelog-semver.mjs is the mechanical check: refuses a
changelog "### Breaking" section under too small a bump, and refuses a
version that skips, reverses, or repeats the previous published tag. Skips
re-litigating a version that is already tagged (rebuilding immutable
history is not a release action). Wired into publish-packages.yml for the
four npm/PyPI packages, and into build-unity-package.ps1 (cultlib),
packages/cultmath/scripts/build-unity-package.ps1 (cultmath), and a new
scripts/verify-caching-unity-release.ps1 (caching.unity has no build script
of its own to hook into).

18 node:test cases cover the required table (breaking+major/minor/patch,
non-breaking, 0.y.z, skip, downgrade) plus CLI-level git-tag resolution.
Hand-mutated each rule (bump classification, pre-1.0 lane requirement,
breaking-heading detection, breaking early-return, already-published skip)
and confirmed each kill, then restored byte-exact. No mutation harness is
committed; CultLib has none today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:03:22 +02:00
Meta Vens
45c2f4006f Say the OpenSSL bridge ships with this release, and pack the changelog
Soul S1: the 1.0.60 entry claimed the OpenSSL MsQuic bridge had shipped
since 1.0.59; git says both blobs first appear in this release. S2: the
package build staged package.json and README only, so the produced package
and the tracked template differed by the changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:40:12 +02:00
Meta Vens
ef81c96094 Renormalize CultAssetGuidKey.cs to CRLF and rebuild dependent plugin DLLs
CultAssetGuidKey.cs (and the test file added in 0b88706... no, 6c7cfa7) had a
literal LF working-tree checkout under core.autocrlf=true / text=auto, so a
build run from a stale bypassed-checkout copy of this worktree embedded a
source checksum a fresh clone's CRLF checkout would never reproduce. The
tracked git object was already normalized; only the working-tree bytes in
this worktree were wrong. Restored the CRLF checkout (git add reports no
diff against the index for CultAssetGuidKey.cs itself), then rebuilt with
scripts/build-unity-package.ps1 -UpdateTemplate. GameCult.Caching(.pdb) and
every managed plugin that depends on it (GameCult.Caching.MessagePack,
GameCult.Mesh, GameCult.Mesh.Quic.Native, GameCult.Networking,
GameCult.Networking.WebSockets) picked up new MVIDs/PDB checksums as a
result; the native gamecult_mesh_quic_native.dll/msquic.dll are untouched
since the native build did not run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:12:52 +02:00
Meta Vens
9847a96070 Track the new CHANGELOG.md.meta files (force-added like other tracked .meta)
*.meta is gitignored repo-wide; every tracked Unity .meta file, these two
included, is force-added on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:11:48 +02:00
Meta Vens
0b8870677e Add CHANGELOG.md to both Unity packages (UPM convention)
CultLib 1.0.60: CultInspectorAssetPath removed for CultInspectorAssetGuid +
CultAssetGuidKey (breaking for inspector annotations), and the native QUIC
bridge's Schannel to OpenSSL MsQuic 2.5.9 static-link change shipping since
1.0.59. Caching Studio 1.4.0: the GUID picker, sub-asset keys, built-in
refusal, and the stale-value warning/Clear button. build-unity-package.ps1
only stages files it names explicitly, so it neither drops nor overwrites
these; no script change needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:11:33 +02:00
Meta Vens
6c7cfa7a94 Studio: refuse built-ins by asset identity, not GUID digits; add Clear for stale keys
TrimEnd('0','f') missed Library/unity default resources built-ins whose GUID
has no all-zero or all-f suffix (e.g. 0000000000000000e000000000000000, the
LegacyRuntime font / Cube mesh in 6000.3.24f1). Ask AssetDatabase instead: a
real project asset is always a main or sub-asset under Assets/ or Packages/;
built-ins are neither. Also add a Clear button beside the stale-value warning
so a non-resolving stored key can be cleared without picking a substitute
asset first (drawing still never rewrites a stored value on its own).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:09:53 +02:00
Meta Vens
0c9cb57bea Correct stale Schannel/40,448-byte claims, ship OpenSSL's license notice
Soul flagged (F3) two docs still describing the pre-release native QUIC
bridge: unity/org.gamecult.cultlib/README.md said Schannel, and
native/GameCult.Mesh.Quic.Native/README.md still described the older
40,448-byte committed plugin as current. The shipped bridge (since the
1.0.60/1.4.0 release, commit 0155ba8) is the OpenSSL MsQuic 2.5.9 build,
302,080 bytes, static CRT, no CRT import. Both docs now describe that build
and name what it replaced.

The OpenSSL build's NuGet package carries an Apache-2.0 notice for OpenSSL
itself (Microsoft.Native.Quic.MsQuic.OpenSSL's THIRD-PARTY-NOTICES) that the
committed Third Party Notices folder never carried - the Schannel build had
no such dependency to notice. build-quic-native.ps1 now copies it out
alongside MSQUIC-LICENSE.txt, build-unity-package.ps1 stages it into the
package output and the tracked template, and it is now committed as
OPENSSL-NOTICE.txt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:49:14 +02:00
Meta Vens
1e398e0116 Studio: read and write sub-asset Addressables keys, warn on stale values
Ruling S (2026-09-17): a stored [CultInspectorAssetGuid] value is the
Addressables key form itself, the bare GUID for a main asset or
"guid[subAssetName]" for a picked sub-asset (one sprite of a sheet, say) -
the same shape AssetReference keeps and Addressables.LoadAssetAsync<T>
accepts.

CultAssetGuidKey (engine-free, GameCult.Caching) pins the parse/format rule
with unit tests (bare guid, guid[name], names with brackets/spaces,
malformed input), so headless checks can share it without touching Unity.

The Studio drawer's AssetGuid branch now: resolves either key form on read;
refuses to store a built-in resource's GUID (it has no Addressables entry)
and logs why instead of silently writing nothing; shows a warning next to a
stored value that fails to parse or resolve instead of drawing it as an
ordinary empty field, and never rewrites that stale value on its own.

Rebuilt GameCult.Caching/GameCult.Caching.MessagePack plugin DLLs
(scripts/build-unity-package.ps1 -UpdateTemplate) since the new type ships
in GameCult.Caching. Compiled CultCacheStudioDrawers.cs and
CultCacheStudioWindow.cs against Unity 6000.3.24f1's editor reference set
plus these plugins in a scratch csproj (removed after): 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:48:45 +02:00
Meta Vens
0155ba8cca Prepare 1.0.60/1.4.0 release: rebuild plugin DLLs, bump package.json
Removing the public asset path attribute is breaking. Runs
scripts/build-unity-package.ps1 -UpdateTemplate to refresh the tracked
plugin DLLs/pdbs in unity/org.gamecult.cultlib/Runtime/Plugins, and bumps
unity/org.gamecult.cultlib/package.json to 1.0.60 and
src/GameCult.Unity/Assets/Caching/package.json to 1.4.0 (dependency bumped
to cultlib 1.0.60). Tags are the next, separate step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:46:07 +02:00
Meta Vens
95a3c32250 Replace the asset path attribute with a stable GUID identity
CultInspectorAssetPathAttribute stored an engine asset path, which breaks
when the asset moves. CultInspectorAssetGuidAttribute stores the asset's
stable identity (in Unity, the .meta GUID) instead. The Studio drawer now
resolves the stored GUID to a path only to load the preview object, and
writes back AssetPathToGUID on a change. No path form survives for the
next field to pick up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:43:03 +02:00
Meta Vens
244154e2f5 Say what the restore digest is, since it is not an identity
The last report quoted the restored sha256 as though two runs agreeing on it
meant they had run the same source. They had not: `text=auto` gives a Windows
checkout carriage returns, so the win32-x64 run restored to b163cab7 and the
linux-x64 run to e3c6528a from the same commit. The digest is a restore check —
the file this run started with is the file it ended with — and it is said where
it is printed, so the next report does not borrow it for something it cannot do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 03:11:05 +02:00
Meta Vens
b985f07887 Say what the single-waiter wake actually rests on, and what is still unreached
The note beside the close's wake claimed the narrowing to `notify_one` was
equivalent, and gave the weaker half of the reason. The equivalence needs two
premises together: `closing` is set permanently under the gate before the notify
and appears in every waiter's predicate, so after a close begins no poller can
block at all and the set of waiters is fixed; and each exiting call's own scope
destructor takes the gate, decrements and wakes everyone, so one wake starts a
chain that reaches all of them. Neither carries it alone. Both are now written
where the claim is made, so it can be checked rather than believed.

Narrowing both wakes also survives, and that is not equivalent by any argument:
a wake consumed by the closer, waiting on the same variable for the count to
reach zero, leaves the chain stopped and the rest of the pollers sitting out
their timeouts. It passes on wait-queue ordering — the pollers are queued before
the closer, so a FIFO head is never the closer until it is the only waiter left.

Recorded as unreached, with the reason, and deliberately not as unreachable:
that claim has been wrong twice in this campaign. A scenario cannot force it
while the first premise holds, since the only way to put the closer ahead of a
poller in that queue is a poller that blocks after the close began. Reaching it
would need a seam that chooses which waiter a notify goes to. The standard does
not promise FIFO, so what survives here is the implementation's doing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:47:34 +02:00
Meta Vens
5ab4c627d7 Put a call in both states so the seam's own guard is pinned
The last pass's only non-comment source change was held by nothing. Reverting the
hold's guard so it fires unconditionally survived all five scenarios, because no
scenario ever had a call in both states at once: the timeout scenario never arms
the hold, and the hold scenarios never let a timeout expire.

`holdtimeout` is the missing state. The hold is armed, the runtime is idle, and
one poll asks for 150 ms while the hold stays armed for 800. A call the wait woke
is the seam's to park; a call whose own timeout expired is on its way out, and
parking it would let `holdclose` report the bridge keeping a host call inside the
library when what kept it was the fixture. The measurement is which of those two
numbers the poll comes back at.

Two entries, and the loosening is the one worth reading: the same two conditions
in the other order. The hold blocks, so the order is the rule — asked first, it
parks the call and then decides whether it should have. Both die on
`holdtimeout` at about 800 ms and on nothing else; `holdclose` and `latecall`
stay green under both, which is the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:46:49 +02:00
Meta Vens
c682619fbd Close the timeout against waits derived from the argument, not only constants
The two bands ruled out a constant and ruled out nothing else. Any mapping that
is the identity at 200 and 900 passed for free, and the late tolerance was a flat
300 ms, so anything within that of the truth passed at every value forever.
Three ordinary ones survived all five scenarios on both targets: clamping the
wait to at most a second, adding 150 ms to it, and scaling it by six fifths.

The clamp is the one that matters. Its failure is verbatim the bug the last pass
claimed to fix — a host asking for five seconds gets one, forever, with nothing
reporting it — and "cap the wait so a shutdown gets noticed" is the most ordinary
spelling that line will ever be given.

So the long probe goes to 5000, above any ceiling a bridge would plausibly clamp
to, which kills the clamp and the scale on the early check and the late one. And
the late tolerance becomes a fraction of what was asked for — 60 ms plus an
eighth — which is what kills the offset: 150 ms is 75% late at the 200 ms probe
and there is no flat gap left to hide in. Measured overshoot on either target is
about 10 ms, against 85 ms allowed at the short probe and 685 at the long.

The table gets the clamp as an entry of its own, because the whole lesson here is
that a table of constants says nothing about a wait computed from the argument,
and a table of constants would go green again the moment these numbers drifted
back. Its honest limit is stated where the tolerance is defined: a scaling
smaller than an eighth survives, and nothing here claims otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:45:57 +02:00
Meta Vens
c177533b4f Offer the container line to both shells, and name the short-path requirement
The container recipe carried a PowerShell backtick continuation in a snippet
whose next paragraph invites a POSIX shell to run it, where a backtick is not a
continuation. A continuation has to pick a shell, so the line does not have one.
The Dockerfile's own copy of the recipe still mounted one machine's checkout by
absolute path; it mounts whatever checkout is running it, like the other two.

And the requirement a newcomer meets by tripping over it: on win32-x64 the
harness needs the checkout somewhere short. MSBuild's file tracker gives out on
long paths, so a run from a deep temporary directory fails the build for every
mutant, the no-op control included. That is loud — the harness stops on a red
control and prints what the child said — but what the child says is MSBuild, not
the path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:44:47 +02:00
Meta Vens
ca6c0f5852 Keep shell scripts LF so a Windows clone can run the Linux path
`* text=auto` and nothing else, so a Windows clone writes every shell script with
carriage returns. The documented Linux path is a container that runs one of them,
and it dies in under a second with a complaint about an interpreter named
`bash\r`. Nothing in that failure names line endings, and the same clone builds
and tests fine everywhere else, so it reads as a broken image.

`scripts/build-quic-native.sh` is the only shell script in the repository and the
only file on those paths that an interpreter reads by shebang; the mutation
runner is Node and handles carriage returns itself, and the Dockerfile builds
either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:43:37 +02:00
Meta Vens
367c9bc2b8 Record the pass that beat the bands with a clamp
No constant sits in both bands, which is true and is not the rule. A wait that
caps itself at a second passes every scenario, and capping a wait so shutdown
gets noticed is the most ordinary way that line will ever be written. An offset
and a scale survive beside it.

A fresh Windows clone also cannot run the documented Linux path at all, because
nothing tells git to leave shell scripts alone and the container dies on a
carriage return in the interpreter line. The mount fix works; this sat behind
it.

The equivalence holds, but on two premises nobody had written down, and
narrowing both wakes survives while being equivalent to nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 02:36:37 +02:00
Meta Vens
1845301f91 Record the batch that made the timeout observable
The blind spot was not where the hold sat. No scenario ever let a timeout
finish a poll, because both ended their waits with a close, so the duration
governed nothing anyone could see. A scenario that polls an idle runtime and
measures elapsed against what it asked kills all three constants, and it asks
twice at values whose bands do not overlap, because one value cannot tell a
bridge honouring the host from one waiting on that same number of its own.

The probe that reached the two-phase poll through a closed port is committed,
the wake finally has the entry the table had been claiming, and a surviving
mutant is argued equivalent and handed to Soul rather than swapped for an
easier one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 02:12:12 +02:00
Meta Vens
a937a0b07e Let the bridge's own timeout, refusal and two-phase poll be seen
Three rules the bridge has always promised, none of which anything committed
could see.

The timeout was the structural one. Both scenarios end their waits with a close,
so the duration the bridge waits on governed nothing either of them observes: a
hard-coded 2000 ms survived the whole Linux matrix and Windows, and a host asking
for five seconds would have got two forever with nothing reporting it. The fix is
not another entry at another constant. A timeout's only observable is how long
the call takes, so `polltimeout` lets an idle runtime's poll be ended by its own
timeout and measures it, twice at different values, because no single constant
sits in both bands. The seam's hold moves behind that too: it parks only a call
the wait woke, so a poll whose own timeout expired is on its way out rather than
held there by the fixture and counted as the bridge's doing.

The refusal during a close (the header's section 4) had no scenario at all. A
call admitted after the closer has begun increments the in-flight count behind a
wait that has already read it. `latecall` holds the close open with the seam and
makes a call into it.

And the two-phase poll was written off as needing a listener, a credential and a
connection. It needs a closed loopback port: the failure always carries a reason,
which is an 87-byte payload, in about a millisecond. Moving the pop above the
copy crashes on both targets.

18 killed, 0 survived, 0 skipped on linux-x64; 16 killed, 0 survived on
win32-x64, where the two ThreadSanitizer entries are honestly skipped. Both runs
restored the source to the same sha256, b163cab7d48ebc93.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:09:50 +02:00
Meta Vens
25d7de26b3 Point the container recipe at whatever checkout is running it
The mount was this machine's own path, so the one documented way to build and
check the Linux bridge needed editing before it would run anywhere else. It is
the repository root now, named as the shell names it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:09:12 +02:00
Meta Vens
122c2172d9 Rule the reference check onto read now and write later
A reference whose target contradicts its declaration is noticed on read in this
cut and refused by name. Writing the same check into the put path is intended
rather than optional: the declaration already names the target and the writer
already holds the record, so it is cheap, and leaving it to consumers means
every tool that writes records reimplements it and a tool that forgets looks
exactly like one that did not until something refuses.

The consumer story rests on less than it sounds. There is no Studio repository
and no project by that name; the tooling that enforces this today is one
console project in one game. That makes now the cheapest moment to move the
rule to its owner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:44:09 +02:00
Meta Vens
2df124e269 Name what the generator needs before it can exist
A generator runs against published schemas, and the published schema entry is
flat: the cache walks the base types to collect members and then throws the
hierarchy away. Fed that, a generator emits one flat class per leaf with the
inherited members copied into each, which is the opposite of building the
hierarchy up for you.

So the catalog carrying each leaf's base chain is a prerequisite of that
campaign rather than a nicety for naming families, and families fall out of it
once it exists. The campaign itself waits until this cut settles the shape it
would be generating against. No generator exists in the repository today.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:38:24 +02:00
Meta Vens
34df099e8c Rule the cross product, and park families for the right reason
A caller needing exact pairs asks once per schema. The limit is stated rather
than answered with a third addressing list.

Naming a family is parked, but not because there is nothing to name. In one
process a family is an assignability check; over the wire there is no type
system, and nothing publishes the hierarchy where a caller can see it. The fix
is a catalog entry naming each leaf's base chain, which is small work that
moves the catalog's shape, so it belongs in a cut already touching it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:30:45 +02:00
Meta Vens
6e4b9e7eb4 Rule numbers onto decimal strings
Floats are fragile, and every value in this vocabulary is already a string, so
decimal strings keep the wire to one value encoding rather than adding a
second. The recommendation this reverses had that backwards.

What it costs is a comparison rule, and that is where the danger sits: compared
as text, ten is less than nine. The cut owes a canonical decimal form and a
numeric comparison, both pinned, and it no longer owes anyone a sentence about
where exactness runs out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:23:44 +02:00
Meta Vens
fa6c000e7f Record the pass where the verdict splits
A stranger can now rerun both targets from the committed container and the
README, which was the batch's point and holds. The rules are another matter.
The scenario holds its callers after the wait whose duration is the rule, so
a hard-coded two seconds walks through the whole matrix while the committed
entry only ever died because one constant was smaller than another.

The gap recorded as unreachable is reachable in fifteen lines against a closed
port, and the mutant crashes a plain optimised build on contact. Second gap
falsified today. The next batch fixes the cause once instead of adding another
entry at another constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:19:02 +02:00
Meta Vens
47618428a3 Close three questions the map still lists as open
The operator ruled ordinals, the two deletions and the leaf titles earlier
today, in the other campaign's map. This one still carried them as open, and
a stale live question is exactly what a later agent reads as a decision still
to make.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:13:41 +02:00
Meta Vens
ee91062cee Finish the selection map, and price it honestly
The cache already declares and persists what type each member is, so numbers
cost no migration. What they do cost is a public way to read a declared value
at all, because the evaluator today cannot reach one from outside the cache,
which is why the existing snapshot loop reflects. That is a hundred and fifty
lines, not sixty, and the ledger moves the wrong way to say so.

Three claims inherited from the earlier pass were wrong and were cut rather
than designed around. A fifth selector engine turned up in Rust. And the
cache's index maps keep one key per value, so an evaluator that reached for
them would answer a query about a whole family with a single row.

Watching a selection is mapped as its own cut, where nine overloads that are
a query language in disguise collapse into one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:13:08 +02:00
Meta Vens
211b5e320f Point the Linux recipe at the container that exists
The map carried a one-line invocation that installed no JavaScript runtime,
so it could not run the mutation runner, and said nothing about disabling
randomisation or relaxing seccomp. That is how the Linux path came to be red
for everyone except the agent that had found the flags by hand. The recipe now
lives with the image and the bridge README, and the map points at them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 00:48:50 +02:00
Meta Vens
440709829d Commit the container the linux-x64 entries are honest in
The image the bridge is built and mutated in existed nowhere in the repository,
and the recipe that stood in for it installed a C++ toolchain and no JavaScript
runtime — so the container could not run the harness that checks the bridge it
had just built. It is a file now, pinned to the same debian:13 digest as the
release workflow, carrying ninja, Node, setarch and MsQuic's four runtime
dependencies.

The harness configures with `-G Ninja` on linux-x64, which is what
scripts/build-quic-native.sh uses. CMake's Linux default is Makefiles, the image
carries ninja rather than make, and the default configured nothing at all: the
first run in the committed container died in cmake. It said so, because the
control now prints what the child said, which is the whole point of that change.

The seccomp requirement is stated where someone hitting it will read it — the
bridge's README, beside the recipe — rather than being knowledge the runner
assumes its caller already has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:37:16 +02:00
Meta Vens
c2c07605d8 Map the selection cut for CultNet's database organ
Three selector engines evaluate the same two allowlists today, two of them
carrying identical copies of the same reflection, and projection is a delegate
no production code ever constructs. The map replaces all of it with one
selection a caller can state: any-of over the aliases the cache already
declares, a projection it asks for, an order, a cursor, and one hop along a
declared reference.

The audit is reported as measured rather than as hoped. About 326 of 21,720
lines collapse in CultMesh, and the repository grows by about 370, because the
Rust runtime gains a capability it never had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 00:36:44 +02:00
Meta Vens
12c8bfcc7d Let the prohibition on a late call stand on its own
Section 4 forbade any host call that begins after `cultmesh_quic_runtime_close`
starts, and promised in the same sentence that such a call is refused with -1;
section 6 repeated the promise for `cultmesh_quic_next_event`. The refusal lives
behind the runtime's own gate, so once the close has returned there is nothing
left to read it: the case the sentence describes is exactly the case where the
promise cannot hold.

The prohibition stands alone. The refusal is stated for what it is — a call that
races the start of a close, while the close is still running, is refused and
touches nothing — and no longer as a guarantee covering the call the section
forbids.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:25:59 +02:00
Meta Vens
e4e09a086c Enter the rules the quiesce rests on, and stop exiting green on skips
Four entries the native target did not have. Two for the rule that a blocking
crossing is a counted host call: the revert deletes the call scope from
`cultmesh_quic_next_event`, and the loosening keeps the scope and its refusal but
drops it at the door, before the call blocks. Two for the rule that a positive
timeout blocks until an event or the close: the revert inverts the comparison,
and the loosening waits a token millisecond. All four were green before this,
because nothing checked that the pollers were inside the library at all.

The bounded-wait loosening goes from 1 ms to 50 ms. A 1 ms bound only ever lost
a race — the margin was a few hundred lock handoffs — and the same loosening at
50 ms survived on both targets. `holdclose` holds its pollers for 500 ms, so any
bound shorter than the hold now expires with calls still inside. A bound longer
than the hold is not distinguished, which is this entry's honest limit.

A run that skips entries covered those rules with nothing, and exiting 0 let a
win32-x64 run — where every ThreadSanitizer rule is skipped — be read as a green
gate. It exits 2 and says so, with a killed/survived/skipped line.

A red control arrived as a bare colour with the child's output thrown away,
which is how a configuration that could not start at all stayed invisible. The
control now prints what the child said. On linux-x64 the sanitizer
configurations are re-executed through `setarch -R`, because ThreadSanitizer
dies before `main` on the kernel's address-space randomisation; both that and
the seccomp denial behind `personality(ADDR_NO_RANDOMIZE)` stop the run with the
cause and the required invocation instead of being reported as kills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:24:21 +02:00
Meta Vens
62e53df447 Hold host calls inside the bridge so the quiesce is tested, not assumed
The closerace scenario pinned the order of the count and the notify but never
that a host call is counted at all. Deleting the call scope from
`cultmesh_quic_next_event` outright leaves every configuration green — the
pollers finish before the teardown reaches them — and so does inverting the
comparison that makes a positive timeout block, which empties the library before
the close even begins. A scenario that only sleeps and hopes cannot tell any of
that apart from the real thing.

So the bridge says it itself, in development builds only (section 7 of the
header, behind CULTMESH_QUIC_DEBUG_ASSERTS, absent from every shipped binary):
the peak number of host calls counted inside the library, the number counted
when the close began its wait, and a hold that parks a call inside the library
until the scenario releases it. A held call returns the 0 an empty queue would
have and touches the runtime no further, so a bridge that frees the runtime
under it is reported by the scenario rather than crashing inside it.

`holdclose` drives the quiesce through that seam: the calls are counted, they
were still counted when the close began, and the close did not return while they
were held. `closerace` keeps hunting the race and now also refuses to pass when
a poller left before the close began, which is a scenario that raced nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:23:04 +02:00
Meta Vens
51fb449b56 Record the second fix batch and the harness that was never there
The lifetime fix holds under a thread sanitizer and the assertion kills the
deleted wait on both targets. The harness defending all of it does not hold.
Every native kill this cut had reported came from scratchpads that no longer
exist, the committed Linux path dies before its scenario starts and hides the
reason, and the scenario stays green when calls are not counted at all or
when nothing ever parks.

Two rules are defended by nothing committed. They are recorded as undefended
rather than given a number that would look better.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 00:02:05 +02:00
Meta Vens
180917d75b Mutate the native bridge, and name the target each entry is honest on
The mutation suite covered only the TypeScript rules. The bridge's two runtime
lifetime rules now have entries of their own, each with a revert that puts back
the wrong spelling and a loosening that keeps the shape and gives away the
guarantee: a notify moved outside the gate, a notify outside it that fires only
on the last call out, the quiesce wait deleted, and the quiesce wait made
bounded. Their killer is the scenario runner, built through CMake rather than
npm, so the runner now takes a check per target instead of assuming a workspace,
and a target without a dist/ keeps the verified restore without the sentinel.

Every native entry names the platforms it is honest on, and a run elsewhere
reports it as skipped rather than killed. There is no ThreadSanitizer for MSVC,
so the notify rule cannot be seen on win32-x64 at all. Soul's Cut 3 pass found
this the hard way, reporting two mutations killed from a win32-x64 release run
where both in fact survive.

Naming a target on the command line runs only that one, which is what lets the
native entries run inside the Debian 13 container where the TypeScript workspaces
have no installed dependencies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:23:50 +02:00
Meta Vens
f4186e7427 Run the runtime-lifetime scenarios from the repository
The bridge's close path had no committed test. Whether a close is safe against
host threads already inside the library is the one thing about it a host cannot
establish by reading, and every check of it so far lived in a session scratchpad
and is gone.

The scenario runner under tests/ drives that path and nothing else: it parks
pollers in cultmesh_quic_next_event, closes the runtime under them, and asserts
that the close is what woke them. It consumes the bridge through the public
header, as a host does. CULTMESH_QUIC_BUILD_TESTS builds it, off by default,
because a delivery build has no business producing an executable.

CULTMESH_QUIC_DEBUG_ASSERTS, also off by default, makes the bridge state the
quiesce invariant itself rather than leaving it to whatever a sanitizer happens
to catch. Deleting the quiesce wait is otherwise falsifiable only by luck:
AddressSanitizer saw it 4 times in 48 attempts with pollers blocked mid-copy of
large frames, and never on release. With the assertion and pollers blocked it
fails on the first close.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:23:30 +02:00
Meta Vens
19d8d5c948 Say that the committed Unity plugin is the older build
The README claimed a rebuild reproduces the committed Unity plugin DLL. It does
not. That plugin is 40,448 bytes, predates both the static CRT and the move off
Schannel, and still imports VCRUNTIME140.dll and MSVCP140.dll; the current source
builds to 302,080 bytes with no CRT import.

What /Brepro buys is that a build reproduces itself, and the README now says
that, along with the fact that the committed plugin stays the older build until
the Unity package build replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:22:59 +02:00
Meta Vens
53d59296d4 Say what the return code makes portable
Section 3 and cultmesh_quic_last_status both told a host that the portable answer
is the return code. What the mapping makes portable is the sign. The magnitude is
not: the same MsQuic failure carries different numbers on different platforms, a
TLS error being -12032 on win32-x64 and -1126 on linux-x64, and the 16-bit mask
gives QUIC_STATUS_USER_CANCELED and QUIC_STATUS_FILE_NOT_FOUND the same -1002 on
Windows.

The header now says the sign is what a host may branch on, that the magnitude is
diagnostic and platform-specific, and that switching on a particular code
identifies nothing reliably.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:21:50 +02:00
Meta Vens
dc9e12cd38 Print a Windows status without sign-extending it
Every error sentence widened its QUIC_STATUS straight to uint64_t. On Windows
that status is a signed HRESULT, so the sentence carried 0xFFFFFFFF80072B18 where
the status is 0x80072B18. The status is 32 bits on both platforms and now goes
through uint32_t on its way in.

Diagnostics only: the v1 managed connector matches on the wording of these
sentences, not on the hex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:20:55 +02:00
Meta Vens
a4753f4e9f Notify the closer while still holding the gate
A call scope used to drop the lock after decrementing the in-flight count and
notify afterwards. That left a window: the closer's predicate is already true
once the count reaches zero, so a scope preempted between the unlock and the
notify could come back to find the runtime torn down and signal a condition
variable that no longer exists. ThreadSanitizer reports it as a broadcast racing
the destroy.

The decrement and the notify now happen under one lock guard. The closer cannot
return from its wait until the scope releases the gate, so the condition
variable is alive for as long as anything can signal it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:18:26 +02:00
Meta Vens
27ae3cb292 Record Soul's second pass, and rule the surviving mutation killable
The cut does not close. The lifetime model was right in shape and wrong by
one line: the last call notifies after it has released the lock, so the
runtime can be destroyed in between and the wake lands on freed memory. A
sanitizer found it on the unmutated bridge.

The mutation that survived by construction is ruled killable rather than
accepted. Under a sanitizer with pollers blocked mid-copy it fails four times
in forty-eight, so a dev-only assertion makes it fail every time instead. Two
further mutations are honest only on the sanitizer target and the table must
say so, because a mutation whose kill depends on the target is not killed
until it runs there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:58:15 +02:00
Meta Vens
e1ce4915b6 Record the Cut 3 fix batch
The bridge keeps an object alive across the call that names it, closes
each handle exactly once, and quiesces before it frees. The contract is a
header hosts and tests are checked against. Return codes are negative on
every platform, one completion per frame, and every connection's end is
observable. One mutation survived by construction and is handed to Soul
to rule on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:24:51 +02:00
Meta Vens
c729dff2ee Pin the Windows MsQuic to the OpenSSL build, and both of its digests
Q9 ruled A. The Schannel MsQuic refuses QUIC_CREDENTIAL_TYPE_CERTIFICATE_PKCS12
with QUIC_STATUS_NOT_SUPPORTED, and PKCS12 is the only credential type a
provider has on both platforms, so a Windows provider could not open a
listener against the runtime this repo was shipping. The same bridge
binary passes all twenty smoke checks and the v1 gate against the OpenSSL
build, which is what Soul verified and what this now downloads.

Two digests, not one. The zip is pinned by FEE9A664 and 19,179,817 bytes;
the msquic.dll inside it by C17C6581 and 4,181,856 bytes. The earlier
report and the cut map's first record put the zip's digest on the DLL,
which is a pin that checks the archive and then ships a file nobody looked
at. Sizes ride along with both, because a digest that fails tells you less
than a digest and a length that fail together.

The CRT is linked statically. A Node or Unity host without the Visual C++
redistributable cannot load a DLL that imports vcruntime140.dll, and
Windows reports that as "the specified module could not be found" while
naming the module that was found. Confirmed on the rebuild: msquic.dll,
CRYPT32, ntdll, KERNEL32, and no CRT import at all. The DLL goes from
83,456 to 302,080 bytes, which is the price of the host not needing
anything installed.

The Unity plugin's committed msquic.dll is still the Schannel one and this
does not touch it; the next build-unity-package.ps1 run is what replaces
it, and that is not in this batch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:04:27 +02:00
Meta Vens
8c418961c4 Close the three ABI gaps the bridge left open
Event 7 was not one per frame. The bridge writes the stream's kind byte
itself, that send completed as an anonymous event 7, and a host counting
completions saw six for five frames with nothing to tell them apart. The
send request now says which send it was; the kind byte's completion is the
bridge's own business and is dropped. A send MsQuic released without
putting on the wire reports it: event 7 carries a send result in `code`,
completed or canceled, so an aborted send is accounted for rather than
silently missing. Measured: 32 sends queued under an abort, 32 event 7, 28
canceled and 4 completed.

The initiator of a shutdown learned nothing. MsQuic gives it only
shutdown-complete, and the bridge swallowed that, so a host could end its
own connection and never see the id end. Event 4 now has one emitter —
shutdown-complete — and the two initiated-by callbacks record why rather
than announcing it. Each connection id gets exactly one event 4, whoever
ended it, with the code its side passed, and it is emitted after the id
has stopped resolving, so a host told a connection is gone cannot still
resolve it on the next line.

Return codes inverted by platform. QUIC_STATUS is a negative HRESULT on
Windows and a positive errno on POSIX, so a Linux host checking for a
negative code saw every failure as success, against a README that promised
negative. A MsQuic refusal is now -(1000 + (status & 0xffff)) on both, and
cultmesh_quic_last_status hands over the raw bits for diagnostics. On
win32-x64 a garbage PKCS12 returns -12032 with last_status 0x80072B18.
The v1 exports move with it; the managed connector tests the value against
zero and formats it, so the contract it was shipped with holds.

Smaller ones, from the same pass: AppendTrace is v1-only and bounded,
because a long-lived v2 connection grew that string once per MsQuic event
for nothing to read; a listener configuration failure takes the connection
out of the map instead of leaving it naming a handle MsQuic is about to
close; and next_event refuses a null payload with a non-zero capacity
rather than treating the lie as a discard.

Soul's C consumer now compiles against the header and asserts its own
struct and its eleven bound prototypes against it, and the koffi smoke
checks its declarations and the thirteen exports the same way: 20 of 20,
seq clean, cancel clean, the v1 gate 3 passed 1 skipped as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:00:06 +02:00
Meta Vens
47aa7b668b Keep bridge objects alive across the call that names them
FindStream, FindConnection and FindListener took the lock, dropped it, and
handed back a raw pointer. MsQuic's worker freed the object on
shutdown-complete and the host thread dereferenced it on its next send.
One release run in three crashed on each target under an ordinary
peer-driven stream teardown; ASan caught it three times out of three, with
the free inside DestroyStream on a worker and the read inside
stream_send_frame on the host thread. A null check cannot fix that: the
pointer is not null, it is dead.

The maps hold shared_ptr now and a lookup returns a reference the caller
owns for the length of its call. The worker's destroy takes the object out
of its map and drops the map's reference only. The MsQuic handle follows
the object: the destructor closes it exactly once, under the exchange
guard, so the handle is alive for as long as any holder is. A stream holds
its connection, which orders every stream handle's close before its
connection's without a rule anywhere saying so.

runtime_close quiesces. It marks the runtime closing, wakes every blocked
poller, and waits for every in-flight call to return before it closes a
handle or frees a byte; a call that arrives after that is refused with -1.
Waking a poller without waiting for it was how close returned a poller
into freed memory while looking like it supported the pattern.

Two things fall out of the model rather than being fixed on top of it: a
listener that fails partway through opening leaves no map entry and no
handle, because every failure path just drops the local reference, and a
stream is in the map before StreamStart, because the shutdown-complete
that StreamStart can reach on a worker has to find it.

Cut with it: Listener::stopped, V1Client::connection (an unread raw
pointer into the same hazard), and FindListener.

Event 7 is still one too many and the shutdown initiator still learns
nothing; that is the next commit. The rest of Soul's C consumer passes on
win32-x64, including race at 400 streams, which crashed before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:50:48 +02:00
Meta Vens
9a01321a8a Give the QUIC bridge a host-facing header
Soul wrote a C consumer of this ABI from the spec text, because there was
nothing to include. The struct layout, the exports and the conventions
lived in the .cpp and in prose, which is three places for one contract and
no way to notice when they part.

include/cultmesh_quic_native.h is now the contract: the 64-byte event with
every field offset asserted, the exports with their signatures, the id and
return-code conventions, the object lifetime rules, and the threading
rules. The .cpp includes it and keeps none of its own copy. The README
points at it rather than restating it.

The lifetime and return-code sections describe where the bridge is going,
not where it is; the two commits after this one make them true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:43:01 +02:00
Meta Vens
7badce1e4e Record Soul's Cut 3 pass
The bridge's lifetimes are wrong: a lookup hands the host a pointer the
worker can free before the next call. Release crashes on both platforms
under an ordinary stream teardown. Three ABI gaps ride with it, and the
return-code contract inverts between Windows and POSIX. One digest in
this map was the zip's, not the DLL's. All in a fix batch before Cut 4.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:34:24 +02:00
Meta Vens
27446e7761 Rule Q9: pin the OpenSSL MsQuic on Windows
One credential type on both platforms, the provider role works, the ABI
is unchanged. The tree pays four megabytes more and the Unity plugin's
copy diverges until its rebuild. The map's claim that PKCS12 was one type
for both providers is withdrawn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:29:57 +02:00
Meta Vens
aa22b6141a Record the Cut 2 fixes and Cut 3's landing, and raise Q9
The bridge is a runtime-neutral two-role event queue, built and smoked
on both platforms with the Linux artifact named as development-loop only.
Two hangs the old shape never exercised are pinned. One fork stops the
Windows provider: the pinned Schannel flavour does not implement the
PKCS12 credential the spec assumed was one type everywhere, while the
same bridge passes beside the OpenSSL flavour. Recommended: pin OpenSSL
on Windows and pay the tree size.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:07:55 +02:00
Meta Vens
23dc0939e0 Give the bridge a Linux build the deploy host's own distribution can produce
There is no Linux MsQuic binary in any NuGet package, and the v2.5.9 GitHub
release carries only test bundles, so the Windows script's approach does not
transfer. What does exist is Microsoft's Debian 13 pool, which is the same
distribution Yggdrasil runs, and the msquic source tree at the matching tag for
the POSIX headers the NuGet omits.

`scripts/build-quic-native.sh` fetches the pinned `libmsquic_2.5.9_amd64.deb`
and the three headers `msquic.h`, `msquic_posix.h` and `quic_sal_stub.h`, and
verifies all four by SHA-256 the same way `build-quic-native.ps1:14` verifies
the NuGet. A digest that stops matching is a changed dependency, not a warning.

Two details the deb forced. It carries three library files and no licence text,
so the MIT licence is copied from the one this repo already committed for the
Windows plugin — the same text for both platforms. And it ships only
`libmsquic.so.2.5.9`, with no `libmsquic.so` for `-lmsquic` to resolve against,
so the script makes that link in the extracted tree rather than in the image.

The Linux CMake branch links `-fvisibility=hidden`, so only the twelve v2
exports leave the `.so` and the Windows-only v1 five are absent by construction
rather than by discipline, and sets `RPATH $ORIGIN` so the `libmsquic.so.2`
shipped beside the bridge resolves without being installed system-wide.

`build-quic-native.ps1`'s default output directory becomes
`artifacts/quic-native/win32-x64` so the two platforms' outputs sit beside each
other instead of on top of each other. Both existing callers —
`build-unity-package.ps1:80-82` and the Native.Tests csproj `:20-22` — pass an
explicit `-OutputDirectory`, so neither is affected.

Built and smoked in the workstation's Docker `debian:13` engine pinned by
digest: g++ 14.2.0, glibc 2.41, `libgamecult_mesh_quic_native.so` 85,456 bytes
with `NEEDED libmsquic.so.2` and `RUNPATH $ORIGIN`, and the same 15-check v2 FFI
smoke that runs on Windows passes under Node 24 on that image. That is a Linux
build on a Linux host and nothing more: Q6 ruled the release artifact comes from
the Actions job, and this one is the development loop.

A Linux host still needs MsQuic's own runtime dependencies present — libssl3t64,
libnuma1, libxdp1, libnl-route-3-200 — which is now written down, because the
first smoke run failed on a missing libxdp.so.1 and the reason was not obvious
from the error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:04:14 +02:00
Meta Vens
6a091ef65c Make the MsQuic bridge a two-role event queue instead of a Unity client
The bridge was one platform, one role and one host: Windows by construction
(`windows.h`, `__declspec`, `CryptHashCertificate2`), client-only, and shaped
around the managed connector that polls it every 2 ms. Nothing about MsQuic
required that; the shape came from having exactly one caller.

It now carries a C ABI v2 of twelve exports over a single per-runtime event
queue: open a runtime, open listeners and connections on it, open streams and
send frames, and take typed events out of one blocking wait. The event struct is
64 bytes described by layout rather than by a shared header, so a host declares
it in its own FFI and no header travels with the ABI. `next_event` keeps the v1
two-phase convention — return 2, leave the event at the head, report the size
the caller needs — because that is the one part of v1 that was already
host-agnostic.

The rule that shapes everything else is that nothing calls out. MsQuic's worker
threads only push onto the queue and wake that wait, so no host runtime is
entered from a thread it does not own, and the bridge needs to know nothing
about what is on the other side. That is what makes one binary serve both a
Node process through FFI and a Unity player through P/Invoke.

Trust moved out. A v2 client connection defers validation, emits the DER as
event 3 and answers MsQuic only when the host calls
`connection_certificate_complete`; if the host never answers, MsQuic's handshake
timeout closes the connection and event 4 says so. The C code no longer has an
opinion about certificates — except in the v1 shim, where it must.

The five v1 exports are unchanged in signature and behaviour and are now
`#ifdef _WIN32`, matching `CanConnect`'s own Windows gate. A v1 client is a
private v2 runtime holding one connection, and the pin comparison is the single
thing it still decides in C, because that is the contract the shipped Unity
package was built against. `windows.h`, `wincrypt.h` and `CryptHashCertificate2`
appear nowhere outside that block.

Two things the rewrite had to get right that the old shape never exercised.
The v1 pin must be attached before `ConnectionStart`, not after it returns: the
handshake runs on an MsQuic worker thread and reaches the certificate event
immediately on loopback, so a pin applied afterwards loses the race and the
connection silently takes the host path nobody is listening on. And teardown
must close the connection handles itself — `RegistrationClose` blocks until the
app has closed every child handle, and a silent shutdown delivers no shutdown
events at all, so leaning on the callbacks to do it deadlocks the caller
forever. Both were found by the tests hanging, and both now have a mutation that
reproduces them.

Verified: the four v1 tests in tests/GameCult.Mesh.Quic.Native.Tests pass
unchanged (3 passed, 1 skipped for want of an external endpoint), including the
wrong-pin refusal and the departed-client broadcast test. A 15-check FFI smoke
drives the whole v2 path — listener, deferred certificate, both sides connected,
latest-only stream, one frame with FIN, identical bytes out — and passes on
Windows x64 and on Linux x64. Ten source mutations, ten killed, two green
no-op controls.

One open fork, reported separately and not decided here: the pinned Windows
MsQuic is the Schannel build, which refuses
QUIC_CREDENTIAL_TYPE_CERTIFICATE_PKCS12 with QUIC_STATUS_NOT_SUPPORTED, so
`listener_open` cannot open a provider against it. The map assumed PKCS12 was
"one credential type for Schannel and OpenSSL"; the header declares it but the
Schannel provider does not implement it. The identical bridge binary passes the
full provider smoke against OpenSSL MsQuic 2.5.9, so this is a question about
which flavour Windows pins, not about the bridge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:03:01 +02:00
Meta Vens
7a0749b8e2 Say what the eleven stricter endpoint spellings actually are
Both comments around `STRICTER_THAN_CSHARP` called the divergences host
repairs. Most are not. Of the eleven, three carry a backslash and two have no
authority for `System.Uri` to accept; `refusedByCSharpUriShape` refuses those
five on the raw string, before there is a host to repair. Four are genuine host
repairs `URL` would make and C# would not — two trailing dots, two fullwidth
hosts — and the last two throw in `URL` before any rule of ours runs.

The same slip sits in `isProtectedEndpoint`'s own doc comment, over a different
eleven: it says `URL` repairs eleven spellings `System.Uri` refuses, when only
the four carrying a tab or CR/LF are rewritten and the other seven are admitted
as written.

Wording only; no rule changes. It matters because the next reader deciding
whether the pre-check earns its keep will reach for the reason the comment
gives, and "host repairs" points at the wrong half of the list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:21:42 +02:00
Meta Vens
6ed0ff75af Run both halves of the realtime frame parity in CI
The codec's byte parity rests on two vector files written from opposite sides:
`realtime-frame-vectors.json` by the C# reference and decoded by TypeScript,
and `realtime-frame-vectors.ts-written.json` by the TypeScript codec and
decoded by C#. Neither half ran in CI. The TypeScript suite ran nowhere —
cultmesh-ts was not in any job — and the C# test ran nowhere either, because
`cultnet-interop.yml` is windows-latest and main-only and does not filter for
it. A codec that agreed only with itself would have gone green on every push,
which is the exact failure the two-file scheme exists to prevent.

Both now run in the `reactive-and-resource-lifetime` job beside the authority
steps, on windows-latest and ubuntu-latest, for main, codex/** and pull
requests. That job was already the one place the TypeScript side of CultMesh
gets exercised on Linux at all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:21:27 +02:00
Meta Vens
9a4c9396b4 Stop the realtime decoder handing back a view of its own frame
Two mutants the cut-2 pass recorded as equivalent were not. Both notes were
reasoning, and both were wrong in the same way: they argued from the shape of
the code instead of trying the input that breaks it.

The decoder's length refusal reads `payloadLength < 0 || expected !== length`.
The note said the first clause could never fire on its own, because a negative
payload length makes the sum smaller than the frame. It does not: the sum also
carries the three identity lengths, and a negative payload cancels against
them. A 37-byte frame declaring `channelLength=65535, payloadLength=-65535`
sums to exactly 37, matches its own byte length, and without the clause decodes
to three blank identities and an empty payload where `CultMeshRealtimeWireProtocol`
refuses it. Two smaller spellings do the same, `ch=1, pl=-1` over 37 bytes and
`ch=2, pl=-1` over 38. No test covered that shape; one does now, and the
mutation is listed and killed rather than excused.

The payload copy was worse, because the fix and the bug looked identical. The
decoder used `bytes.slice(...)`, which copies a `Uint8Array` — and the existing
test passes one, so it was green — but Node's `Buffer` overrides `slice` with
an alias of `subarray`. A `Buffer` is what a socket read hands a Node caller,
which is exactly how the QUIC plane in cut 4 will reach this function, so every
decoded payload would have been a live view of a frame buffer the transport is
free to reuse.

`Uint8Array.prototype.slice.call(bytes, ...)` was the obvious repair and does
copy, but `%TypedArray%.prototype.slice` takes its result constructor from
`@@species`, and `Buffer`'s is `FastBuffer`: the payload then comes back a
`Buffer` from a `Buffer` frame and a `Uint8Array` from a `Uint8Array` one, so
the same frame compares equal or unequal under `deepStrictEqual` depending on
how it arrived. `new Uint8Array(bytes.subarray(...))` copies and is one type on
both. Three mutations now guard the line — return a view, return a `Buffer`
view, return a species-typed copy — and the first two die only on their own
input type.

Mutation runner: 75 mutations, 75 killed, 0 survivors, three green controls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:20:16 +02:00
Meta Vens
65f2b666ef Run the TypeScript workspaces by the name cultcache-ts actually has
`npm run build:ts` and `test:ts` have been dead since 8cb3b72, not 8dd5a45 as
first recorded. That commit renamed the package to @gamecult/cultcache-ts and
left `run-typescript-workspaces.mjs` asking npm for a workspace called
`cultcache-ts`, which no longer exists; npm refused at the first entry in the
list, so neither script reached any workspace at all. npm resolves a workspace
by manifest name, not directory, so the list moves whenever a package is scoped
— said out loud in the source now, since nothing else will say it.

The mutation runner is renamed `scripts/mutate-cultmesh.mjs`. It stopped being
about the route verifier alone when cut 2 gave it a third target, the realtime
frame codec, and a file named for one of its three targets invites the reader to
assume the other two are somebody else's problem.

`docs/typescript-quic-realtime-cut.md:46-47` names the old path; the map is
Self's and is updated separately.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:19:49 +02:00
Meta Vens
343dffc1de Record Soul's Cut 2 pass and correct one attribution
Byte parity held both ways under Soul's own frames. Three small things
did not: a mutant declared equivalent was not, the payload copy is a view
for a Node buffer, and the frame parity has no CI executor. The breakage
of the aggregate scripts dates from the scoped rename, not the version
bump this map first named.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:58:50 +02:00
Meta Vens
fef767d34a Record Cut 2's landing
The frame codec exists with parity proven both ways from its first
commit, and its test found a real divergence before any consumer could:
the JavaScript decoder deletes a leading byte-order mark the .NET decoder
keeps. Two housekeeping rulings taken: the aggregate TypeScript scripts
have been broken by a rename for two days and get their one-word fix, and
the mutation runner is renamed for what it now covers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:24:11 +02:00
Meta Vens
ae29910b30 Mutate the realtime frame codec, one mutation per rule
A third target in the committed runner, `realtime`, over
`packages/cultmesh-ts/src/realtime-wire.ts`, killed by that package's own
suite. It gets the machinery the other two already have: a no-op control
through the same write path, the sidecar and its verified restore, and the
post-restore rebuild that proves `dist/` no longer carries a mutant, with
`0x31545343` as the sentinel its last mutation removes.

Thirty-seven mutations, one per rule the codec owns: each header field's
offset, width and endianness; the field order the reference fixed; the magic,
the stamped header size and wire version; the delivery ordinal; UTF-8
identities and byte-counting length prefixes; the identity and payload
ceilings; the ALPN, the two QUIC error codes and the two stream kinds; the
four encode refusals from the transport contract; each decode refusal and the
order they fire in; the byte-order mark; and the two aliasing rules, that
decode reads only the viewed bytes of an offset view and that the decoded
payload does not alias the frame. A mutation that changes the encoding
symmetrically still dies, because the vector bytes were written on the other
side.

All seventy-two mutations across the three targets are killed and all three
controls are green.

One mutant is not listed and says why in the table, as the P1363 length guard
already did: removing `payloadLength < 0` from the decoder's length refusal is
equivalent. The clause mirrors the reference but cannot fire alone, because
reaching it means the frame is already 37 bytes and a negative length can then
never make the sum match. It survived when tried.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:20:57 +02:00
Meta Vens
6cbc73fb17 Encode a CultMesh realtime frame in TypeScript
One codec, `packages/cultmesh-ts/src/realtime-wire.ts`, rule for rule what
`CultMeshRealtimeWireProtocol` does: the 37-byte little-endian fixed header
with magic 0x31545343, the delivery byte as the C# enum ordinal, the i64
epoch and sequence, three u16 identity lengths, the i32 payload length, the
stamped header size and wire version, then UTF-8 identities and the payload.
It touches no socket; QUIC stream framing stays transport-owned. The blank
reading in `Validate` is `IsNullOrWhiteSpace`, whose whitespace set is not
`String.prototype.trim`'s, so it borrows `isNullOrWhiteSpaceCSharp` from
`cultnet-ts` rather than growing a second reading of the same rule.

Byte parity is proven in both directions and in two processes, which is what
the authority vectors cost us to learn. `contracts/cultmesh/realtime-frame-vectors.json`
is written by the new C# test under CULTMESH_WRITE_VECTORS=1 and decoded and
re-encoded here; `realtime-frame-vectors.ts-written.json` is written by
`scripts/write-cultmesh-realtime-frame-vectors.mjs` with this codec, over
different identities and epochs, and decoded by the same C# test with the
reference implementation alone. Both files share one field shape, so each
side reads both with one reader. The two cases that would put megabytes into
a committed contract file, identities at exactly 65,535 UTF-8 bytes and a
64 MiB payload, are recorded as a build rule plus the digest of the whole
encoding; both sides build them the same way.

Three things the C# reference gets from its type system and TypeScript does
not, each refused rather than left to surface as a stray runtime error: a
`bigint` outside the i64 range, a delivery string the union cannot spell, and
a leading U+FEFF, which `TextDecoder` deletes by default and
`Encoding.UTF8.GetString` keeps. The last one is a real divergence the test
caught; the decoder now sets `ignoreBOM`. U+FEFF is not whitespace to .NET,
so such an identity passes the transport contract and reaches the wire.

There was no C# test for this codec before; this is also its first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:20:41 +02:00
Meta Vens
fe2555da63 Repair only the file states the mutation runner itself wrote
A SIGKILL leaves the sidecar behind, and the next run wrote it over whatever
the file then held. That is right after a kill and destructive after one: a fix
written to the source between the kill and the next run was reverted to bytes
from before the kill, with a line claiming a repair, and the run then reported
mutation results for code nobody had written.

The mutation table is the record of what a live run can leave behind: the
original (which is the sidecar, and also what the no-op control writes) or the
original with exactly one of that target's mutations applied. Repair now
recognises those and nothing else. A file matching neither stops the run with
its own digest and the sidecar's and changes nothing.

Reproduced both halves against the shared target. Killed mid-mutation, then
appended a line to the source: the rerun exits 1, prints both sha256s, and the
hand-written line is still there afterwards. With the mutant put back and the
sidecar unchanged, the rerun repairs as before and now names the mutation the
dead run was inside.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:35:44 +02:00
Meta Vens
6c933d034b Spell the transcript separator as an escape, not a raw byte
The unit separator that joins protocol ids sat in the source as a literal
U+001F: once in `cultmesh-authority.ts` and twice in its test. Every editor,
every diff and every review renders it as nothing, so `join("")` reads as a
join on the empty string and the transcript looks wrong to anyone checking it
against the C# reference, which spells the same separator as an escape.
The bytes were right, which is exactly why nothing caught it.

The three become escapes. The bytes do not move: the C#-written and the
TypeScript-written route vectors both still verify through the shared module,
and the sorted-transcript test still builds the same expected bytes.

A new test reads both files and refuses any byte below 0x20 that is not LF or
CR, so the class recurs loudly rather than silently. It earned its keep on the
first run by catching a literal U+001F in the comment introducing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:32:07 +02:00
Meta Vens
c64a2da81d Refuse the URL-repaired spellings in the protection gate too
`isLoopbackEndpoint` hands its endpoint to `refusedByCSharpUriShape` before
`URL` sees it; `isProtectedEndpoint` did not, and `URL` repairs eleven
spellings `System.Uri` refuses outright. Every one of them answered protected
here and unprotected in the C# reference, which is the dangerous direction:
the channel-protection gate opened on a route `IsProtected` would have closed.
`wss:x`, `wss:host`, `wss:host:8443`, `wss:/host` and `wss:///host` have no
authority for `System.Uri`; `wss://host\path` and `wss://host:8443\` carry a
backslash; `wss://ho\tst/m`, `wss://host\t/m` and `quic://ho\tst` a tab, and
`wss://host\r\n/m` a CR/LF, all of which `URL` deletes.

Soul's 71 raw-shape spellings go in as a table with the answer the C#
`IsProtected` gave for each on 2026-09-16, and the test asserts the invariant
rather than equality: TypeScript may never call an endpoint protected that C#
refuses. Eleven of the 71 now differ the safe way round, C# protected and this
not, and they are pinned by name rather than excluded, so neither a twelfth
joining them nor one of them flipping passes quietly. Two throw in `URL` before
any rule of ours runs (an IPv6 zone id, a non-breaking-space host); the other
nine are host repairs `refusedByCSharpUriShape` refuses on purpose, because the
same repair would make `isLoopbackEndpoint` lie. None is dialable as a browser
CultMesh route.

The mutation that drops the pre-check from `isProtectedEndpoint` alone, leaving
`isLoopbackEndpoint`'s in place, is killed by that test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:29:13 +02:00
Meta Vens
565ce79a4a Close Cut 1 and start Cut 2
Three Soul passes and three fix batches leave one verifier whose rules
match the C# reference as executed, parity pinned both ways and run in
CI on both operating systems. Three residuals ride with Cut 2. Two
statements this map made about its own batch were overstated and are
corrected: the NUL byte never caused a rewrite, and CI was not green
throughout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:21:52 +02:00
Meta Vens
74fee16003 Record the Cut 1 edge-alignment batch
TypeScript now cleans a route the way the C# constructors do, reads
whitespace with C#'s alphabet, refuses the endpoint spellings the URL
parser would otherwise repair, and names the two signature refusals as
C# names them. Every probe reruns with zero reachable disagreements. Two
scars kept for the postmortem: a sidecar that reverted fixes it was meant
to protect, and a NUL byte that made git read source as binary.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:51:25 +02:00
Meta Vens
0dfef0e5eb Run the TypeScript authority tests in CI, on both operating systems
Nothing ran `npm run test --workspace packages/cultnet-ts` in CI. The shared
route verifier's tests, which are the whole TypeScript side of the CultMesh
authority contract, ran only on a developer's machine, and the browser client's
tests ran nowhere. `cultnet-interop.yml` is windows-latest and triggers on main
only, so even that would not have covered Linux or a pull request.

Both now run in `cultmesh-portability.yml`'s `reactive-and-resource-lifetime`
job, beside the step that verifies the same vectors with the C# reference. That
job already matrixes windows-latest and ubuntu-latest, already triggers on
main, codex/** and pull requests, and already installs the TypeScript workspace
dependencies, so the steps cost two npm runs and no new setup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:40:39 +02:00
Meta Vens
dfe582edbc Kill the browser's local reading of "unsigned", and mutate the browser too
Soul replaced the shared `isUnsignedCertificate` call in the browser's
session-acceptance check with a local empty-string comparison and every one of
the 18 browser tests still passed, because every test used an empty signature.
A whitespace signature is where the two readings part: the shared route
verifier accepts such a route as unsigned on a loopback local-development
endpoint, and an inline comparison then demands a provider proof for the
session the verifier just let through. The test now opens that session with
three spaces, U+0085 and a no-break-space/tab/newline mix as well as the empty
string, and asserts U+FEFF the other way, since it is whitespace to JavaScript
and a signature to C#.

The mutation runner grew a second target. `shared` is the one verifier, killed
by the cultnet-ts tests; `browser` is `packages/cultmesh-browser/src/index.ts`,
killed by the browser tests. A browser mutation cannot be killed by cultnet-ts
tests, so the killer is the target's own workspace, and each target carries its
own sidecar, control, sentinel and post-restore dist assertion.

Two repairs this pass earned:

- The sidecar is now removed inside the `finally`, right after the verified
  restore. A throw mid-run left it behind holding the pre-run bytes, and the
  next run "repaired" the file from them, silently reverting everything fixed
  in between. That happened here, twice, before it was noticed.
- The NUL in the loopback pre-check's strip regex had been written as a literal
  byte rather than an escape. It read the same, but git classified the file as
  binary and stopped normalising its line endings, so the whole file diffed as
  rewritten. Escaped, along with the ASCII host range beside it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:39:50 +02:00
Meta Vens
803ee12a7a Refuse the endpoint spellings WHATWG URL repairs and System.Uri does not
`isLoopbackEndpoint` handed the raw endpoint to `URL` and read the host it got
back. `URL` is a repairing parser and `System.Uri` is not, so seven spellings
were loopback here and remote in C#: a trailing dot on the host, which `URL`
drops; `ws:127.0.0.1`, `ws:/127.0.0.1` and `ws:///127.0.0.1`, whose authority
`URL` infers; a backslash path separator, which `URL` reads as a separator;
fullwidth digits and letters in the host, which `URL` folds to ASCII; and a tab
in the host, which `URL` deletes. Each of those is a local-development bypass:
a route the C# consumer would have demanded a certificate for opened unsigned
here.

`refusedByCSharpUriShape` refuses exactly those, on the raw string, before
`URL` sees it. Leading and trailing C0 controls and spaces around the whole
endpoint are stripped first, because both sides accept them. The spellings
Hands probed earlier and C# accepts -- `127.1`, hex, decimal,
`[::FFFF:7F00:1]`, userinfo, `localhost:8443` -- are untouched.

The doc comment claimed equivalence with `System.Uri.IsLoopback`. It is not
equivalent and now says so: two divergences remain, both unreachable from a
browser CultMesh route, and both named. An IPv6 zone id throws in `URL` and is
a host-local interface name no browser socket can dial; `file:` and `mailto:`
have no `URL` host and are refused by `openSocket`, which takes `ws:` and
`wss:` only.

The test is Soul's endpoint list with the C# answer as the expectation: 82 of
the 88 it probed, the six unreachable ones named in the doc comment instead.

Mutations: the trailing-dot refusal dropped, the scheme-and-authority shape
inverted, the backslash refusal dropped, and the non-ASCII host refusal
dropped -- each killed by the table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:22:06 +02:00
Meta Vens
c4c2330805 Clean the route fields before the transcript, as the C# constructors do
The C# reference signs a transcript over a *constructed* route, and the
constructors clean on the way in: `CultMeshAuthorityRoute.Clean` trims each
protocol id, drops the blank ones, keeps one of each ordinally and sorts
ordinally; `RequireNonEmpty` trims the runtime id and endpoint; `Require`
trims the provider key id and coordinates; `CultMeshRouteCertificate` trims
the Odin key id; a blank generation becomes the runtime id and endpoint
joined by "@". TypeScript cleaned none of it and transcribed the caller's
string, so a route C# signed and accepted was refused here the moment any
field carried padding or a duplicate.

Soul's tamper loop is the evidence: C# accepted a duplicated protocol id, a
padded generation and a padded Odin key id against the committed vector's own
signature, and TypeScript refused all three. The test now reruns those, plus a
padded protocol id, a blank one, a reversed list, a padded endpoint and a
padded provider key id, against the C#-written vector's signature. A padded
Odin key id must also find its root, so the trust map keys on the trimmed id
as the C# dictionary does.

`verseId` is left alone on purpose: it is not a route field, and
`CanonicalRoute` passes the caller's string into `Canonical` untrimmed. The
test pins that too.

`protocolId ?? "cultmesh.documents.v1"` is gone. A C# route with no protocol
ids is `Array.Empty<string>()` and transcribes as the empty string; reading
the handshake's `protocolId` as a one-element list wrote bytes the reference
never writes. `canonicalSession` matches its C# counterpart the other way:
it cleans nothing on the request, because the handshake message is what the
wire carried, and only the endpoint is cleaned, because the one C# passes is
`Route.Endpoint`.

Mutations: the dedupe removed, the blank-id filter removed, the generation
trim removed, the provider key id trim removed, and the Odin key id trim
removed from the lookup -- each killed by its test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:14:35 +02:00
Meta Vens
e67e695a63 Read whitespace and the signature refusals as C# does
Two edge alignments that have to land together, because the test for the
first asserts the messages of the second.

`String.prototype.trim` is not `char.IsWhiteSpace`. JavaScript counts U+FEFF,
which .NET does not; .NET counts U+0085, which JavaScript does not. Every
`trim()` in the shared verifier stood in for a C# `IsNullOrWhiteSpace` or
`Trim()`, so both were wrong at those two code points: a U+0085 signature was
an unsigned route to C# and a bad signature to TypeScript, and a U+FEFF
signature the other way round. `isCSharpWhiteSpace`, `trimCSharp` and
`isNullOrWhiteSpaceCSharp` are that set, and `isUnsignedCertificate` and the
signature trim now use it. The test pins all 34 code points Soul probed on
both sides against the answers .NET 10 gave on 2026-09-16.

The C# `Verify` also has three signature refusals, not one: a signature that
is not base64, a decoded length that is not 64, and a signature that decodes
but does not verify. TypeScript collapsed the first two into the third and
lost the distinction the reference draws. Split, with each message pinned by
its exact C# string. `verifyP256` keeps its own checks: it is the silent
`VerifySession` path, which returns false for all three.

Mutations: the whitespace-set site reverted to `String.prototype.trim`,
U+0085 dropped from the set, and the two new messages merged back into
"invalid" -- each killed by its test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:09:48 +02:00
Meta Vens
b7bb483989 Record Soul's second Cut 1 pass
The cut closes on its invariants. What is left is the gap between the C#
rules as written and as executed: the route is cleaned before the
transcript on one side and transcribed raw on the other, whitespace and
loopback are decided by different alphabets at the edges, and the browser
guard is enforced by name only. All in a final Hands batch before Cut 4
makes a TypeScript signer real.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:02:03 +02:00
Meta Vens
04087c6ab6 Record the browser session alignment and widen the name grep
The browser's local-development short-circuit reads "unsigned" through the
shared predicate, so the two TypeScript sides cannot disagree with each
other or with C#. The negative grep now names the two functions the fix
batch added. The generated contracts file's line-ending churn is recorded
where unassigned findings live.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:45:00 +02:00
Meta Vens
a801e0a441 Share one reading of an unsigned certificate between verifier and browser
The browser's local-development session-proof short-circuit tested
`!route.certificate`, while the shared verifier already treated an empty or
whitespace signature as unsigned, matching C# `Verify` (`:120`). A loopback
route carrying a blank-signature certificate therefore passed the verifier
and was then refused for lacking a provider key, where C# returns
`IsLocalDevelopment` and skips the session proof.

`isUnsignedCertificate` in `cultnet-ts/authority` is now the one owner of
that reading; the verifier and the browser both call it. A browser test pins
the local-development loopback open without a provider proof, the signed
loopback certificate still demanding it, and the remote counterpart refused
in both trust modes. The mutation runner gains two predicate mutations
(whitespace treated as signed; only a missing certificate treated as
unsigned), both killed by the existing whitespace-signature test in
cultnet-ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:43:09 +02:00
Meta Vens
e3f56c183f Record the Cut 1 fix batch
Both parity directions are now pinned in the repo and run in CI, the
public verifier copies its bytes, and the four edge rules match the C#
reference, with the loopback set probed rather than assumed. One scar: the
runner's multi-line anchors had never matched on a CRLF checkout, so two
earlier kill claims rested on an LF-on-disk state. One browser-side
divergence remains and is in Hands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:37:04 +02:00
Meta Vens
0e4f479ac5 Make the mutation runner leave no mutant behind
Every test run inside the runner rebuilds `packages/cultnet-ts/dist/`
from the mutant under test, so after the final restore of the source the
compiled module still carried the last mutant. The runner now rebuilds
the package after the restore and asserts `dist/cultmesh-authority.js`
contains a sentinel the last listed mutation removes, checked against the
list itself so a reordering cannot make the assertion vacuous.

The original bytes are also written to `<target>.mutation-original`
before the first write and removed after the verified restore. A run
killed mid-mutation is repaired from that sidecar at the next run's
start, instead of by whoever notices git status.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:34:31 +02:00
Meta Vens
cf1203a607 Run the C# authority vector checks in CI
`CultMeshAuthorityProofTests` verifies both vector files with the C#
reference, but no workflow ran it. The portability workflow's
reactive-and-resource-lifetime job is the one that already builds and
runs `GameCult.Mesh.Tests` on every push to main and codex branches and
on every pull request, on Windows and Ubuntu, so it gains one step:
`dotnet test --filter FullyQualifiedName~AuthorityProof`. The interop
workflow was not chosen: it runs on Windows only, on main pushes only,
and runs no Mesh tests. It already covers the TypeScript direction
through `npm test --workspace packages/cultnet-ts`.

CULTMESH_WRITE_VECTORS stays unset in CI; the committed files are the
contract.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:28:29 +02:00
Meta Vens
1590145a81 Pin the TypeScript-signed direction of the authority parity check
`contracts/cultmesh/authority-route-vectors.json` proves C# bytes verify
in TypeScript. Nothing in the repo proved the reverse; Soul's probe did it
once in a scratch directory. It is now a committed check in the same shape,
mirrored:

- `scripts/sign-cultmesh-authority-vectors.mjs` builds the route and
  session transcripts with the shared module, signs them with fixed
  TypeScript-owned test keys over awkward inputs (a quic scheme with a
  query, three unsorted protocol ids, non-ASCII generation text, priority
  13), self-verifies, and writes
  `contracts/cultmesh/authority-route-vectors.ts-signed.json`.
- `CultMeshAuthorityProofTests.AuthorityRouteVectorsSignedByTypeScriptVerifyHere`
  reads that file with keys taken from the file, asserts the protocol ids
  are unsorted, that `Validate` and `VerifySessionProof` accept, and that
  a priority-tampered route and a replayed nonce are refused.
- The TypeScript vector test runs over both files, so a transcript change
  on the TypeScript side fails here before C# could trust a stale file.

Checked negatively: one base64 character changed in the committed
signature fails the C# test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:27:06 +02:00
Meta Vens
c2ffc32ff6 Make the TypeScript route verifier the C# rules exactly
verifyP256 handed WebCrypto `view.slice().buffer`. For a Node Buffer,
`slice` is `subarray`, so `.buffer` is the shared pool: a pooled Buffer or
a subarray of a larger allocation verified false while the same bytes as a
plain Uint8Array verified true. Copy exactly the viewed bytes.

Trust rules that had drifted from `CultMeshAuthorityProof.cs`:

- A certificate whose signature is empty or whitespace is an unsigned
  route (C# `Verify` line 120): local-development loopback accepts it,
  everything else refuses with the unsigned message.
- Loopback is `System.Uri.IsLoopback`: any 127.0.0.0/8 host, `::1`, the
  IPv4-mapped `::ffff:127.0.0.1` (only `.1`), and the host names
  `localhost` and `loopback`. WHATWG URL normalises `127.1`, `0x7f000001`
  and `2130706433` the way System.Uri does. Probed on the C# side through
  the public policy before the rule was written; the test pins that set.
- The root lookup runs before the validity window, so an expired route
  under an unknown root is refused as untrusted on both sides.
- A policy listing one Odin key id twice is refused at first use, the
  closest a plain object gets to the C# constructor throwing.

One test per rule, one mutation per rule in the runner's list. A fixture
whose protocol ids are unsorted and whose expected transcript is the
sorted one means removing the sort is now killed.

The runner now matches anchors in the file's own line ending. On an
autocrlf checkout the two multi-line anchors it already had ("route
transcript field order", "session transcript binds the nonce") matched
nothing, so the run threw at those entries instead of testing them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:24:41 +02:00
Meta Vens
c00bed8575 Record Soul's Cut 1 pass
The verifier is sound in both directions, the second one by probe. What
can go wrong silently is the verification story: a mutant left compiled
in an ignored directory, a C# vector check with no CI owner, a public
function that reads a pooled buffer wrong, and four edge rules that drift
from the reference. One map claim about error texts is withdrawn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:19:46 +02:00
Meta Vens
737e13286b Record Cut 1's landing
One route verifier in cultnet-ts, the browser copy deleted, a C#-written
vector file, and a mutation runner with a control. Soul is on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:01:46 +02:00
Meta Vens
062a5c6678 Add a mutation check for the shared route verifier
One mutation per rule the cut map names, applied to the source bytes and
restored from the original bytes with a digest check; a no-op control
runs first through the same write path. A survivor is a rule no test
pins and fails the script.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:59:09 +02:00
Meta Vens
66286b9dfb Delete the browser copy of the route verifier
cultmesh-browser now verifies routes and provider session proofs through
cultnet-ts/authority. Its own verifyAuthorityRoute, canonicalRoute,
canonicalSession, canonicalFields, verifyP256, isLoopbackEndpoint and the
two base64 helpers are gone; the six route and trust types are re-exported
under their old Browser names as aliases of the shared types. randomNonce
stays: it is client behaviour, not verification. All 17 browser tests pass
unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:59:09 +02:00
Meta Vens
e234a64ae9 Add the shared CultMesh route verifier to cultnet-ts
Cut 1 of the TypeScript QUIC realtime plane: one TypeScript
implementation of Odin root lookup, the validity window, the canonical
route and session transcripts, P-256/P1363 verification, loopback
detection and the protected-scheme rule, published as
`cultnet-ts/authority`. Browser-safe by construction: WebCrypto,
TextEncoder, atob/btoa, URL, DataView; no node: import, and a test that
says so.

The protected-scheme rule is the C# rule (wss, https, any scheme
containing "quic") rather than wss-only, so the module is not wrong for
QUIC routes; the browser client still refuses non-ws(s) endpoints itself.

The transcript bytes are pinned by contracts/cultmesh/authority-route-
vectors.json, written by the C# reference under CULTMESH_WRITE_VECTORS=1
with fixed test keys and otherwise re-verified by both runtimes. The
vector carries a route certificate and a provider session proof.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:59:09 +02:00
Meta Vens
1c12136872 Rule Q8: the native binaries are committed
The deploy tarball is a git archive, so committed files are the only ones
that reach Yggdrasil without a second mechanism, and the Windows pair is
already committed for Unity. Four binaries, about eight megabytes, with a
manifest and digests beside them. No operator question is open on this map.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:57:33 +02:00
Meta Vens
4a2fdafa61 Remap Cut 6 under C: binaries in the tree, a release workflow, no registry
The deploy tarball is a git archive of two package directories, so only
committed files reach Yggdrasil and cultmesh-ts does not ride at all
today. Under C the binding resolves binaries from an environment variable
or the package's own native directory, a workflow builds the Linux artifact
in a debian:13 container and attaches it to a release with a manifest and
digests, and the operator smoke ships its own archive. Registry publication
is parked with its seam named. One question remains: whether the four
binaries are committed or fetched at deploy time.

The corrected npm premise is recorded in the operator's words: reachable,
publication deferred by choice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:28:31 +02:00
Meta Vens
8d8ad568aa Re-rule Q5 to C: no registry, because npmjs is unreachable here
Ruling A assumed a publish job whose token is minted by logging in to
npmjs.com, and the operator's subnet is blocked there. That is also why the
existing cultcache-ts publish job has never run. Consumers keep file: paths;
the binaries ship in the CultLib tree the deploy path carries; the Actions
build attaches the Linux artifact to a release. Cut 6 and the lines of Cuts
4 and 7 that assumed a registry are flagged for remapping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:12:08 +02:00
Meta Vens
3c29fc934d Record the Q5, Q6 and Q7 rulings in the QUIC cut map
All three recommendations were taken: unscoped cultnet-ts and cultmesh-ts
with only the native platform packages scoped; the Linux artifact built by
GitHub Actions in a debian:13 container and smoked on Yggdrasil; the lookup
source port plus a static source now, with the Node Odin rendezvous parked.

The status header now says no operator question is open and cut 1 has not
started, so a reader of the header alone knows where the campaign stands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 13:35:11 +02:00
Meta Vens
bf9f6f7874 Map the TypeScript QUIC realtime plane migration
Seven cuts against main at f2cd2eb4, with the largest liability priced first
rather than discovered later. The native binding was the open question: a probe
loaded the committed msquic.dll through koffi under Node 24, drove a handshake
on MsQuic's own threads and closed cleanly, with no addon build and no toolchain
at load time. It is one cut, not the campaign.

The consumer audit found no package at zero external consumers and every
external consumer on a file: path rather than the registry, which is why
registry naming is an operator question rather than an assumption.

Two corrections against the target are recorded in the map rather than absorbed
silently: a cited transport filename that does not exist under that name, and a
registry path the target assumes for cultmesh-ts that has never existed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:16:56 +02:00
Meta Vens
f2cd2eb464 Add the TypeScript QUIC realtime plane migration target
Eureka target for giving cultmesh-ts a QUIC realtime provider and consumer,
byte-compatible with GameCult.Mesh.Quic and the Unity native MsQuic connector.
First consumer: the StreamPixels service publishing to its Unity overlay.

Operator rulings 2026-09-16: overlay character, subathon window and scene
intents travel as QUIC state frames and the costume catalog as an HTTPS
content artifact; shared TypeScript route verification moves to a
browser-safe module; Windows x64 and Linux x64 only; datagrams out of scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:02:33 +02:00
Meta Vens
a0813c6eed Release CultLib Unity 1.0.59 and the Studio 1.3.1
Corrects the 1.0.58/1.3.0 release. Rebuilds the tracked plugins with
scripts/build-unity-package.ps1 -UpdateTemplate so they carry "" as the
unset CultRecordRef wire form in every position (nil still reads as unset)
and the edit-start Record snapshot. The native QUIC connector changes once
to its /Brepro form; later rebuilds reproduce it byte for byte. The Studio
package moves to 1.3.1 and depends on cultlib 1.0.59.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:35:34 +02:00
Meta Vens
2053ab740d Build the native QUIC connector reproducibly with /Brepro
b85a828 changed x86_64/gamecult_mesh_quic_native.dll with no native
source change. The two copies differ in six bytes: the PE header
TimeDateStamp and the debug directory timestamp. The MSVC link stamps the
build time, so every -UpdateTemplate run rewrote the committed DLL.

/Brepro on compile and link replaces the timestamps with content hashes.
Two clean builds from an emptied build root now produce identical bytes,
and the DLL embeds no build path. The committed DLL changes once to the
reproducible form; after that the release byte check covers it like the
managed plugins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:34:23 +02:00
Meta Vens
b03118db2c Space the Studio drawer's ReplaceKey and FreshKey arguments
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:34:23 +02:00
Meta Vens
29bec57021 Decode an edit's Record from the bytes its Document came from
BeginEdit cloned Document eagerly but Record lazily from record.Document,
so a cache change in place before Record's first read made the two copies
come from different states. BeginEdit now serializes the document once and
deserializes Document from those bytes; Record deserializes the same bytes
on first read. Record is the state the edit began from, and costs one
deserialize only when a drawer reads it, with no second serialize.

Each commit still spends the edit, so a slider drag begins an edit per
frame; that serialize was already paid for Document, and Record adds a
deserialize only in frames where a drawer reads it.

The new test mutates the cached document between BeginEdit and the first
Record read; it fails on the lazy clone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:34:23 +02:00
Meta Vens
a92381e589 Write an unset CultRecordRef as "" in every position
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>
2026-09-14 21:34:23 +02:00
Meta Vens
b85a82872e Release CultLib Unity 1.0.58 and the Studio 1.3.0
Rebuilds the tracked plugins with scripts/build-unity-package.ps1
-UpdateTemplate so they carry the unset-ref wire fix and
CultInspectorEdit.Record. The Studio package moves to 1.3.0 for
CultInspector.Record and depends on cultlib 1.0.58.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:18:01 +02:00
Meta Vens
90e7dbee27 Give inspector drawers the record they sit in, read-only
A drawer saw only its value; Aetheria's schematic-shape drawer needs sibling
members of the owning document (texture path, aspect, hull tints).

The model owns the seam: CultInspectorEdit.Record is a second copy of the
stored document as the edit began, cloned on first read and kept for the
edit. The Studio lowers it as CultInspector.Record, so a runtime CultUI
panel gets the same seam from the same edit. The Draw signature is
unchanged, so existing drawers keep compiling.

Why not hand drawers the edit copy the Studio already draws: that copy is
what Commit upserts, so a write through it would be saved without passing
through the returned value, GUI.changed, or read-only metadata. The record
copy is read by nothing, so writing to it saves nothing and changes neither
the edit nor the cache. It does not show edits other drawers made earlier in
the same frame.

The Studio's separate document-type field collapses into the held edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:01:22 +02:00
Meta Vens
452f928bb7 Write an unset CultRecordRef as nil and read it back unset
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>
2026-09-14 21:01:07 +02:00
Meta Vens
0f2c1f04ef Add PCG integer hashes to CultMath and release 0.2.3
CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and
returns float. Unity.Mathematics' hash returned a well-mixed uint, and
seeding from the float hash collapses: a 100x100 position grid gave 1625
distinct values in Aetheria's zone generator.

- math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4)
  follow Jarzynski and Olano, "Hash Functions for GPU Rendering"
  (JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32.
  pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the
  constants checked against pcg-c's pcg_variants.h.
- CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit
  patterns in int3/int4. The float overloads hash IEEE-754 bits via the
  HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at
  0, the paper's route for 2 -> N inputs.
- The HLSL mirror gains the same functions, written over scalar uints
  so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean)
  and in C#. HlslSourceCompatibilityTests now compares int and uint
  arguments and results by value; a wrong shift in the mirror's pcg4d
  gives 208 mismatches.
- Known-answer vectors come from an independent Python transcription.
  pcg3d separates a 100x100 half-unit grid (the full int3). One
  component alone has one birthday collision there, as a 32-bit value
  can. The same grid on the integer lattice has none.
- design.md states that the hashes are integer-exact in HLSL and on the
  CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does.
- Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
Meta Vens
111b918ec0 Make CultMath normalize follow dxc and release 0.2.2
dxc 1.9.0.5402 lowers float2/3/4 normalize(x) to x * Rsqrt(Dot(x, x)),
and DXIL.rst defines Rsqrt as 1 / sqrt(src). A zero vector normalizes
to NaN and a NaN component poisons every component. CultMath divided
by max(length, 1e-20), which returned 0 for a zero vector and a huge
finite component beside a NaN one.

- math.normalize(float2/3/4) is now value * (1 / sqrt(dot(value, value))).
- The HLSL mirror drops cultmath_normalize and its epsilon constant: it
  now equals the normalize intrinsic, and nothing consumed it.
- HlslSemanticsTests pins zero and NaN normalize, the rsqrt bit pattern,
  and zero length/distance. design.md states the rule and notes that
  DXIL.rst's Rsqrt special-value table is a copy of Round's.
- GameCult.Geometry's iso-surface gave degenerate triangles (an inside
  vertex on the isovalue) a zero normal through the old epsilon. It now
  guards them explicitly, with a test.
- quaternion normalize keeps its identity fallback: quaternion is not
  an HLSL type.
- Bump to 0.2.2 and rebuild the tracked Unity DLL and pdb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:04:04 +02:00
Meta Vens
f0605367e0 Release CultMath 0.2.1
Bump the package and assembly version and rebuild the tracked Unity
DLL and pdb from a deterministic build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:16:32 +02:00
Meta Vens
7821e06d55 Add HLSL vector conversions, int clamp, and double frac to CultMath
Aetheria Cut 8a needs int2(float2), clamp(int, int, int), and
frac(double). Fill them in CultMath as dxc compiles them:

- generate-swizzles.ps1 emits same-size conversion constructors and
  math functions between float, int, and bool vectors. dxc lowers
  float to int as fptosi (toward zero; NaN and out-of-range undefined),
  numeric to bool as != 0 (fcmp une, NaN is true), bool to numeric as
  0 or 1. floatN(int) splat keeps floatN(0) from going ambiguous
  against floatN(intN).
- Int and int-vector clamp are min(max(x, a), b), matching dxc's
  IMin(IMax(x, a), b); inverted bounds return b.
- frac(double) keeps double precision. dxc demotes double frac to
  float, as it does exp and lerp; CultMath's existing double overloads
  already diverge that way, and design.md now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:16:32 +02:00
Meta Vens
ae194d5eb2 Release CultLib Unity 1.0.57 with deterministic plugin assemblies
Some checks failed
Publish packages / cultcache-py to PyPI (push) Has been cancelled
Publish packages / cultmesh-py to PyPI (push) Has been cancelled
Publish packages / cultnet-py to PyPI (push) Has been cancelled
Publish packages / @gamecult/cultcache-ts to npm (push) Has been cancelled
Bumps org.gamecult.cultlib to 1.0.57 and rebuilds its managed plugins from
main, so the Unity package carries the store-routing CultCache (one home
store per type, AddBackingStore(CacheBackingStore, Type[]), atomic commit)
instead of the Sep 3 assemblies. Install docs point at the new tag.

The build script now publishes without Source Link or the source revision,
with CI path mapping and cleared Release intermediates. The previous DLLs
named the commit and the worktree they were built in, so committed plugins
could never match a fresh build of the commit that holds them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:18:42 +02:00
Meta Vens
546c919d4a Merge the CultCache store-routing cuts into main
Brings Cuts 1-6 of the Aetheria CultCache migration (one home store per
type, fixed routes, atomic commit with compare-exchange, parked SoA) and
CultMath 0.2.0 onto main, which had moved on with the CultNet contract
consolidation. The two lines touch disjoint files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:16:29 +02:00
Meta Vens
61549aa90b Make cultnet-ts a self-contained package again; revert fa00f3f's rootDir widen
REC-032 Soul finding: dist/contracts.js required "../../../contracts/cultnet/...",
escaping the cultnet-ts package tree entirely. That breaks every installed
consumer: the two-runtime sample script (packs and installs cultnet-ts into a
clean consumer), test-typescript-package-closure.mjs, and Heimdall's pnpm copy
of cultnet-ts (honours `files`, would 404 on its next vendor/CultLib bump).
Only Stonks (file: symlink into the live checkout) happened to still work.

Fix: keep CultLib/contracts/cultnet as the only tracked source, but have
tools/generate-swarm-contracts.mjs wipe and re-copy it into a package-local,
gitignored packages/cultnet-ts/contracts/cultnet/ every time it runs (build,
typecheck, test, test:interop, prepack, the two-runtime script, and Heimdral's
`npm run build` all already invoke it). src/contracts.ts and the swarm-contracts
manifest now import that package-local copy ("../contracts/cultnet/...") instead
of reaching three directories up. package.json gets back `files: ["dist",
"contracts"]` and `prepack` so a packed tarball carries its own schema copy.

Also revert fa00f3f's tsconfig.test.json rootDir widen ("." -> "../..") and the
matching test-script/dist-test path changes and __dirname-relative recomputation
in cultnet-interop.test.ts and runtime-presence-health.test.ts: that widening
was working around dist-test not receiving a copy of the (now out-of-package)
contracts, but nothing in this repo actually needs the wider rootDir once the
schemas are package-local again, and the widened paths never matched what CI's
interop lanes and package.json's own dist-test paths expect --
packages/cultnet-ts/dist-test/test/interop/cultnet-interop.test.js, not
.../dist-test/packages/cultnet-ts/test/interop/.... PR #3 CI run 34766892424 on
7d3a3dc failed the first interop lane on exactly this missing-file defect.
cultnet-interop.test.ts's interopSchemaPath already resolved correctly once
cultNetTsRoot was reverted (it was never touched by fa00f3f) and continues to
point at the canonical CultLib/contracts/cultnet/interop copy, which is fine
since it is only read by the spawned non-TS peer processes, not required() by
this package. The cultmesh-ts shard-catalog $id lookup fix from fa00f3f
(cultnet.shard_catalog_request.v0 -> the CultLib contracts URL id) is unrelated
to the rootDir defect and is left as is.

REC-032, Soul redirect on branch claude/rec-032-cultnet-contracts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 18:03:29 +02:00
Meta Vens
7d3a3dc42c Let the CultNet contract drift guard pass on a clean tree
git grep exits 1 when it finds nothing, and the GitHub pwsh step wrapper
ends the step with that $LASTEXITCODE, so the guard failed on exactly the
clean tree it should accept. End the step with an explicit exit 0 after the
checks; a real drift still throws first.

REC-032.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:52:15 +02:00
Meta Vens
59aca27b67 Add CI drift guard for the CultNet contracts consolidation
REC-032 commit 2: fail cultnet-interop.yml before it installs Node
dependencies if either negative check from commit 1 finds anything:
a *.schema.json file living outside contracts/cultnet/, or a lingering
reference to the retired https://github.com/GameCult/cultnet-ts/contracts
(or .../integration/contracts) namespace anywhere but .voidbot/birth
snapshots. The old-namespace grep uses git grep -I so the deferred
Unity plugin DLL (D4, not rebuilt in this pass) doesn't trip a
permanent false failure.

Verified locally: staging a stray contracts/stray.schema.json and a
stray-id.ts referencing the old namespace makes the guard's logic
report failure for both; removing them makes it pass again against the
real tree.

REC-032.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 17:45:53 +02:00
Meta Vens
fa00f3ffa2 Fix cultnet-ts test build for out-of-package contract location
REC-032 commit 1 follow-up, found during required verification: moving
the CultNet schemas out of packages/cultnet-ts/contracts into
CultLib/contracts/cultnet broke npm test for cultnet-ts. tsconfig.test.json
compiled with rootDir "." (to cover both src/ and test/ as siblings), and
TypeScript's resolveJsonModule copy-into-output behavior only mirrors
files that fall within the declared rootDir. The old contracts/ folder
was a sibling of src/ (inside rootDir), so it got copied into
dist-test/contracts and the "../contracts/x" import resolved. The new
contracts/cultnet is two directories above the package, outside any
possible rootDir short of the repo root, so nothing got copied and the
"../../../contracts/cultnet/x" import 404'd at runtime under dist-test
(the production "dist" build was unaffected, since dist/ and src/ sit at
the same depth under the package).

Fix: widen tsconfig.test.json's rootDir to the CultLib repo root so
resolveJsonModule can find and copy contracts/cultnet into dist-test,
update the "test"/"test:interop" npm scripts for the resulting deeper
dist-test/packages/cultnet-ts/... output path, and correct the two
__dirname-relative computations in test/interop/cultnet-interop.test.ts
and test/runtime-presence-health.test.ts that assumed the old shallower
depth.

Also fixes packages/cultmesh-ts/test/cultmesh.test.ts, which still
looked up the shard-catalog builtins by their old bare
cultnet.shard_catalog_{request,response}.v0 schemaId; CultNetSchemaCatalog
keys by $id, which D1 changed to the CultLib contracts URL, so the
lookup returned undefined.

REC-032, decision D1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 17:43:55 +02:00
Meta Vens
fcc49cffa0 Consolidate CultNet schema contracts into CultLib/contracts/cultnet
REC-032 commit 1: move the canonical 33 TS + witness-artifact-bundle
schema files (34 total) into runtime-neutral CultLib/contracts/cultnet,
move the interop-note schema (and delete the zero-consumer discovery-*
schemas per D3, tagged parked/cultnet-discovery-contracts) into
contracts/cultnet/interop, and delete the 27 C# and 24 Rust duplicate
copies after confirming byte-for-byte equivalence (only
cultnet.snapshot-request differs: TS carries shardId/shardEpoch, a
known drift already resolved in favor of the TS shape).

Rewrite every $id from https://github.com/GameCult/cultnet-ts/contracts/
and .../cultnet-ts/integration/contracts/ to
https://github.com/GameCult/CultLib/contracts/cultnet/(interop/); give
the two shard-catalog schemas URL ids per D1, replacing the bare
cultnet.shard_catalog_*.v0 identifiers; leave ghostlight.agent-state's
Ghostlight-owned $id untouched per D5.

Repoint every runtime's load path and id literal at the new location:
C# csproj EmbeddedResource + LogicalName, Rust include_str! paths,
TS contracts.ts imports + swarm-contracts manifest/tooling + package.json
files/prepack, and the id/path literals in the C#, Rust, Python and
Kotlin interop tests and peers. Python and Kotlin still build their own
schema JSON in-process (D2); only their base constants, id literals,
and (for Kotlin) contentHash literals were swapped. Every Kotlin
contentHash was recomputed from the final canonical files using the
runtime's own stable-stringify + SHA-256 function; all matched the
planner's precomputed values except cultnet.schema-catalog-response,
where the planner's proposed value did not match recomputation and the
freshly computed value was used instead.

REC-032, decisions D1/D2/D3/D5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 17:27:41 +02:00
975 changed files with 268938 additions and 9298 deletions

41
.gitattributes vendored
View file

@ -1,2 +1,43 @@
# Auto detect text files and perform LF normalization
* text=auto
# Shell scripts keep LF in the working tree as well as in the index. Left to
# `text=auto`, a Windows clone writes them with carriage returns, and the
# documented Linux path then dies inside the container in under a second: the
# shebang reads as the interpreter `bash\r`, which does not exist. Nothing about
# that failure names line endings, and the same clone builds and tests fine
# everywhere else, so it reads as a broken image rather than a broken checkout.
#
# A clone checked out before this rule keeps its carriage returns, because the
# blob is unchanged and git has no reason to rewrite the file. The one-line
# repair is in native/GameCult.Mesh.Quic.Native/README.md, beside the container
# path it unblocks.
*.sh text eol=lf
# The committed CultMesh QUIC native binaries (cut 6, packaging) ride the
# deploy `git archive` as raw bytes. `* text=auto` already autodetects them as
# binary by content, but that autodetection is a heuristic; say so explicitly
# so no future text-processing rule (a diff driver, an export filter) can
# apply text conversion and poison the committed digests in SHA256SUMS.
packages/cultmesh-ts/native/**/*.dll binary
packages/cultmesh-ts/native/**/*.so binary
packages/cultmesh-ts/native/**/*.so.* binary
# The digest list and manifest beside them keep LF on every checkout. Left to
# `text=auto`, a Windows clone writes them with carriage returns, and the
# documented `sha256sum -c SHA256SUMS` then fails on every line (each filename
# ends in `\r`) while CI, checking out on Linux, stays green.
packages/cultmesh-ts/native/SHA256SUMS text eol=lf
packages/cultmesh-ts/native/MANIFEST.txt text eol=lf
# CultMath source keeps CRLF in the working tree on every checkout. The release
# gate compares the CultMath.dll it builds with the one org.gamecult.cultmath
# tracks, byte for byte, and the bytes follow the checkout's line endings: the
# pdb's document checksums feed the DLL's pdb id, timestamp and MVID. The
# tracked DLL was built from CRLF source, so CRLF here keeps it valid, and an
# LF-configured or Linux clone builds the same bytes as a Windows one. The index
# stays LF; only the checkout changes.
packages/cultmath/src/** text eol=crlf
# Stores the Python typed-leaf tests write for the C# reference to open.
tests/vectors/**/*.cc binary

View file

@ -95,3 +95,82 @@ jobs:
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
--filter FullyQualifiedName~Document_OneHundredThousandDistinctLeaseCyclesLeaveNoResourcesOrCallbacks
--logger "console;verbosity=minimal"
# The TypeScript side of the CultMesh authority rules. This job is the
# only one that runs on both Windows and Linux for main, codex/** and
# pull requests: cultnet-interop.yml is windows-latest and main-only, so
# until these steps existed the shared verifier's tests never ran on
# Linux at all, and the browser client's never ran in CI on any OS.
# packages/cultnet-ts verifies both vector files (content.test.ts judges both
# C# content files, the Rust one and its own); cultmesh-browser proves
# the browser client reads the shared rules rather than its own, and that its content reader releases only verified
# bytes, requesting the same URLs as the C# HTTPS connector.
# No workflow ran cultcache-ts's tests, and its test script listed two files by hand while a third sat compiled
# and unrun. The wiring check fails when any TypeScript test file is run by no script, and the cache's own suite
# runs here beside the packages built on it.
- name: Check that every TypeScript test file is run by its package
run: node scripts/check-typescript-tests-wired.mjs
- name: Verify the CultCache document owner with the TypeScript cache
run: npm run test --workspace packages/cultcache-ts
# Packs the four TypeScript packages and installs them into a clean consumer, so a package that builds but does
# not install or load fails here.
- name: Verify the packed TypeScript packages install and load from a clean consumer
run: npm run test:pack:ts
- name: Verify the CultMesh authority rules with the TypeScript verifier
run: npm run test --workspace packages/cultnet-ts
# cultmesh-browser's test compiles test/interop, which imports cultmesh-ts/content, so cultmesh-ts builds first,
# as in scripts/run-typescript-workspaces.mjs.
# The realtime frame codec and the HTTP content chunk provider (content.test.ts,
# its answers over node:http). The codec's byte parity rests on two vector files,
# contracts/cultmesh/realtime-frame-vectors.json written by the C#
# reference and decoded by this step, and realtime-frame-vectors.ts-written.json
# written by this codec and decoded by the C# step below. Neither half ran
# in CI on any OS until these two steps existed, so a codec that agreed
# only with itself would have gone green.
- name: Verify the realtime frame codec and the content provider with the TypeScript runtime
run: npm run test --workspace packages/cultmesh-ts
- name: Verify the browser client against the shared TypeScript verifier
run: npm run test --workspace packages/cultmesh-browser
# Both directions of the CultMesh authority vectors: the C#-written
# file (contracts/cultmesh/authority-route-vectors.json) and the
# TypeScript-signed file (authority-route-vectors.ts-signed.json) are
# verified by the C# reference here; the TypeScript side verifies both
# in the step above. CULTMESH_WRITE_VECTORS is never set in CI: the
# committed files are the contract.
- name: Verify the CultMesh authority vectors with the C# reference
run: >-
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
--filter FullyQualifiedName~AuthorityProof
--logger "console;verbosity=minimal"
# The other half of the frame parity: the C# reference decodes and
# re-encodes the vector file the TypeScript codec wrote. A fixture
# authored on one side alone would only agree with itself.
- name: Verify the realtime frame vectors with the C# reference
run: >-
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
--filter FullyQualifiedName~RealtimeWireProtocol
--logger "console;verbosity=minimal"
# The content plane: the reference writes the manifest and record-key
# vectors and judges the manifests the TypeScript packer wrote
# (content-vectors.ts-written.json). This suite also holds the C#/Rust
# content parity, which no CI step ran before this one. CULTNET_WRITE_VECTORS
# is never set in CI: the committed files are the contract.
- name: Verify the content vectors with the C# reference
run: >-
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
--filter FullyQualifiedName~CultMeshContentVectorTests
--logger "console;verbosity=minimal"
# The cross-process HTTPS content parity: the C# transfer service over the unmodified HTTPS connector and the
# TypeScript reader fetch from the TypeScript provider in its own process. Both refuse bytes tampered in flight,
# and the provider refuses a corrupt store.
- name: Verify HTTPS content parity across C#, the TypeScript provider and the TypeScript reader
run: npm run test:interop --workspace packages/cultmesh-browser

View file

@ -12,6 +12,13 @@ jobs:
name: C# / TypeScript / Rust / Python / Kotlin
runs-on: windows-latest
timeout-minutes: 60
# Windows runners default to pwsh, which does not stop when a native command fails: a
# multi-command step reported only its last command, and a failing cultnet-py suite
# went green that way. Git Bash runs as `bash -e -o pipefail`, so every command counts.
# Steps that name `shell: pwsh` check $LASTEXITCODE themselves.
defaults:
run:
shell: bash
env:
DOTNET: dotnet
@ -65,6 +72,36 @@ jobs:
"KOTLIN_JAVA_HOME=$env:JAVA_HOME" | Out-File -FilePath $env:GITHUB_ENV -Append
"$kotlinHome\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
- name: Verify CultNet schema contracts stayed consolidated
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
# packages/cultnet-rs/contracts/cultnet/ holds R-AB's vendored byte copies (the crate must build
# when copied alone). Their drift guard is packages/cultnet-rs/tests/schema_discovery_contracts.rs:74-99.
$tracked = git ls-files '*.schema.json'
if ($LASTEXITCODE -ne 0) { throw "git ls-files failed" }
$stray = $tracked | Where-Object { $_ -notmatch '^(packages/cultnet-rs/)?contracts/cultnet/' }
# -I skips binary files: unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll
# still embeds the retired namespace until it is rebuilt (REC-032 D4, deferred).
$oldNamespace = git grep -I -lE 'GameCult/cultnet-ts/(integration/)?contracts' -- ':!.voidbot'
if ($LASTEXITCODE -eq 2) {
throw "git grep failed"
}
if ($stray) {
Write-Host "Schema files found outside contracts/cultnet/:"
$stray | ForEach-Object { Write-Host " $_" }
}
if ($oldNamespace) {
Write-Host "Files still referencing the retired GameCult/cultnet-ts contracts namespace:"
$oldNamespace | ForEach-Object { Write-Host " $_" }
}
if ($stray -or $oldNamespace) {
throw "CultNet schema contracts drifted out of contracts/cultnet/ (REC-032). See CultLib/contracts/cultnet/ and rewrite any `$id back to https://github.com/GameCult/CultLib/contracts/cultnet/."
}
# git grep exits 1 when nothing matches, and the runner's pwsh wrapper
# ends the step with that $LASTEXITCODE; a clean tree must pass.
exit 0
- name: Install Node dependencies
run: npm install --no-package-lock

View file

@ -10,8 +10,8 @@ name: Publish packages
#
# Tag a release to publish, for example `cultcache-ts-v0.13.6` or
# `cultnet-py-v0.1.0`. The tag's version must match the version in the package
# manifest; the workflow refuses the publish otherwise rather than shipping a
# mislabelled artifact.
# manifest; scripts/release-after-suite.mjs refuses the release otherwise rather
# than shipping a mislabelled artifact.
on:
push:
@ -42,8 +42,12 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# fetch-depth: 0 so the semver check in the release step can see every previously
# published tag, not just the one that triggered this run.
- name: Check out CultLib
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node
uses: actions/setup-node@v4
@ -51,29 +55,30 @@ jobs:
node-version: 24.x
registry-url: https://registry.npmjs.org
# The release-checks suite, which Publish runs, needs .NET for the API gate.
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Install workspace dependencies
run: npm ci
- name: Verify the tag matches the manifest version
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
run: |
manifest="$(node -p "require('./packages/cultcache-ts/package.json').version")"
tagged="${GITHUB_REF_NAME#cultcache-ts-v}"
if [ "$manifest" != "$tagged" ]; then
echo "tag $GITHUB_REF_NAME does not match manifest version $manifest" >&2
exit 1
fi
# Manual runs publish nothing, so they build and test here. A tag run's
# Publish runs these tests itself.
- name: Build and test
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
working-directory: packages/cultcache-ts
run: npm test
# Scoped packages default to restricted even on public npm, and this is
# MIT like the rest of the repo, so say public explicitly.
# One process checks that the tag is the manifest version, runs the
# release-checks suite, the semver policy check (docs/semver-policy.md)
# and npm test, and only then runs npm publish --access public. Only
# npm publish receives the token. It starts without NODE_OPTIONS, so no
# preloaded module runs inside it.
- name: Publish
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
working-directory: packages/cultcache-ts
run: npm publish --access public
run: env -u NODE_OPTIONS node scripts/release-after-suite.mjs npm-publish packages/cultcache-ts "$GITHUB_REF"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@ -105,9 +110,13 @@ jobs:
echo "run=false" >> "$GITHUB_OUTPUT"
fi
# fetch-depth: 0 so the semver check in the release step can see every previously
# published tag, not just the one that triggered this run.
- name: Check out CultLib
if: steps.selected.outputs.run == 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
if: steps.selected.outputs.run == 'true'
@ -115,29 +124,33 @@ jobs:
with:
python-version: "3.12"
- name: Verify the tag matches the manifest version
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')
run: |
manifest="$(python -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('packages/${{ matrix.package }}/pyproject.toml').read_text())['project']['version'])")"
tagged="${GITHUB_REF_NAME#${{ matrix.package }}-v}"
if [ "$manifest" != "$tagged" ]; then
echo "tag $GITHUB_REF_NAME does not match manifest version $manifest" >&2
exit 1
fi
- name: Set up Node
if: steps.selected.outputs.run == 'true'
uses: actions/setup-node@v4
with:
node-version: 24.x
- name: Set up .NET
if: steps.selected.outputs.run == 'true'
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
# Lower layers install from source so a release of one package is tested
# against the sibling revision it was cut with, not whatever PyPI has.
- name: Install and test
- name: Install
if: steps.selected.outputs.run == 'true'
run: |
python -m pip install -e packages/cultcache-py -e packages/cultnet-py -e packages/cultmesh-py
python -m unittest discover -s packages/${{ matrix.package }}/tests
run: python -m pip install --upgrade build -e packages/cultcache-py -e packages/cultnet-py -e packages/cultmesh-py
# One process checks, on a tag, that the tag is the manifest version, runs
# the release-checks suite, on a tag the semver policy check
# (docs/semver-policy.md), and the package's unit tests, and only then
# builds the sdist and wheel. Publish uploads only what this step built,
# so it has nothing to upload unless every check passed. It starts without
# NODE_OPTIONS, so no preloaded module runs inside it.
- name: Build sdist and wheel
if: steps.selected.outputs.run == 'true'
run: |
python -m pip install --upgrade build
python -m build packages/${{ matrix.package }}
run: env -u NODE_OPTIONS node scripts/release-after-suite.mjs python-build packages/${{ matrix.package }} "$GITHUB_REF"
- name: Publish
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')

202
.github/workflows/quic-native.yml vendored Normal file
View file

@ -0,0 +1,202 @@
name: CultMesh QUIC native
# Builds and, on a tag, releases the CultMesh QUIC native binaries that
# packages/cultmesh-ts/native/{win32-x64,linux-x64} commits (cut 6,
# docs/typescript-quic-realtime-cut.md:2108-2159). Under Q8 Option 1 the
# operator downloads a tagged run's Release assets, verifies every digest
# against this workflow's own logs and SHA256SUMS, and commits the bytes;
# nothing here writes to the tree. Under Q6 A, the Linux artifact comes only
# from the pinned debian:13 container below, never from a local or Yggdrasil
# build.
on:
push:
branches:
- main
tags:
- "cultmesh-quic-native-v*"
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
# Push to main and pull_request run this job only: it proves the tree's
# committed bytes still match the manifest they shipped with. A tag push
# also runs it, alongside the two builds and the release.
verify-committed:
name: Verify committed binaries match SHA256SUMS
if: >-
github.event_name == 'pull_request' ||
(github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/cultmesh-quic-native-v')))
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out CultLib
uses: actions/checkout@v4
- name: sha256sum -c SHA256SUMS
run: |
set -euo pipefail
if [ ! -f packages/cultmesh-ts/native/SHA256SUMS ]; then
echo "packages/cultmesh-ts/native/SHA256SUMS is missing" >&2
exit 1
fi
cd packages/cultmesh-ts/native
sha256sum -c SHA256SUMS
# workflow_dispatch and a cultmesh-quic-native-v* tag run the two builds.
# A dry (workflow_dispatch) run uploads artifacts and creates no release.
build-win32-x64:
name: Build win32-x64
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/cultmesh-quic-native-v'))
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Check out CultLib
uses: actions/checkout@v4
- name: Build the native QUIC bridge
shell: pwsh
run: .\scripts\build-quic-native.ps1 -OutputDirectory artifacts\quic-native\win32-x64
- name: Print toolchain version and digests
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$compilerCmake = Get-ChildItem -Recurse -LiteralPath "artifacts\quic-native-build\x64\Release" -Filter "CMakeCXXCompiler.cmake" |
Select-Object -First 1
if ($compilerCmake) {
Select-String -LiteralPath $compilerCmake.FullName -Pattern "CMAKE_CXX_COMPILER_VERSION" |
ForEach-Object { Write-Host $_.Line.Trim() }
}
Get-FileHash -Algorithm SHA256 artifacts\quic-native\win32-x64\gamecult_mesh_quic_native.dll
Get-FileHash -Algorithm SHA256 artifacts\quic-native\win32-x64\msquic.dll
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: quic-native-win32-x64
path: artifacts/quic-native/win32-x64
if-no-files-found: error
build-linux-x64:
name: Build linux-x64
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/cultmesh-quic-native-v'))
runs-on: ubuntu-latest
# Q6 A: this pinned Debian 13 container is the only permitted Linux
# release builder. The digest is the pin; the `debian:13` tag is only a
# label. The artifact links against this image's glibc (2.41); a bare
# ubuntu-latest runner carries glibc 2.39, which is not the deploy host's
# OS (docs/typescript-quic-realtime-cut.md:2192-2197).
container:
image: debian:13@sha256:f324c7ff54321e8d9c588493a20244965938ce0aa50bbd1022d38010e9ffc4b1
timeout-minutes: 20
steps:
- name: Check out CultLib
uses: actions/checkout@v4
- name: Install build tools
run: |
set -euo pipefail
apt-get update
apt-get install -y --no-install-recommends cmake ninja-build g++ curl ca-certificates dpkg
- name: Build the native QUIC bridge
run: ./scripts/build-quic-native.sh artifacts/quic-native/linux-x64
- name: Print toolchain version and digests
run: |
g++ --version
sha256sum artifacts/quic-native/linux-x64/libgamecult_mesh_quic_native.so
sha256sum artifacts/quic-native/linux-x64/libmsquic.so.2
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: quic-native-linux-x64
path: artifacts/quic-native/linux-x64
if-no-files-found: error
# Only this job has write permission, and only a tag push runs it. It
# attaches a draft MANIFEST.txt: the fields both build jobs cannot know
# (host names, MSVC version, the pinned dependency sources) are left for
# the operator to fill in from this run's own logs before committing the
# binaries, per 11.5's manifest paragraph.
release:
name: Release native binaries
needs: [build-win32-x64, build-linux-x64]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/cultmesh-quic-native-v')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Check out CultLib
uses: actions/checkout@v4
- name: Download win32-x64 artifact
uses: actions/download-artifact@v4
with:
name: quic-native-win32-x64
path: release/win32-x64
- name: Download linux-x64 artifact
uses: actions/download-artifact@v4
with:
name: quic-native-linux-x64
path: release/linux-x64
- name: Write SHA256SUMS and a draft MANIFEST.txt
run: |
set -euo pipefail
cd release
# Relative paths matching packages/cultmesh-ts/native/ exactly, so
# this file is the one the operator commits verbatim (11.5, 11.7).
sha256sum \
win32-x64/gamecult_mesh_quic_native.dll \
win32-x64/msquic.dll \
linux-x64/libgamecult_mesh_quic_native.so \
linux-x64/libmsquic.so.2 \
> SHA256SUMS
cat > MANIFEST.txt <<EOF
# Draft written by ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}.
# The operator fills the remaining fields from this run's job logs
# (build-win32-x64 and build-linux-x64 each print their toolchain
# version) before committing packages/cultmesh-ts/native/MANIFEST.txt.
msquic.version=2.5.9
msquic.windows.source=
msquic.linux.source=
bridge.commit=${GITHUB_SHA}
build.workflow_run=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}
build.release=${GITHUB_REF_NAME}
build.win32-x64.host=
build.linux-x64.host=
date=$(date -u +%Y-%m-%dT%H:%M:%SZ)
EOF
# Named after the upload-artifact names above, not after the parked
# scoped per-platform npm package names from the registry-publication
# seam (11.9): these are release archives, not npm packages, and
# 11.7's negative grep for that parked naming must stay clean.
tar -C win32-x64 -czf quic-native-win32-x64.tar.gz .
tar -C linux-x64 -czf quic-native-linux-x64.tar.gz .
- name: Create release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
cd release
gh release create "$GITHUB_REF_NAME" --verify-tag \
--title "$GITHUB_REF_NAME" \
--notes-file MANIFEST.txt \
quic-native-win32-x64.tar.gz \
quic-native-linux-x64.tar.gz \
SHA256SUMS \
MANIFEST.txt

3
.gitignore vendored
View file

@ -6,6 +6,9 @@
# dotenv files
.env
# Stryker.NET mutation-testing output
StrykerOutput/
# User-specific files
*.rsuser
*.suo

View file

@ -49,6 +49,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath.Tests", "packages\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.ConsumerBuild", "tests\GameCult.Caching.ConsumerBuild\GameCult.Caching.ConsumerBuild.csproj", "{D3138F3C-9531-4906-A5D3-74BD857D0685}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.GeneratorConsumer", "tests\GameCult.Caching.GeneratorConsumer\GameCult.Caching.GeneratorConsumer.csproj", "{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.OutsideStoreTests", "tests\GameCult.Caching.OutsideStoreTests\GameCult.Caching.OutsideStoreTests.csproj", "{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -275,6 +279,30 @@ Global
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x64.Build.0 = Release|Any CPU
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.ActiveCfg = Release|Any CPU
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.Build.0 = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.ActiveCfg = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.Build.0 = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.ActiveCfg = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.Build.0 = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.Build.0 = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.ActiveCfg = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.Build.0 = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.ActiveCfg = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.Build.0 = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x64.ActiveCfg = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x64.Build.0 = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x86.ActiveCfg = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x86.Build.0 = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|Any CPU.Build.0 = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x64.ActiveCfg = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x64.Build.0 = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x86.ActiveCfg = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -299,5 +327,7 @@ Global
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
{D3138F3C-9531-4906-A5D3-74BD857D0685} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
EndGlobalSection
EndGlobal

View file

@ -30,6 +30,30 @@ bespoke JSON bridge at every boundary. A frontend is just another CultMesh
client: the daemon publishes a typed Eve/CultUI surface, and any compatible
runtime can lower that surface where it needs to live.
## Entry Points
- C# reference runtime: `src/` (`GameCult.Caching`, `GameCult.Networking`,
`GameCult.Mesh`, `GameCult.Geometry`, `GameCult.Unity`). Build and test with
`dotnet build CultLib.sln` and `dotnet test CultLib.sln`.
- Other runtimes, wire-compatible with the C# CultCache: `packages/cultcache-ts`,
`cultcache-py`, `cultcache-rs`, and the CultNet and CultMesh packages beside
them under `packages/`; portable math in `packages/cultmath`.
- Smallest working path: `src/GameCult.Mesh/docs/durable-node-quickstart.md`.
Application path: `src/GameCult.Mesh/docs/getting-started/README.md` and
`samples/eve-two-runtime`.
## Live And Planned
Live: typed CultCache persistence, CultNet schema-v0 transports and native RUDP,
CultMesh nodes with Verse discovery, shard replication and authority leases,
the geometry documents, and CultUI for Unity. The browser client boundary
(`packages/cultmesh-browser`, `GameCult.Networking.WebSockets`) is proven on
loopback under an explicit `local-development` trust policy by
`samples/eve-browser-network`.
Planned: a complete public Verse proof over deployed infrastructure; the
remaining gates are listed in `src/GameCult.Mesh/docs/browser-verse-transport.md`.
## Which Package Do I Want?
If the job is shared game state, start here before inventing a fourth drawer
@ -97,7 +121,6 @@ The solution includes:
- `GameCult.Logging`: common logging abstraction plus console and file implementations
- `GameCult.Caching`: `DatabaseEntry`-based cache, indexes, global entries, and backing-store abstractions
- `GameCult.Caching.MessagePack`: MessagePack-backed persistence for the cache
- `GameCult.Caching.NewtonsoftJson`: Newtonsoft.Json-backed persistence for the cache
- `GameCult.Math` / CultMath: portable numeric primitives, deterministic random, noise, and shader-parity helpers
- `GameCult.Geometry` / CultGeometry: engine-neutral geometry algorithms plus CultCache-native domain, selected-cut, and mesh artifact documents
- `GameCult.Networking`: encrypted login/register/verify flows, schema-v0 contracts, transport adapters, and native RUDP sessions
@ -125,7 +148,6 @@ src/
GameCult.Logging/
GameCult.Caching/
GameCult.Caching.MessagePack/
GameCult.Caching.NewtonsoftJson/
GameCult.Geometry/
GameCult.Networking/
GameCult.Networking.WebSockets/
@ -344,7 +366,6 @@ Each subproject has a local README with package-specific detail:
- [GameCult.Logging](src/GameCult.Logging/README.md)
- [GameCult.Caching](src/GameCult.Caching/README.md)
- [GameCult.Caching.MessagePack](src/GameCult.Caching.MessagePack/README.md)
- [GameCult.Caching.NewtonsoftJson](src/GameCult.Caching.NewtonsoftJson/README.md)
- [GameCult.Geometry](src/GameCult.Geometry/README.md)
- [GameCult.Networking](src/GameCult.Networking/README.md)
- [GameCult.Mesh](src/GameCult.Mesh/README.md)

View file

@ -0,0 +1,44 @@
{
"nowUnixMilliseconds": 1800000000000,
"odinRoot": {
"keyId": "odin-vector",
"x": "shJLGtmRrW6D87VQKv0xYQekLIfEkeN7dVawwpmIKWM=",
"y": "iR6Gx3aVCghzSyo7TqXqr+1397L73KDNZuzN2cZ55IE="
},
"route": {
"verseId": "aetheria",
"authorityRuntimeId": "aetheria-daemon",
"endpoint": "wss://provider.example/mesh",
"protocolIds": [
"cultmesh.content.v1",
"cultmesh.documents.v1"
],
"priority": 7,
"generation": "vector-generation-1",
"certificate": {
"providerKey": {
"keyId": "provider-vector",
"x": "6XIvgx3W1q7lJFEV0yYbeKWNFgssq54BHBuh9gCwcH8=",
"y": "oi1FiKraueLic+VemzOx18MNwG6thmZMUWeqNw6vNOk="
},
"odinKeyId": "odin-vector",
"issuedAtUnixMilliseconds": 1799999940000,
"expiresAtUnixMilliseconds": 1800003600000,
"signature": "ZHuELFuCNNfbJt1T2mx1D9ZHXyxBedacDetmryMYhWKQxfROncltymkmL5KXl+1h8tDK6NytJCGy+7mYlb/DMA=="
}
},
"sessionProof": {
"request": {
"schemaVersion": "cultmesh.session_open.v2",
"messageId": "vector-message-1",
"sourceRuntimeId": "vector-consumer",
"verseId": "aetheria",
"authorityRuntimeId": "aetheria-daemon",
"protocolId": "cultmesh.documents.v1",
"routeGeneration": "vector-generation-1",
"clientNonce": "VczqjH8NdBaOb+rh8xb6P0xZjWD1el2ReNB8Vbp52ko="
},
"providerKeyId": "provider-vector",
"providerSignature": "6kGxSu/pwcR4lzh/7Av9nhblVIwIwVFzB7kY34vVvkpaFAbAxt8mWqIyZDTftE28G1g+NL4Ea41C3QsuImmkaA=="
}
}

View file

@ -0,0 +1,45 @@
{
"nowUnixMilliseconds": 1800000000000,
"odinRoot": {
"keyId": "odin-ts-vector",
"x": "lqw9TE0EyuDU5Ghe8SafIbBBYkhHMUF7HXJQUc0nXsw=",
"y": "nUj+Xhakih97JiJcTUNMHM9JIo7MVQaauuAkCXph+tw="
},
"route": {
"verseId": "aetheria",
"authorityRuntimeId": "aetheria-daemon",
"endpoint": "cultmesh-state+quic://provider.example:4433/?cert-sha256=ABCDEF",
"protocolIds": [
"cultmesh.documents.v1",
"cultmesh.content.v1",
"cultmesh.realtime.v1"
],
"priority": 13,
"generation": "ts-generation-ünïcode-∞-1",
"certificate": {
"providerKey": {
"keyId": "provider-ts-vector",
"x": "JKyjtWnoE5fhSWbeqr188SbReQWCjssf5mSMEa5pV+E=",
"y": "vtduzojDpbT49sbxBSsBQ+f3PG3rm+CUk/MTy2MM/ws="
},
"odinKeyId": "odin-ts-vector",
"issuedAtUnixMilliseconds": 1799999940000,
"expiresAtUnixMilliseconds": 1800003600000,
"signature": "NS5a7X8fNqTi1CdpPAcJaMmEsozwFRoygTW4z+JlhHFq7uaZcb4G7NwoEpPqo/Fv+UA7pNl5N+qUMuSD/oAq1Q=="
}
},
"sessionProof": {
"request": {
"schemaVersion": "cultmesh.session_open.v2",
"messageId": "ts-vector-message-1",
"sourceRuntimeId": "ts-vector-consumer",
"verseId": "aetheria",
"authorityRuntimeId": "aetheria-daemon",
"protocolId": "cultmesh.documents.v1",
"routeGeneration": "ts-generation-ünïcode-∞-1",
"clientNonce": "HICtAmV++1MarssY04Tkyn1j9qVEcOMp/P5Ce1M0Yec="
},
"providerKeyId": "provider-ts-vector",
"providerSignature": "OgJHkmw3fhPRS5TXxZm94yZvkWAV5TPDuvG+/zIVlnU7OFENZ0Nn0Pdx0IT/vDfEWXD+8/+sCZVFIFwa4tUjsg=="
}
}

View file

@ -0,0 +1,702 @@
{
"artifacts": [
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
32,
32,
77,
105,
120,
101,
100,
32,
67,
97,
115,
101,
47,
80,
97,
116,
104,
32,
32
],
"versionUnits": [
32,
32,
32
],
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:artifact:mixed-case-path:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
304,
115,
116,
97,
110,
98,
117,
108
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:artifact:\u0130stanbul:1.0.0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
913,
931
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:\u03B1\u03C3:v2-beta:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
65279,
98,
111,
109
],
"versionUnits": [
252
],
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:bom:\u00FC:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
133,
110,
101,
108,
133
],
"versionUnits": [],
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:artifact:nel:unversioned:\u0130\u03C3\uD801\uDC28"
},
{
"artifactIdUnits": [
26085,
26412,
35486,
47,
36039,
29987
],
"versionUnits": [
32,
32,
32
],
"contentHash": "sha256:",
"recordKey": "mesh:cdn:artifact:\u65E5\u672C\u8A9E-\u8CC7\u7523:unversioned:"
},
{
"artifactIdUnits": [
55357,
56898,
32,
115,
109,
105,
108,
101
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:artifact:smile:1.0.0:\u017Fha256:abc"
},
{
"artifactIdUnits": [
45,
45,
45
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:cb3f91d54eee30e53e35b2b99905f70f169ed549fd78909d3dac2defc9ed8d3b:v2-beta:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
97,
55296,
98
],
"versionUnits": [
252
],
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:artifact:a-b:\u00FC:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
55296
],
"versionUnits": [],
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:artifact:83d544ccc223c057d2bf80d3f2a32982c32c3c0db8e2674820da5064783fb097:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
120,
178,
121
],
"versionUnits": [
32,
32,
32
],
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:x-y:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
1635,
100
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:\u0663d:1.0.0:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
55297,
56320,
65
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:artifact:a:v2-beta:\u0130\u03C3\uD801\uDC28"
},
{
"artifactIdUnits": [
32,
32,
45,
45,
45,
32,
32
],
"versionUnits": [
252
],
"contentHash": "sha256:",
"recordKey": "mesh:cdn:artifact:fcd2a2fb27957aa9a10d2d9eca16ccbc9c35a76933b03f470211d327f6946c3f:\u00FC:"
},
{
"artifactIdUnits": [
9398
],
"versionUnits": [],
"contentHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:artifact:7d9633ae307dae04a8b362df41dca4b38a653823d69f27a259e7b6928508d3a6:unversioned:\u017Fha256:abc"
},
{
"artifactIdUnits": [
8555
],
"versionUnits": [
32,
32,
32
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:8c0e55c55f47de02406174ec976279a977e2e73ea5aec8ab90f94c40a6ab2d60:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
32,
32,
32
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:1.0.0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:v2-beta:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
252
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:\u00FC:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:\u0130\u03C3\uD801\uDC28"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "sha256:",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:\u017Fha256:abc"
}
],
"chunks": [
{
"chunkHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:chunk:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:chunk:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:chunk:\u0130\u03C3\uD801\uDC28"
},
{
"chunkHash": "sha256:",
"recordKey": "mesh:cdn:chunk:"
},
{
"chunkHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:chunk:\u017Fha256:abc"
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,120 @@
{
"vectors": [
{
"label": "ts multi-chunk body",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 131072,
"tags": [
"a",
"b"
],
"metadata": [
[
"10",
"x"
],
[
"alpha",
"y"
],
[
"2",
"z"
]
],
"body": {
"length": 777777,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:cf8a453a36981321977355448d24df49bcefee22be6e3730b9140e87c29013f9",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBjZjhhNDUzYTM2OTgxMzIxOTc3MzU1NDQ4ZDI0ZGY0OWJjZWZlZTIyYmU2ZTM3MzBiOTE0MGU4N2MyOTAxM2Y5zgAL3jG/YXBwbGljYXRpb24veC1jdWx0bmV0LXRzLXZlY3RvctkhMjAyNi0xMC0xOVQwMDowMDowMC4wMDAwMDAwKzAwOjAwlpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDgAzgACAADZT21lc2g6Y2RuOmNodW5rOjM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDihMJXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAIAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjEzMTA3MpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAQAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjI2MjE0NJXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAYAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjM5MzIxNpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAgAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjUyNDI4OJXZQGEwYmI2OWY0NDg2MWViM2RiNTYwMjZhMWFiYmY5MWZmYTYyMTMyNTA4ZDVhMDZlMGQwZGRjNWNjMjZiZTMzZmPOAAoAAM4AAd4x2U9tZXNoOmNkbjpjaHVuazphMGJiNjlmNDQ4NjFlYjNkYjU2MDI2YTFhYmJmOTFmZmE2MjEzMjUwOGQ1YTA2ZTBkMGRkYzVjYzI2YmUzM2ZjpjY1NTM2MJKhYaFig6IxMKF4pWFscGhhoXmhMqF6"
},
{
"label": "ts empty body",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 131072,
"tags": [
"a",
"b"
],
"metadata": [
[
"10",
"x"
],
[
"alpha",
"y"
],
[
"2",
"z"
]
],
"body": {
"length": 0,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1AL9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRldlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQAA2U9tZXNoOmNkbjpjaHVuazplM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1oTCSoWGhYoOiMTCheKVhbHBoYaF5oTKheg=="
},
{
"label": "ts single chunk, versioned",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "V1 Beta",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 4194304,
"tags": [],
"metadata": [],
"body": {
"length": 1000,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:v1-beta:89f4ff56a25dd1db06a4ce6033603775d705fb96f30f8693733fef602a1ca532",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSnVjEgQmV0YdlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMs0D6L9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRldlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMgDNA+jZT21lc2g6Y2RuOmNodW5rOjg5ZjRmZjU2YTI1ZGQxZGIwNmE0Y2U2MDMzNjAzNzc1ZDcwNWZiOTZmMzBmODY5MzczM2ZlZjYwMmExY2E1MzKhMJCA"
},
{
"label": "ts whitespace version, padded kind and mime",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": " build ",
"version": " ",
"mimeType": " text/plain; charset=utf-8 ",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 4194304,
"tags": [],
"metadata": [],
"body": {
"length": 1000,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:89f4ff56a25dd1db06a4ce6033603775d705fb96f30f8693733fef602a1ca532",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOnIGJ1aWxkIKIgINlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMs0D6L0gIHRleHQvcGxhaW47IGNoYXJzZXQ9dXRmLTggINkhMjAyNi0xMC0xOVQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZXZQDg5ZjRmZjU2YTI1ZGQxZGIwNmE0Y2U2MDMzNjAzNzc1ZDcwNWZiOTZmMzBmODY5MzczM2ZlZjYwMmExY2E1MzIAzQPo2U9tZXNoOmNkbjpjaHVuazo4OWY0ZmY1NmEyNWRkMWRiMDZhNGNlNjAzMzYwMzc3NWQ3MDVmYjk2ZjMwZjg2OTM3MzNmZWY2MDJhMWNhNTMyoTCQgA=="
}
]
}

View file

@ -0,0 +1,177 @@
{
"frames": [
{
"label": "reliable-ordered, small payload",
"fields": {
"channelId": "aetheria.zone-1",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "body-7",
"producerEpoch": 1,
"sequence": 2,
"delivery": "reliable-ordered",
"payload": "AH+A/yo="
},
"encoding": "Q1NUMQABAAAAAAAAAAIAAAAAAAAADwAUAAYABQAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03AH+A/yo=",
"encodedLength": 83,
"sha256OfEncoding": "d04539b99790560e5e570208d92d3ac25f85909a36bfeb190c2080288cc9f1d4"
},
{
"label": "latest-only, small payload",
"fields": {
"channelId": "aetheria.zone-1",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "body-7",
"producerEpoch": 3,
"sequence": 4,
"delivery": "latest-only",
"payload": "AQID"
},
"encoding": "Q1NUMQEDAAAAAAAAAAQAAAAAAAAADwAUAAYAAwAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03AQID",
"encodedLength": 81,
"sha256OfEncoding": "2c875718cf4968979f3cd4083ba2b279e8c03815ad3447c66037ee4530b2814a"
},
{
"label": "unreliable, small payload",
"fields": {
"channelId": "aetheria.zone-1",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "body-7",
"producerEpoch": 5,
"sequence": 6,
"delivery": "unreliable",
"payload": "/g=="
},
"encoding": "Q1NUMQIFAAAAAAAAAAYAAAAAAAAADwAUAAYAAQAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03/g==",
"encodedLength": 79,
"sha256OfEncoding": "b932adf333a0ca73d3a9077e15cf9062070a3084a50a3a30c3baa53312d864b1"
},
{
"label": "empty payload",
"fields": {
"channelId": "aetheria.zone-1",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "body-7",
"producerEpoch": 0,
"sequence": 0,
"delivery": "reliable-ordered",
"payload": ""
},
"encoding": "Q1NUMQAAAAAAAAAAAAAAAAAAAAAADwAUAAYAAAAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03",
"encodedLength": 78,
"sha256OfEncoding": "06c0362f2b395bf7855522190f6a59c45c46947ce7a9a4b8be115cf54a48f419"
},
{
"label": "multi-byte identities",
"fields": {
"channelId": "aetheria.zöne-∞",
"schemaId": "cultmesh.realtime.€1",
"bodyId": "bødy-\uD83D\uDD25",
"producerEpoch": 11,
"sequence": 12,
"delivery": "latest-only",
"payload": "cGF5bG9hZC3iiJ4="
},
"encoding": "Q1NUMQELAAAAAAAAAAwAAAAAAAAAEgAWAAoACwAAACUAAAABAGFldGhlcmlhLnrDtm5lLeKInmN1bHRtZXNoLnJlYWx0aW1lLuKCrDFiw7hkeS3wn5SlcGF5bG9hZC3iiJ4=",
"encodedLength": 98,
"sha256OfEncoding": "90c6ae8f1362d9e000aa57682612bb1d7606cabe2cd0c0969fdd07b816f44b1c"
},
{
"label": "epoch and sequence at long.MaxValue",
"fields": {
"channelId": "aetheria.zone-1",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "body-7",
"producerEpoch": 9223372036854775807,
"sequence": 9223372036854775807,
"delivery": "unreliable",
"payload": "Wg=="
},
"encoding": "Q1NUMQL/////////f/////////9/DwAUAAYAAQAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03Wg==",
"encodedLength": 79,
"sha256OfEncoding": "208068a668b48dc1abde9b3d7f025e7f7b6de6c480faea25c1383b6de9fd37ca"
},
{
"label": "identities at exactly 65535 UTF-8 bytes",
"fields": {
"channelId": {
"repeat": "ä€",
"byteLength": 65535
},
"schemaId": {
"repeat": "ۊ",
"byteLength": 65535
},
"bodyId": {
"repeat": "ñ€",
"byteLength": 65535
},
"producerEpoch": 13,
"sequence": 14,
"delivery": "reliable-ordered",
"payload": "ESI="
},
"encoding": null,
"encodedLength": 196644,
"sha256OfEncoding": "e3c1a353b606e86b35169efb875e76349dcfcb2d036973ee3bbe3c601fbda3e1"
},
{
"label": "64 MiB payload",
"fields": {
"channelId": "aetheria.zone-1",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "body-7",
"producerEpoch": 15,
"sequence": 16,
"delivery": "reliable-ordered",
"payload": {
"ramp": 67108864
}
},
"encoding": null,
"encodedLength": 67108942,
"sha256OfEncoding": "4d99183bdb450af854d87a81ba86a3a4c92ecc1b208675d916f1b75a9f1e7928"
}
],
"malformed": [
{
"label": "truncated header",
"encoding": "Q1NUMQABAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAACUAAAAB",
"message": "Realtime frame header is truncated."
},
{
"label": "wrong magic",
"encoding": "vFNUMQABAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03CQkJ",
"message": "Realtime frame magic is invalid."
},
{
"label": "wire version 2",
"encoding": "Q1NUMQABAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAACUAAAACAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03CQkJ",
"message": "Realtime frame wire version is unsupported."
},
{
"label": "header size 36",
"encoding": "Q1NUMQABAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAACQAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03CQkJ",
"message": "Realtime frame wire version is unsupported."
},
{
"label": "delivery 3",
"encoding": "Q1NUMQMBAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03CQkJ",
"message": "Realtime frame delivery mode is invalid."
},
{
"label": "delivery 255",
"encoding": "Q1NUMf8BAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03CQkJ",
"message": "Realtime frame delivery mode is invalid."
},
{
"label": "payload length -1",
"encoding": "Q1NUMQABAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAgCUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03CQkJ",
"message": "Realtime frame length is invalid."
},
{
"label": "length sum off by one",
"encoding": "Q1NUMQABAAAAAAAAAAIAAAAAAAAADwAUAAYAAwAAACUAAAABAGFldGhlcmlhLnpvbmUtMWN1bHRtZXNoLnJlYWx0aW1lLnYxYm9keS03CQkJAA==",
"message": "Realtime frame length is invalid."
}
]
}

View file

@ -0,0 +1,177 @@
{
"frames": [
{
"label": "ts reliable-ordered, small payload",
"fields": {
"channelId": "ts.zone-a",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "ts-body-1",
"producerEpoch": 21,
"sequence": 22,
"delivery": "reliable-ordered",
"payload": "AAH+/w=="
},
"encoding": "Q1NUMQAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/w==",
"encodedLength": 79,
"sha256OfEncoding": "68c264a42ce34e540e9dbee31dab5b73dec98c767f0b44591ce6a477cd17fc04"
},
{
"label": "ts latest-only, small payload",
"fields": {
"channelId": "ts.zone-a",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "ts-body-1",
"producerEpoch": 23,
"sequence": 24,
"delivery": "latest-only",
"payload": "f4A="
},
"encoding": "Q1NUMQEXAAAAAAAAABgAAAAAAAAACQAUAAkAAgAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xf4A=",
"encodedLength": 77,
"sha256OfEncoding": "3d4f49f4aa2c1f132c3a09ee83555c13e0bc036d636524a972e2829cf417b959"
},
{
"label": "ts unreliable, small payload",
"fields": {
"channelId": "ts.zone-a",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "ts-body-1",
"producerEpoch": 25,
"sequence": 26,
"delivery": "unreliable",
"payload": "pQ=="
},
"encoding": "Q1NUMQIZAAAAAAAAABoAAAAAAAAACQAUAAkAAQAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xpQ==",
"encodedLength": 76,
"sha256OfEncoding": "2c2faec604850959d44ee65d87d2ea59462b667c5d0db8565a8c159d75301851"
},
{
"label": "ts empty payload",
"fields": {
"channelId": "ts.zone-a",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "ts-body-1",
"producerEpoch": 0,
"sequence": 0,
"delivery": "latest-only",
"payload": ""
},
"encoding": "Q1NUMQEAAAAAAAAAAAAAAAAAAAAACQAUAAkAAAAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0x",
"encodedLength": 75,
"sha256OfEncoding": "6d2a04b399f32db4086e728194249b0279330ffa5517cdd82228f691a0f10708"
},
{
"label": "ts multi-byte identities",
"fields": {
"channelId": "ts.zøne-∂",
"schemaId": "cultmesh.realtime.µ1",
"bodyId": "ts-bödy-🧊",
"producerEpoch": 27,
"sequence": 28,
"delivery": "unreliable",
"payload": "dHMtcGF5bG9hZC3iiII="
},
"encoding": "Q1NUMQIbAAAAAAAAABwAAAAAAAAADAAVAA0ADgAAACUAAAABAHRzLnrDuG5lLeKIgmN1bHRtZXNoLnJlYWx0aW1lLsK1MXRzLWLDtmR5LfCfp4p0cy1wYXlsb2FkLeKIgg==",
"encodedLength": 97,
"sha256OfEncoding": "d5d22458bd50a446c7bdce4e2598fe8e5e7bfac3646d8fcd7215363c7c5c8ca1"
},
{
"label": "ts epoch and sequence at long.MaxValue",
"fields": {
"channelId": "ts.zone-a",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "ts-body-1",
"producerEpoch": 9223372036854775807,
"sequence": 9223372036854775807,
"delivery": "reliable-ordered",
"payload": "Bw=="
},
"encoding": "Q1NUMQD/////////f/////////9/CQAUAAkAAQAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xBw==",
"encodedLength": 76,
"sha256OfEncoding": "2abb67a876dc4ed006f33708b4c4dda55a90a6f95da7352749225195b10d45ba"
},
{
"label": "ts identities at exactly 65535 UTF-8 bytes",
"fields": {
"channelId": {
"repeat": "ü☃",
"byteLength": 65535
},
"schemaId": {
"repeat": "☃ü",
"byteLength": 65535
},
"bodyId": {
"repeat": "ç☃",
"byteLength": 65535
},
"producerEpoch": 29,
"sequence": 30,
"delivery": "latest-only",
"payload": "M0Q="
},
"encoding": null,
"encodedLength": 196644,
"sha256OfEncoding": "8bd4cd4c5232339f76b8e394eb93b66e8d14302398ecce2ae5bdb78070ef783a"
},
{
"label": "ts 64 MiB payload",
"fields": {
"channelId": "ts.zone-a",
"schemaId": "cultmesh.realtime.v1",
"bodyId": "ts-body-1",
"producerEpoch": 31,
"sequence": 32,
"delivery": "unreliable",
"payload": {
"ramp": 67108864
}
},
"encoding": null,
"encodedLength": 67108939,
"sha256OfEncoding": "f2c20e0b47a7681fbaeff6fcba024363542805de4cd95eeb7fa9f125ba646af0"
}
],
"malformed": [
{
"label": "ts truncated header",
"encoding": "Q1NUMQAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACUAAAAB",
"message": "Realtime frame header is truncated."
},
{
"label": "ts wrong magic",
"encoding": "Q1OrMQAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/w==",
"message": "Realtime frame magic is invalid."
},
{
"label": "ts wire version 2",
"encoding": "Q1NUMQAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACUAAAACAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/w==",
"message": "Realtime frame wire version is unsupported."
},
{
"label": "ts header size 38",
"encoding": "Q1NUMQAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACYAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/w==",
"message": "Realtime frame wire version is unsupported."
},
{
"label": "ts delivery 3",
"encoding": "Q1NUMQMVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/w==",
"message": "Realtime frame delivery mode is invalid."
},
{
"label": "ts delivery 128",
"encoding": "Q1NUMYAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/w==",
"message": "Realtime frame delivery mode is invalid."
},
{
"label": "ts payload length -1",
"encoding": "Q1NUMQAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAgCUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/w==",
"message": "Realtime frame length is invalid."
},
{
"label": "ts length sum off by one",
"encoding": "Q1NUMQAVAAAAAAAAABYAAAAAAAAACQAUAAkABAAAACUAAAABAHRzLnpvbmUtYWN1bHRtZXNoLnJlYWx0aW1lLnYxdHMtYm9keS0xAAH+/wA=",
"message": "Realtime frame length is invalid."
}
]
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultmesh.session-accepted.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultmesh.session-accepted.schema.json",
"title": "CultMesh Session Accepted",
"type": "object",
"required": ["schemaVersion", "messageId", "accepted", "verseId", "authorityRuntimeId", "protocolId", "routeGeneration", "clientNonce", "providerKeyId", "providerSignature"],

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultmesh.session-open.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultmesh.session-open.schema.json",
"title": "CultMesh Session Open",
"type": "object",
"required": ["schemaVersion", "messageId", "sourceRuntimeId", "verseId", "authorityRuntimeId", "protocolId", "routeGeneration", "clientNonce"],

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultmesh.verse-catalog-request.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultmesh.verse-catalog-request.schema.json",
"title": "CultMesh Verse Catalog Request",
"type": "object",
"required": ["schemaVersion", "messageId"],

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultmesh.verse-catalog-response.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultmesh.verse-catalog-response.schema.json",
"title": "CultMesh Verse Catalog Response",
"type": "object",
"required": ["schemaVersion", "messageId", "verses"],

View file

@ -104,6 +104,17 @@ Not implemented yet:
- cross-runtime rollback/resimulation helpers for simulation frames
- declared CRDT merge policies
## Error Codes
`cultnet.error.v0` carries an optional machine-readable `code`. The set of codes is a closed enum
(`cultnet.error.schema.json`): Rust and TypeScript refuse a message whose code they do not know, they do
not guess. Adding a code is therefore a coordinated change across every runtime, made together: the schema (both
copies), the C# `CultNetErrorMessage` factory, the Rust `CultNetErrorCode`, and a captured-bytes case in
`packages/cultnet-rs/tests/error_message.rs` for it. TypeScript reads the schema. The current codes are
`selection_invalid`, `cursor_stale`, `cursor_invalid`, `reference_outside_target`, `variant_unsupported`,
`unowned_schema` (a write, or a remote put or delete, for a schema and key no shard owns) and
`write_not_authorized` (a peer's put or delete its server's remote write authority did not grant).
## Live Invariants
- CultCache owns document schema, identity, local indexes, and local diffing.
@ -112,6 +123,9 @@ Not implemented yet:
- One primary owns writes for a shard until a stronger policy exists.
- Non-owner writes are rejected or explicitly forwarded. They are not silently
applied.
- A peer's put or delete is applied or forwarded only when the server's
`RemoteWriteAuthority` grants it; none configured refuses every remote write.
The connection key admits a peer to the transport and is never a grant.
- Raw snapshot/put/delete messages must pass through `CultNetDatabase`, not
bypass shard policy.
- The accepted shard log is stored in replica catch-up wire form. Object logs

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-change-raw.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.database-change-raw.schema.json",
"title": "CultNet Raw Database Change Message",
"type": "object",
"required": ["schemaVersion", "messageId", "subscriptionId", "changeKind"],

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-subscribe.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.database-subscribe.schema.json",
"title": "CultNet Database Subscribe Message",
"type": "object",
"required": ["schemaVersion", "messageId", "subscriptionId", "includeSnapshot"],

View file

@ -0,0 +1,24 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.database-subscribe.v1.schema.json",
"title": "CultNet Database Subscribe Message v1",
"type": "object",
"required": ["schemaVersion", "messageId", "subscriptionId", "selection"],
"additionalProperties": false,
"properties": {
"schemaVersion": { "const": "cultnet.database_subscribe.v1" },
"messageId": { "type": "string", "minLength": 1 },
"subscriptionId": { "type": "string", "minLength": 1 },
"selection": { "$ref": "cultnet.selection.schema.json" },
"includeSnapshot": { "type": "boolean", "default": true },
"consumerRuntimeId": { "type": "string", "minLength": 1 },
"bodyIds": {
"type": "array",
"items": { "type": "string", "minLength": 1 }
},
"supportedBodyTransports": {
"type": "array",
"items": { "type": "string", "minLength": 1 }
}
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.database-unsubscribe.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.database-unsubscribe.schema.json",
"title": "CultNet Database Unsubscribe Message",
"type": "object",
"required": ["schemaVersion", "messageId", "subscriptionId"],

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-delete.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.document-delete.schema.json",
"title": "CultNet Document Delete Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-mutation-contract.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.document-mutation-contract.schema.json",
"title": "CultNet Document Mutation Contract",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-put-raw.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.document-put-raw.schema.json",
"title": "CultNet Raw Document Put Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-put.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.document-put.schema.json",
"title": "CultNet Document Put Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-record.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.document-record.schema.json",
"title": "CultNet Document Record",
"type": "object",
"required": [

View file

@ -0,0 +1,46 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.error.schema.json",
"title": "CultNet Error Message",
"description": "R-X (docs/cultnet-selection-cut.md, fix batch 4): CultNetErrorMessage is a MessagePack-CSharp map-mode object, so every declared key is written on the wire, nil included - routingHint, code and details are `required` here the same as cultnet.raw-document-header.schema.json's nullable fields, not optional just because a given refusal leaves them null.",
"type": "object",
"required": [
"schemaVersion",
"error",
"routingHint",
"code",
"details"
],
"additionalProperties": false,
"properties": {
"schemaVersion": { "const": "cultnet.error.v0" },
"error": { "type": "string", "minLength": 1 },
"routingHint": {
"description": "Optional shard routing detail for an authority failure. Null for every selection/cursor refusal code (R-N).",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["shard", "reason"],
"properties": {
"shard": { "type": ["object", "null"] },
"reason": { "type": "string" }
}
},
"code": {
"description": "Machine-readable refusal code (R-N). Additive: a v0 peer that does not know this field ignores it. The set is a closed enum: Rust and TypeScript refuse a message whose code is not listed, so adding a code is one coordinated change across every runtime (this schema, the C# factory, the Rust CultNetErrorCode, and a captured-bytes case for each).",
"type": ["string", "null"],
"enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported", "unowned_schema", "write_not_authorized"]
},
"details": {
"description": "Code-specific structured detail (R-N). Every field is populated only for the code that defines it; the others are null, not omitted.",
"type": ["object", "null"],
"additionalProperties": false,
"required": ["field", "value", "asOf", "current"],
"properties": {
"field": { "description": "selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended. unowned_schema: \"schemaId\". write_not_authorized: \"recordKey\".", "type": ["string", "null"] },
"value": { "description": "selection_invalid: the offending value, when there is one string worth naming. variant_unsupported: the id of the subscription that ended. unowned_schema: the schema id no shard owns. write_not_authorized: the key of the refused write.", "type": ["string", "null"] },
"asOf": { "description": "cursor_stale: the cursor's own minted asOf.", "type": ["integer", "null"], "minimum": 0 },
"current": { "description": "cursor_stale: the answering server's current asOf.", "type": ["integer", "null"], "minimum": 0 }
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.hello.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.hello.schema.json",
"title": "CultNet Hello Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.login-success.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.login-success.schema.json",
"title": "CultNet Login Success Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.login.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.login.schema.json",
"title": "CultNet Login Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.operation-request.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.operation-request.schema.json",
"title": "CultNet Operation Request",
"type": "object",
"required": ["schemaVersion", "messageId", "serviceId", "operation", "payloadSchema", "payload"],

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.operation-response.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.operation-response.schema.json",
"title": "CultNet Operation Response",
"type": "object",
"required": ["schemaVersion", "messageId", "serviceId", "operation", "status", "payloadSchema", "payload"],

View file

@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.raw-document-header.schema.json",
"title": "CultNet Raw Document Header",
"description": "R-S (docs/cultnet-selection-cut.md, fix batch 3): CultNetRawDocumentRecord minus payload/payloadEncoding - CultNetRawDocumentHeader has no such members at all, so a header must not $ref the record schema (which requires them). Matches MessagePack-CSharp's map-mode output: every declared key is written, nil included, so every property here is `required` even where its value may be null.",
"type": "object",
"required": [
"schemaId",
"schemaName",
"schemaVersion",
"schemaContentHash",
"recordKey",
"storedAt",
"sourceRuntimeId",
"sourceAgentId",
"sourceRole",
"tags"
],
"additionalProperties": false,
"properties": {
"schemaId": { "type": "string", "minLength": 1 },
"schemaName": { "type": ["string", "null"], "minLength": 1 },
"schemaVersion": { "type": ["string", "null"], "minLength": 1 },
"schemaContentHash": { "type": ["string", "null"], "minLength": 1 },
"recordKey": { "type": "string", "minLength": 1 },
"storedAt": { "type": "string", "minLength": 1 },
"sourceRuntimeId": { "type": ["string", "null"], "minLength": 1 },
"sourceAgentId": { "type": ["string", "null"], "minLength": 1 },
"sourceRole": { "type": ["string", "null"], "minLength": 1 },
"tags": {
"type": ["array", "null"],
"items": { "type": "string", "minLength": 1 }
}
}
}

View file

@ -0,0 +1,43 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.raw-document-record.schema.json",
"title": "CultNet Raw Document Record",
"description": "R-S (docs/cultnet-selection-cut.md, fix batch 3): matches MessagePack-CSharp's map-mode output for CultNetRawDocumentRecord - every declared key is written, nil included, so every property here is `required` (present on the wire) even where its value may be null.",
"type": "object",
"required": [
"schemaId",
"schemaName",
"schemaVersion",
"schemaContentHash",
"recordKey",
"storedAt",
"payloadEncoding",
"payload",
"sourceRuntimeId",
"sourceAgentId",
"sourceRole",
"tags"
],
"additionalProperties": false,
"properties": {
"schemaId": { "type": "string", "minLength": 1 },
"schemaName": { "type": ["string", "null"], "minLength": 1 },
"schemaVersion": { "type": ["string", "null"], "minLength": 1 },
"schemaContentHash": { "type": ["string", "null"], "minLength": 1 },
"recordKey": { "type": "string", "minLength": 1 },
"storedAt": { "type": "string", "minLength": 1 },
"payloadEncoding": { "const": "messagepack" },
"payload": {
"type": "string",
"contentEncoding": "base64",
"x-cult-runtime-type": "bytes"
},
"sourceRuntimeId": { "type": ["string", "null"], "minLength": 1 },
"sourceAgentId": { "type": ["string", "null"], "minLength": 1 },
"sourceRole": { "type": ["string", "null"], "minLength": 1 },
"tags": {
"type": ["array", "null"],
"items": { "type": "string", "minLength": 1 }
}
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.register.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.register.schema.json",
"title": "CultNet Register Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.sample-change-name.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.sample-change-name.schema.json",
"title": "CultNet Sample Change Name Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.sample-chat.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.sample-chat.schema.json",
"title": "CultNet Sample Chat Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-catalog-request.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.schema-catalog-request.schema.json",
"title": "CultNet Schema Catalog Request Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-catalog-response.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.schema-catalog-response.schema.json",
"title": "CultNet Schema Catalog Response Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-descriptor.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.schema-descriptor.schema.json",
"title": "CultNet Schema Descriptor",
"type": "object",
"required": [

View file

@ -0,0 +1,100 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.selection.schema.json",
"title": "CultNet Typed Selection",
"type": "object",
"additionalProperties": false,
"properties": {
"schemas": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"minItems": 1,
"description": "Absent means every schema. Present and empty is refused at the door (selection_invalid), never treated as \"every schema\": omit the field instead."
},
"keys": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"minItems": 1,
"description": "Absent means every key. Present and empty is refused at the door (selection_invalid), never treated as \"every key\": omit the field instead."
},
"fields": {
"type": "array",
"items": { "$ref": "#/$defs/fieldPredicate" }
},
"cites": { "$ref": "#/$defs/citation" },
"cited": { "$ref": "#/$defs/incoming" },
"projection": {
"type": "string",
"enum": ["header", "document"],
"default": "header"
},
"descending": { "type": "boolean", "default": false },
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 200,
"description": "Clamped to 1..=200 by the answering server, not refused outside that range: an absent value or one outside the bound is treated as the nearer bound (absent = 200)."
},
"cursor": { "type": "string" }
},
"$defs": {
"recordRef": {
"type": "object",
"required": ["schemaId", "recordKey"],
"additionalProperties": false,
"properties": {
"schemaId": { "type": "string", "minLength": 1 },
"recordKey": { "type": "string", "minLength": 1 }
}
},
"fieldPredicate": {
"type": "object",
"required": ["index", "op"],
"additionalProperties": false,
"properties": {
"index": { "type": "string", "minLength": 1 },
"op": { "type": "string", "enum": ["any_of", "lt", "le", "ge", "gt"] },
"values": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
},
"number": {
"type": "string",
"description": "Q-J: a canonical decimal string, never a JSON number. Matches ^-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$ and is not \"-0\"; any other spelling is refused typed at the door (selection_invalid), never normalised.",
"pattern": "^(?!-0$)-?(0|[1-9][0-9]*)(\\.[0-9]*[1-9])?$"
}
},
"oneOf": [
{
"required": ["values"],
"not": { "required": ["number"] },
"properties": { "op": { "const": "any_of" } }
},
{
"required": ["number"],
"not": { "required": ["values"] },
"properties": { "op": { "enum": ["lt", "le", "ge", "gt"] } }
}
]
},
"citation": {
"type": "object",
"required": ["target"],
"additionalProperties": false,
"properties": {
"target": { "$ref": "#/$defs/recordRef" },
"role": { "type": "string", "minLength": 1 }
}
},
"incoming": {
"type": "object",
"required": ["role", "exists"],
"additionalProperties": false,
"properties": {
"role": { "type": "string", "minLength": 1 },
"exists": { "type": "boolean" }
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "cultnet.shard_catalog_request.v0",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.shard-catalog-request.schema.json",
"type": "object",
"required": ["schemaVersion", "messageId"],
"additionalProperties": false,

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "cultnet.shard_catalog_response.v0",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.shard-catalog-response.schema.json",
"type": "object",
"required": ["schemaVersion", "messageId", "shards"],
"additionalProperties": false,

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-request.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.snapshot-request.schema.json",
"title": "CultNet Snapshot Request Message",
"type": "object",
"required": [

View file

@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.snapshot-request.v1.schema.json",
"title": "CultNet Snapshot Request Message v1",
"type": "object",
"required": ["schemaVersion", "messageId", "selection"],
"additionalProperties": false,
"properties": {
"schemaVersion": { "const": "cultnet.snapshot_request.v1" },
"messageId": { "type": "string", "minLength": 1 },
"selection": { "$ref": "cultnet.selection.schema.json" },
"shardId": { "type": "string", "minLength": 1 },
"shardEpoch": { "type": "integer" }
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-response-raw.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.snapshot-response-raw.schema.json",
"title": "CultNet Raw Snapshot Response Message",
"type": "object",
"required": [

View file

@ -0,0 +1,59 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.snapshot-response-raw.v1.schema.json",
"title": "CultNet Snapshot Response Raw Message v1",
"description": "R-S (docs/cultnet-selection-cut.md, fix batch 3): matches MessagePack-CSharp's map-mode output for CultNetSnapshotResponseRawV1Message - every declared key is written, nil included, so every property is `required` (present on the wire) even where its value may be null. `headers`/`documents` are still exactly one populated by projection (C5), but the OTHER key is present too, as an explicit null, not absent - the prior `oneOf: [{required:[headers]},{required:[documents]}]` matched both branches on every real page (both keys always present) and so never validated at all.",
"type": "object",
"required": [
"schemaVersion", "messageId", "matched", "asOf", "next",
"headers", "documents", "edges", "shardId", "shardEpoch", "shardLogSequence"
],
"additionalProperties": false,
"oneOf": [
{
"required": ["headers", "documents"],
"properties": { "headers": { "type": "array" }, "documents": { "type": "null" } }
},
{
"required": ["headers", "documents"],
"properties": { "headers": { "type": "null" }, "documents": { "type": "array" } }
}
],
"properties": {
"schemaVersion": { "const": "cultnet.snapshot_response_raw.v1" },
"messageId": { "type": "string", "minLength": 1 },
"matched": { "type": "integer", "minimum": 0 },
"asOf": { "type": "integer", "minimum": 0 },
"next": { "type": ["string", "null"] },
"headers": {
"type": ["array", "null"],
"items": { "$ref": "cultnet.raw-document-header.schema.json" }
},
"documents": {
"type": ["array", "null"],
"items": { "$ref": "cultnet.raw-document-record.schema.json" }
},
"edges": {
"type": ["array", "null"],
"items": {
"type": "object",
"required": ["from", "role", "to", "payloadEncoding", "payload"],
"additionalProperties": false,
"properties": {
"from": { "$ref": "cultnet.selection.schema.json#/$defs/recordRef" },
"role": { "type": "string", "minLength": 1 },
"to": { "$ref": "cultnet.selection.schema.json#/$defs/recordRef" },
"payloadEncoding": { "type": ["string", "null"] },
"payload": {
"type": ["string", "null"],
"contentEncoding": "base64",
"x-cult-runtime-type": "bytes"
}
}
}
},
"shardId": { "type": ["string", "null"], "minLength": 1 },
"shardEpoch": { "type": ["integer", "null"] },
"shardLogSequence": { "type": ["integer", "null"] }
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-response.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.snapshot-response.schema.json",
"title": "CultNet Snapshot Response Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.transport-profile.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.transport-profile.schema.json",
"title": "CultNet Transport Profile",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.verify.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.verify.schema.json",
"title": "CultNet Verify Message",
"type": "object",
"required": [

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.witness-artifact-bundle.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.witness-artifact-bundle.schema.json",
"title": "CultNet Witness Artifact Bundle",
"type": "object",
"required": [
@ -59,6 +59,7 @@
],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"role": { "type": "string", "minLength": 1 },
"schemaId": { "type": "string", "minLength": 1 },
"schemaVersion": { "type": "string", "minLength": 1 },
@ -74,6 +75,7 @@
],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"role": { "type": "string", "minLength": 1 },
"uri": { "type": "string", "minLength": 1 },
"mediaType": { "type": "string", "minLength": 1 },
@ -92,6 +94,7 @@
],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"stage": { "type": "string", "minLength": 1 },
"startedAt": { "type": "string", "minLength": 1 },
"completedAt": { "type": "string", "minLength": 1 },

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-note.schema.json",
"$id": "https://github.com/GameCult/CultLib/contracts/cultnet/interop/cultnet.interop-note.schema.json",
"title": "CultNet Interop Note",
"type": "object",
"required": [

View file

@ -0,0 +1,334 @@
{
"vectors": [
{
"name": "any_of_plus_ge_conjunction",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHOShKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpndlYXBvbqZudW1iZXLAhKVpbmRleKRtYXNzom9womdlpnZhbHVlc8CmbnVtYmVyoTWlY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq"
],
"matched": 1,
"hasNext": false,
"expectedEdges": []
},
{
"name": "descending_with_limit",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8OlbGltaXQDpmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema",
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/zz-tie-schema"
],
"matched": 18,
"hasNext": true,
"expectedEdges": []
},
{
"name": "gt_numeric_boundary",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRtYXNzom9womd0pnZhbHVlc8CmbnVtYmVyoTGlY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/b-hi",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
],
"matched": 4,
"hasNext": false,
"expectedEdges": []
},
{
"name": "incoming_exists_true",
"selectionMessagePackBase64": "iadzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key"
],
"matched": 2,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"payloadBase64": null
},
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"payloadBase64": null
}
]
},
{
"name": "incoming_negation",
"selectionMessagePackBase64": "iadzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema"
],
"matched": 7,
"hasNext": false,
"expectedEdges": []
},
{
"name": "shared_index_value_three_rows",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpnNoYXJlZKZudW1iZXLApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/shared-2",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3"
],
"matched": 3,
"hasNext": false,
"expectedEdges": []
},
{
"name": "float_tie_exact_ge",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRtYXNzom9womdlpnZhbHVlc8CmbnVtYmVyqjIyMzM3NTkuMjWlY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
],
"matched": 1,
"hasNext": false,
"expectedEdges": []
},
{
"name": "hop_by_role_cites_exact_id",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzkadjaXRlci0xpmZpZWxkc8ClY2l0ZXOCpnRhcmdldIKoc2NoZW1hSWTZR3NoYTI1NjpmZjM1ZjNkOWQ2ZGUzZjEwNjZiOTM2OWU3M2ZlNmM3NjQ4ZTg4MzY4ZDE4ZWZjMjI3NWM5OTQ3MzBjZDg0Y2ZmqXJlY29yZEtleaRhLWVxpHJvbGWmRGVzaWdupWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"payloadBase64": null
}
]
},
{
"name": "cites_target_by_name_alias",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzkadjaXRlci0xpmZpZWxkc8ClY2l0ZXOCpnRhcmdldIKoc2NoZW1hSWSpbGVhZl9hLnY5qXJlY29yZEtleaRhLWVxpHJvbGWmRGVzaWdupWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"payloadBase64": null
}
]
},
{
"name": "schemas_by_name_alias",
"selectionMessagePackBase64": "iadzY2hlbWFzkalsZWFmX2Eudjmka2V5c8CmZmllbGRzwKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema"
],
"matched": 9,
"hasNext": false,
"expectedEdges": []
},
{
"name": "astral_key_code_point_order",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "header_projection",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "document_projection",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26oZG9jdW1lbnSqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/\uD83D\uDE02-row"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "row_tiebreak_schema_id_before_record_key_at_tied_ordinal",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORqnRpZS1zY2hlbWGmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/zz-tie-schema",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "shared_key_narrow_ref_resolves_leaf_within_target",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzkbVuYXJyb3ctY2l0ZXItYWNjZXB0ZWSmZmllbGRzwKVjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5p2R1cC1rZXmkcm9sZapuYXJyb3dfcmVmpWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:ee192f6526a8f8cdec7044551aa2c8e28c612426766bb270605023bc94e2614d/narrow-citer-accepted"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:ee192f6526a8f8cdec7044551aa2c8e28c612426766bb270605023bc94e2614d/narrow-citer-accepted",
"role": "narrow_ref",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"payloadBase64": null
}
]
},
{
"name": "shared_key_design_resolves_the_first_in_target_candidate_in_row_order",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzkbRjaXRlci1ib3RoLWluLXRhcmdldKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlk2UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqlyZWNvcmRLZXmnZHVwLWtleaRyb2xlpkRlc2lnbqVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": null,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"payloadBase64": null
}
]
},
{
"name": "refuses_empty_schemas_list",
"selectionMessagePackBase64": "iadzY2hlbWFzkKRrZXlzwKZmaWVsZHPApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": "schemas",
"expectedEvaluationRefusal": null,
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "refuses_blank_key_entry",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzkqRhLWVxoyAgIKZmaWVsZHPApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": "keys",
"expectedEvaluationRefusal": null,
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "refuses_unmatched_cites_target_schema",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlkvHNoYTI1Njpub3QtYS1kZWNsYXJlZC1zY2hlbWGpcmVjb3JkS2V5pGEtZXGkcm9sZcClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
"asOf": 1,
"expectedRefusalField": "cites.target.schemaId",
"expectedEvaluationRefusal": null,
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "refuses_cites_target_by_hash_shaped_alias",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlk2UpzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZi52MalyZWNvcmRLZXmkYS1lcaRyb2xlwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": "cites.target.schemaId",
"expectedEvaluationRefusal": null,
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "shared_key_narrow_ref_refuses_when_only_candidate_is_outside_target",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzkbRuYXJyb3ctY2l0ZXItcmVmdXNlZKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlk2UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqlyZWNvcmRLZXmrbmFycm93LW9ubHmkcm9sZapuYXJyb3dfcmVmpWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedEvaluationRefusal": "reference_outside_target",
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
}
]
}

View file

@ -0,0 +1,105 @@
{
"//": "CultNet typed selection, Cut 1 fix batch (docs/cultnet-selection-cut.md, Self's rulings for the Cut 1 fix batch, R-E and the shared-fixture ruling, 2026-09-22). The one row set both the C# reference and cultnet-rs build their parity vectors from. schemaId is each schema's real SHA-256 content-hash id, exactly as GameCult.Caching.CultDocumentRegistry computes it for the fixture's C# document types (leaf_a=ParityLeafA, leaf_b=ParityLeafB, citer=ParityCiter, citer_narrow=ParityCiterNarrow, tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs) - CultCache.cs's Sha256(semanticFingerprint), which carries no '.vN' suffix of its own; the version is folded into the hash, never appended as text. Both writers read this file instead of building their own fixture rows.",
"//aliases": "Two DIFFERENT alias forms, now deliberate rather than a cross-runtime defect (S-12, 2026-09-22: the Rust alias port made this so - see docs/cultnet-selection-cut.md, 'Self's ruling, 2026-09-22: the C# reference's alias rule is the rule'). Both runtimes resolve a schema alias the same one way: strip a trailing .vN and compare the remainder against the descriptor's declared SchemaName (CultNetSchemaAliasMatching.Matches(candidate, CultDocumentDescriptor), src/GameCult.Networking/CultNetDatabase.cs:74-87; cultnet_rs::selection::schema_alias::matches ports it name-for-name and deletes the hash-stripping overload it used to carry). nameAlias is <SchemaName>.v9 and resolves through both runtimes under that one rule. hashAlias is <schemaId>.v1 and is refused as unmatched by both - it is not a wire form either runtime's production call sites ever produce or accept, and it is kept in the fixture specifically to pin that neither runtime resolves it.",
"//references": "R-AF, 2026-09-23: a reference's `references[].targetKey` is the only thing a stored edge actually carries on the wire - GameCult.Caching.MessagePack.CultRecordRefFormatter<T> serializes a CultRecordRef<T> as a bare key string (probed: MessagePackSerializer.Serialize on a real Design field produced the single msgpack string 'a-eq', nothing else). This fixture used to also carry a `targetSchema` per reference; it was never anything but documentation - SelectionParityVectorTests.BuildDocument and cultnet-rs's build_row both read only targetKey to construct the edge - and it is removed rather than kept as a field the wire does not have. Resolving which row an edge names is by targetKey alone, filtered to the declared role's target leaves (D9); see citer_narrow below for the shared-key shape that rule is pinned against.",
"schemas": {
"leaf_a": {
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"nameAlias": "leaf_a.v9",
"hashAlias": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff.v1",
"indexes": [
{ "name": "kind", "numeric": false },
{ "name": "mass", "numeric": true }
]
},
"leaf_b": {
"schemaId": "sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42",
"nameAlias": "leaf_b.v9",
"hashAlias": "sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42.v1",
"indexes": [
{ "name": "kind", "numeric": false },
{ "name": "mass", "numeric": true }
]
},
"citer": {
"schemaId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc",
"nameAlias": "citer.v9",
"hashAlias": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc.v1",
"roles": [
{ "name": "Design", "targets": ["leaf_a", "leaf_b"] }
]
},
"citer_narrow": {
"//": "R-AF: a citer whose declared reference target is one leaf only (leaf_a), so a record key shared by a leaf_a row and a leaf_b row has exactly one candidate inside the declared target and exactly one outside it - the shape neither runtime's parity vectors covered before this cut (zero duplicate record keys across the original ten rows).",
"schemaId": "sha256:ee192f6526a8f8cdec7044551aa2c8e28c612426766bb270605023bc94e2614d",
"nameAlias": "citer_narrow.v9",
"hashAlias": "sha256:ee192f6526a8f8cdec7044551aa2c8e28c612426766bb270605023bc94e2614d.v1",
"roles": [
{ "name": "narrow_ref", "targets": ["leaf_a"] }
]
}
},
"rows": [
{ "schema": "leaf_a", "key": "a-lo", "ordinal": 1, "fields": { "kind": "weapon", "mass": "4" } },
{ "schema": "leaf_a", "key": "a-eq", "ordinal": 2, "fields": { "kind": "weapon", "mass": "5" } },
{ "schema": "leaf_b", "key": "b-hi", "ordinal": 3, "fields": { "kind": "armor", "mass": "6" } },
{ "schema": "leaf_a", "key": "shared-1", "ordinal": 4, "fields": { "kind": "shared", "mass": "1" } },
{ "schema": "leaf_b", "key": "shared-2", "ordinal": 5, "fields": { "kind": "shared", "mass": "1" } },
{ "schema": "leaf_a", "key": "shared-3", "ordinal": 6, "fields": { "kind": "shared", "mass": "1" } },
{
"schema": "citer",
"key": "citer-1",
"ordinal": 7,
"references": [ { "role": "Design", "targetKey": "a-eq" } ]
},
{
"//": "R-D: 2233759.25 is exactly representable as an f32 and sits on a round-to-even tie between two shortest-form spellings; its exact canonical decimal is 2233759.25 either way. kind=tie so no weapon/armor/shared case can sweep it up.",
"schema": "leaf_a", "key": "tie-mass", "ordinal": 8, "fields": { "kind": "tie", "mass": "2233759.25" }
},
{
"//": "R-C: BMP private-use key (U+E000), same ordinal as the astral row below so the tiebreak - not the ordinal - decides order. kind=unicode, mass=0 so no unfiltered or mass>1 case reaches these.",
"schema": "leaf_a", "key": "-row", "ordinal": 9, "fields": { "kind": "unicode", "mass": "0" }
},
{
"//": "R-C: astral key (U+1F602, encoded as a surrogate pair whose high half 0xD83D is less than 0xE000 - a UTF-16 code-unit compare sorts this first; code-point order sorts it after the BMP row above).",
"schema": "leaf_a", "key": "😂-row", "ordinal": 9, "fields": { "kind": "unicode", "mass": "0" }
},
{
"//": "R-AF shape 1: the only row at this key is outside citer_narrow's declared target (leaf_a only) - resolving the edge below must refuse reference_outside_target in both runtimes, not silently accept an empty page. kind=narrow-fixture (not \"shared\") so this row never sweeps into shared_index_value_three_rows, which pins an exact count of 3.",
"schema": "leaf_b", "key": "narrow-only", "ordinal": 10, "fields": { "kind": "narrow-fixture", "mass": "1" }
},
{
"//": "R-AF shape 2: two rows share this key, one inside citer_narrow's declared target and one outside it - resolving the edge below must accept, deterministically picking the leaf_a row, in both runtimes.",
"schema": "leaf_a", "key": "dup-key", "ordinal": 11, "fields": { "kind": "narrow-fixture", "mass": "1" }
},
{
"schema": "leaf_b", "key": "dup-key", "ordinal": 12, "fields": { "kind": "narrow-fixture", "mass": "1" }
},
{
"schema": "citer_narrow",
"key": "narrow-citer-refused",
"ordinal": 13,
"references": [ { "role": "narrow_ref", "targetKey": "narrow-only" } ]
},
{
"schema": "citer_narrow",
"key": "narrow-citer-accepted",
"ordinal": 14,
"references": [ { "role": "narrow_ref", "targetKey": "dup-key" } ]
},
{
"//": "R-AJ: a tied ordinal across two DIFFERENT schemas (leaf_a and leaf_b), not two rows of the same leaf like the astral/BMP pair above - the shape the row order's schemaId tiebreak needs and the original ten rows never had. Keys are anti-correlated with the real schema ids on purpose: leaf_b's real id sorts before leaf_a's (sha256:b1f2... < sha256:ff35...), so leaf_b gets the alphabetically-*later* key (\"zz-tie-schema\") and leaf_a gets the *earlier* one (\"aa-tie-schema\") - a schemaId-first order and a recordKey-first order disagree here on purpose, so only the correct composition produces leaf_b's row first.",
"schema": "leaf_b", "key": "zz-tie-schema", "ordinal": 15, "fields": { "kind": "tie-schema", "mass": "1" }
},
{
"schema": "leaf_a", "key": "aa-tie-schema", "ordinal": 15, "fields": { "kind": "tie-schema", "mass": "1" }
},
{
"//": "R-AO, 2026-09-23: 'dup-key' already carries two rows (leaf_a ordinal 11, leaf_b ordinal 12) - both are in citer's Design role's declared target ([leaf_a, leaf_b], unlike citer_narrow's narrow_ref above, which only ever had one in-target candidate at a shared key). This citer is the first row in this file (hence in both runtimes' row order) to name 'dup-key' through Design, so resolving it must deterministically pick the leaf_a row - the first in-target candidate in the caller's order - never the leaf_b row, in both runtimes.",
"schema": "citer",
"key": "citer-both-in-target",
"ordinal": 16,
"references": [ { "role": "Design", "targetKey": "dup-key" } ]
}
]
}

View file

@ -0,0 +1,282 @@
{
"vectors": [
{
"name": "any_of_plus_ge_conjunction",
"selectionMessagePackBase64": "g6ZmaWVsZHOSg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpndlYXBvboOlaW5kZXikbWFzc6JvcKJnZaZudW1iZXKhNapwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq"
],
"matched": 1,
"hasNext": false,
"expectedEdges": []
},
{
"name": "descending_with_limit",
"selectionMessagePackBase64": "g6pwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nw6VsaW1pdAM=",
"asOf": 1,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema",
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/zz-tie-schema"
],
"matched": 18,
"hasNext": true,
"expectedEdges": []
},
{
"name": "gt_numeric_boundary",
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRtYXNzom9womd0pm51bWJlcqExqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfC",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/b-hi",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
],
"matched": 4,
"hasNext": false,
"expectedEdges": []
},
{
"name": "incoming_exists_true",
"selectionMessagePackBase64": "hKdzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmalY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key"
],
"matched": 2,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"payloadBase64": null
},
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"payloadBase64": null
}
]
},
{
"name": "incoming_negation",
"selectionMessagePackBase64": "hKdzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmalY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/😂-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema"
],
"matched": 7,
"hasNext": false,
"expectedEdges": []
},
{
"name": "shared_index_value_three_rows",
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpnNoYXJlZKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/shared-2",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3"
],
"matched": 3,
"hasNext": false,
"expectedEdges": []
},
{
"name": "hop_by_role_cites_exact_id",
"selectionMessagePackBase64": "hKRrZXlzkadjaXRlci0xpWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlk2UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqlyZWNvcmRLZXmkYS1lcaRyb2xlpkRlc2lnbqpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"payloadBase64": null
}
]
},
{
"name": "hop_by_role_cites_name_alias",
"selectionMessagePackBase64": "hKRrZXlzkadjaXRlci0xpWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlkqWxlYWZfYS52OalyZWNvcmRLZXmkYS1lcaRyb2xlpkRlc2lnbqpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"payloadBase64": null
}
]
},
{
"name": "schemas_by_hash_alias",
"selectionMessagePackBase64": "g6dzY2hlbWFzkdlKc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmYudjGqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "schemas_by_name_alias",
"selectionMessagePackBase64": "g6dzY2hlbWFzkalsZWFmX2EudjmqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/😂-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema"
],
"matched": 9,
"hasNext": false,
"expectedEdges": []
},
{
"name": "float_tie_exact_ge",
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRtYXNzom9womdlpm51bWJlcqoyMjMzNzU5LjI1qnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfC",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
],
"matched": 1,
"hasNext": false,
"expectedEdges": []
},
{
"name": "astral_key_code_point_order",
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedIds": [
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/😂-row"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "row_tiebreak_schema_id_before_record_key_at_tied_ordinal",
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORqnRpZS1zY2hlbWGqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedIds": [
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/zz-tie-schema",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/aa-tie-schema"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "shared_key_narrow_ref_resolves_leaf_within_target",
"selectionMessagePackBase64": "hKRrZXlzkbVuYXJyb3ctY2l0ZXItYWNjZXB0ZWSlY2l0ZXOCpnRhcmdldIKoc2NoZW1hSWTZR3NoYTI1NjpmZjM1ZjNkOWQ2ZGUzZjEwNjZiOTM2OWU3M2ZlNmM3NjQ4ZTg4MzY4ZDE4ZWZjMjI3NWM5OTQ3MzBjZDg0Y2ZmqXJlY29yZEtleadkdXAta2V5pHJvbGWqbmFycm93X3JlZqpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedIds": [
"sha256:ee192f6526a8f8cdec7044551aa2c8e28c612426766bb270605023bc94e2614d/narrow-citer-accepted"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:ee192f6526a8f8cdec7044551aa2c8e28c612426766bb270605023bc94e2614d/narrow-citer-accepted",
"role": "narrow_ref",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"payloadBase64": null
}
]
},
{
"name": "shared_key_design_resolves_the_first_in_target_candidate_in_row_order",
"selectionMessagePackBase64": "hKRrZXlzkbRjaXRlci1ib3RoLWluLXRhcmdldKVjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5p2R1cC1rZXmkcm9sZaZEZXNpZ26qcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedIds": [
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-both-in-target",
"role": "Design",
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/dup-key",
"payloadBase64": null
}
]
},
{
"name": "refuses_empty_schemas_list",
"selectionMessagePackBase64": "g6dzY2hlbWFzkKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedRefusalField": "schemas",
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "refuses_blank_key_entry",
"selectionMessagePackBase64": "g6RrZXlzkqRhLWVxoyAgIKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedRefusalField": "keys",
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "refuses_unmatched_cites_target_schema",
"selectionMessagePackBase64": "g6VjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZLlzaGEyNTY6bm90LWEta25vd24tc2NoZW1hqXJlY29yZEtleaRhLWVxpHJvbGWmRGVzaWduqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfC",
"asOf": 1,
"expectedRefusalField": "cites.target.schemaId",
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "refuses_cites_target_by_hash_shaped_alias",
"selectionMessagePackBase64": "g6VjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlKc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmYudjGpcmVjb3JkS2V5pGEtZXGkcm9sZaZEZXNpZ26qcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
"asOf": 1,
"expectedRefusalField": "cites.target.schemaId",
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
},
{
"name": "shared_key_narrow_ref_refuses_when_only_candidate_is_outside_target",
"selectionMessagePackBase64": "hKRrZXlzkbRuYXJyb3ctY2l0ZXItcmVmdXNlZKVjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5q25hcnJvdy1vbmx5pHJvbGWqbmFycm93X3JlZqpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
"asOf": 1,
"expectedEvaluationRefusal": "reference_outside_target",
"expectedIds": [],
"matched": 0,
"hasNext": false,
"expectedEdges": []
}
]
}

441
docs/c2a-land-map.md Normal file
View file

@ -0,0 +1,441 @@
# 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`.
## Cut sequence to main (2026-10-27, branch head 7c7cc799)
Landed since the facts below: `land-c2a` (h1), `land-c2a-merge-api-docs` (h1-h6), the 2.0.1 and
cultcache-rs 0.3.1 merges (5baa02fa, f54472e5), `land-c2a-gate` r4 (h1), `store-gate-one-owner`
(h1-h7, Soul s6) and `restamp-on-write` (h1-h2, 8ee66714..7c7cc799, Soul s2 holds). One static `Owns`
decides ownership for cache and cacheless writers, and the High finding
`cut-land-c2a-gate.s1.reflist-record-read-only-at-head` is Fixed by 7c7cc799. origin/main is 73e684b1:
21 commits past the merge base 02cf04b5, all docs and research (15 files, +1879, none under src/,
tests/ or packages/, none on a path the branch changed). What stands between the branch and main, in
order:
1. **claimant-one-owner** (spec r1 at 7c7cc799, Hands then Soul). Restamp's Soul s2 left four Lows
on the landing path. Which entry reads a persisted entry is chosen twice (`Claimant` at
CultCache.cs:703-750, `ClaimantEntry` at :797-799), and so a cacheless writer handed registry {A,B}
overwrites a record that a cache with {A,B} holds read-only (probe X4). The cut keeps one `Claimant`
precedence over a by-id/by-name lookup that the registry or a single entry supplies. A cacheless
writer with a registry then takes its claimant from that registry. The cut also pins the
unpublished-id case and fixes the stale test comment. It closes `claimant-selection-duplicated`,
`cacheless-inverts-registry-claimant`, `unpublished-id-guard-unpinned` and `stale-flag-copy-comment`.
It is kept apart from the gate so that Soul can falsify it alone. Its files (CultCache.cs, the
persistence contract, RestampOnWriteTests.cs) are disjoint from the gate's, so it can run before or
beside step 2. The second cut to land rebases.
2. **land-c2a-gate** (spec r7 at 7c7cc799, Hands h2 then Soul). The ref-list gate does the same write
at 2.0.1 and at head and compares schema ids. The committed vectors are pinned byte for byte under a
fixed clock, and every stage counts its cases. r7 drops r6's head-only restamp assertions, which
`RestampOnWriteTests.TheCommitted1060VectorsListRecordIsWrittenUnderTheHoldersOwnId` already pins.
It also drops the red-at-193cf1c3 run, because restamp is measured. The cut closes
`single-vector-hand-edit-passes`, `gate-green-with-no-refusal-probes` and follow-up
`gate-reads-1x-written-store`.
3. **Ledger** (Self, no Hands; can run now, beside steps 1 and 2). Each of these is in force though
its fix has landed:
- The seven `cut-land-c2a.s1.*` findings. `land-c2a-merge-api-docs` carried their pins (source reads
2026-10-27: GameCult.Caching grants InternalsVisibleTo only to `.Tests` and `.MessagePack`, the
residual being follow-up `caching-messagepack-ivt`; PreCut2StoreFormatTests pins "with a suffix
of"). Close each one against its commit, or defer it. `stale-unity-plugin-dlls` defers to the
Unity 3.0.0 release cut (the plugins are 2.0.1 builds, 1eeb1a01).
- `cut-c2a-write-set.s1.untouched-dropped-slot-resurrects` and follow-up `slot-reuse-contract-doc`.
Ruling `slot-reuse-documented` decided the behaviour, and the paragraph is in
cultcache-schema-compatibility.md:64-66.
- `cut-restamp-on-write.s1.websockets-tests-unreported`, Fixed by h2 (s2 promise
`websockets-tests-reported` holds).
- Follow-up `gate-bound-test-tighten`, carried by restamp r2 (50362447, a768b821). Close it if
restamp's Soul passes measured it; otherwise route it.
- After steps 1 and 2 hold: the four s2 Lows, the two remaining `cut-land-c2a-gate.s1` Lows and
`gate-reads-1x-written-store`.
4. **Consumer pins** (ruling `unpinned-consumers-at-land-3`, follow-up `consumer-pins-before-land`).
Brokkr, Aquarium, Ymir, AquaSynth and Mimir have no `CultLibRevision`/`VerifyCultLibRevision` guard,
and each builds against the sibling `..\CultLib` checkout. In its own repo, each adopts Aetheria's
guard at the 2.0.0 tag, with one Hands per repo, before the merge reaches main. The pins touch no
CultLib file, so they can start now and run in parallel with steps 1-3. Eve and Gjallar get no pin
(ruled). The step is done when each repo's guard fails a build against a CultLib checkout at
another revision and passes at the tag, and the follow-up is resolved.
5. **Main into the branch, then full verification.** After steps 1, 2 and 4, `git merge origin/main`
into `hands/variants-c2a`. It should be clean, because main's commits since 02cf04b5 are docs and
research only. Re-check that before merging (`git diff --stat 02cf04b5 origin/main -- src tests
packages unity`). Then run every suite at the merged head on Yggdrasil: Caching, Mesh, Networking,
Geometry, WebSockets, ConsumerBuild, InteropPeer, cultcache-ts/-rs/-py, cultnet-ts and cultmesh-ts.
Run `old-reader-refusal.sh gate` for csharp, typescript, python and rust. If main gained code by then,
a Soul pass covers the merge.
6. **Land.** The land is `git merge --no-ff`, never a squash, so the SHAs that every report and verdict cites stay reachable from main (skip the branch's non-building commits with `git bisect --first-parent`). Steps 1 and 2 closed at 723b7996 (claimant-one-owner, land-c2a-gate); step 5 closed at the merge commit 3157d4b3 (Soul `cut-land-c2a-main.s1` holds). Step 3 closed except `cut-land-c2a.s1.stale-unity-plugin-dlls` and `cut-land-c2a-gate.s2.mutation-table-reads-gate-as-zero`, which wait on the 3.0.0 release cut and the Eureka tools respectively; step 4 closed with `consumer-pins-before-land` (pins since 2026-10-22). Run `git merge --no-ff hands/variants-c2a` onto main, with an empty diff against the
verified head. There is no tag: tags, the Unity plugin rebuild and `declared-range-gate-own-version`
belong to the 3.0.0 release cut. Open follow-ups `c2a-load-slower` (load 2 to 2.3 times slower than
main on a synthetic Aetheria catalog) and `drift-write-ownership-leaves` do not say "before land".
`c2a-load-slower` still needs a reason or a fix before the 3.0.0 release, under
`cultlib-gaps:ruling:database-shaped-built-for-load`.
## 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.PersistedSchemaId`
and `CacheBackingStore.MarkWritten` (73d438db, afd799ba, 3f6dda8b). Conditional commit
compares the persisted schema id plus storedAt. C2a already owns this as
`StoredSchemaId` + `StoredVersion` (SHA-256 of the stored bytes, which names the schema
id and storedAt) set by `CultCache.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
removes `SerializeSnapshot`/`DeserializeSnapshot` overrides and changes `ConditionsHold`.
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's `readable_store_format` and v3 `decode_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.0` tag into `hands/variants-c2a`.
Versions stay main's. CHANGELOGs get Unreleased entries, and the Unity entry says major.
- **Deletes and collapses.** Delete `PersistedSchemaId`, `PersistedIdentity`,
`CultStoredDocument.MarkWritten` and `CacheBackingStore.MarkWritten` (main, about 60 lines),
and the store-composition contract's MarkWritten paragraph. C2a's `StoredVersion`/`Wrote`
is 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 in `HeaderFor` and 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, sets `Loaded`, adds the store to `_stores`,
pulls, and only then sets `store.Cache` (CultCache.cs 2268-2312). `InStoreHold` lets 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 call `store.WriteDocument`: `Gate(cacheless)` guesses null,
re-enters the store's gate, finds `Cache == null` and runs the write, and its `finally`
calls `UseRegistry(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 read `HoldsIds` only on
staged entries (single-file 4318, 4447 and 4472; directory store 362), and a staged entry
is always the document the cache pushed (`Push` sets `Entries[key] = entry`, 4211-4215).
A single-file `PullAll` returns 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 (ruling `q6-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 sets `Loaded` and calls `UseRegistry` by hand.
- Probe (scratch clone `imagination-store-gate-r2/clone`, commits 4663934e and c099b9e8 on
ec94ab98; Yggdrasil t1.log, t2.log and t3.log; 2026-10-25 around 20:50 CEST). The
probe did three things. `InStoreHold` refuses a cacheless call that re-enters the same
store's gate. The `_codec` field and the two-argument `UseRegistry` are 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 `AddAsync` minted, and an element id that was not 12 hex characters.
No existing test failed, `UserCodeInACachelessCallMayCallItsOwnStore` included.
- 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 and `store.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.v2` header. With no codec loan, the pulled copy does not
mark the store.
- Mutant `return cacheless` changed to `return 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 `Cache` is 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 mirrors `Cache`, and `Loaded` and `_registry` are not read. Precedent:
`ConcurrentHashMap.computeIfAbsent` refuses a mapping function that updates the same
map with `IllegalStateException("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: `UserCodeInACachelessCallMayCallItsOwnStore` stays green.
- The late publish stays. It is what bounds `Gate` at 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 as
`variants:question:pulled-variant-codec`. The spec is written for the recommended option.
- Scar check: `git log -S` over CultCache.cs for `reentrant`, `outermost` and `_attaching`
finds nothing. `Cache?.Codec` appears in 1468a83a (h4) and 619e089f only. 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 `finally` took
the outer call's registry. It is now refused.
## Store gate: Cache is the one attachment fact (store-gate-one-owner r3)
Spec `variants:cut_spec:cut-store-gate-one-owner.r3` (base 0fa836cb), superseding r2. It
closes Soul s4's `attach-window-held-store-writes` (Medium), `cacheless-refusal-order-unpinned`
(Low) and `interop-consumer-suite-unrun` (Low). This is the third time the attach window has
beaten the rule (s3 from the cacheless side, s4 from the held side), so r3 moves the
decision instead of patching the next entry point.
**Facts: every entry point that reads or decides attachment, at 0fa836cb.** Source reads of
CultCache.cs, DirectoryMessagePackBackingStore.cs and CultMeshSingleFileDocuments.cs. The
window column is a journal on the attaching thread, through a real attach of a store holding
`a`. Probe: scratch clone `imagination-store-gate-r3/clone`, commit b2330b6d (test R3Probes),
Yggdrasil `probe-base.log`, 2026-10-25 around 23:00 CEST. "Disagree" means that after the
attach, a commit of `later` and a flush, the attaching cache holds `a,later` and a fresh cache
over the same file reads something else.
| Entry point (line) | Fact it reads | In the attach window at 0fa836cb |
| --- | --- | --- |
| Single-file `PullAll` (4156) | `Gate`: `Cache` guessed unlocked, then re-read under the re-entered store gate | Runs as detached; its `Loaded` re-enters the cache and is refused as a journal write. Agrees. |
| Single-file `Push` (4212) | same | Runs as detached and stages `b`. Disagree: fresh reads `a,b,later`. |
| Single-file `Delete` (4223) | same | Runs as detached and stages a removal the cache never made (dirty after the attach). |
| Single-file `PushAll` (4239), and `Dispose` (3722) through it | same, then in-body `Cache == null` (4531, 4543, 4588, 4603, 4611, 4661) | Runs as a cacheless writer. Push then PushAll writes `b` to the file inside the attach (disagree). Delete then PushAll is refused with the cacheless writer's text. On a variant store it is refused as "a writer with no CultCache" (Soul s4). |
| Single-file `CommitBatch` (4256) | same | Runs as detached and commits `b`. Disagree. |
| Single-file `JudgeChecks` (4568), `RefuseRemovalOfUndeclared` and `StageChecks` (4558, 3915) | same | Run as detached. Agree in the probe, but under the wrong gate's rules. |
| Single-file `ReadStoreSnapshot` (4687) | same | Runs as detached and reads the file. |
| `WriteRecord` (4342), `WriteDocument` (4366) | `InStoreHold`'s per-thread hold (cacheless re-entry), before `Cache` | Refused with the re-entry text (h5). Agree. |
| Directory `PullAll` (48) | `Gate`, as above | Refused as a journal write. Agree. |
| Directory `Push` (147), `Delete` (160), `CommitBatch` (172) | same | Run as detached. Disagree: fresh reads `a,b,later` after Push or CommitBatch, and `later` after Delete. |
| Directory `PushAll` (235) | same | Runs as detached. Push then PushAll and Delete then PushAll disagree. |
| Directory `JudgeChecks` (269), `RefuseRemovalOfUndeclared` (259) | same | Run as detached. Agree. |
| In-body reads: `StoredAtOver` clock (4028), refusal text and `DurableAt` (4531, 4543), declares and cacheless refusal (4588, 4603, 4611), `Wrote` (4661) | `Cache`, under the gate `Gate` chose | On the attaching thread they read null while the thread holds the cache's gate, so a held call runs as a cacheless writer. |
| `AddBackingStore` (2259-2314) | writes `Cache` under both gates, after the pull | The window's author: the store is attached for the pull (registry lent, `Loaded` set, in `_stores`) while `Cache` is null. |
| Mesh `CultMeshSingleFileDocuments` writes and reads (99, 143, 174, 226) | none of the attached store's | `StoreAt` (239) builds a new store per call, so they never touch an attached store object. A file it shares with an attached store is the file lock's and the write set's business. |
| `IsDirty`, `HasStagedChecks`, `ForeignRecords`, `LastSchemaMigrationReports`, `HeaderFor`, `HeaderForWrite` | none | Ungated values or static decisions. They read no attachment fact. |
So during an attach, cacheless entry points decide attachment from the hold, and every held
entry point, plus every in-body read, decides it from `Cache`, which is still null. These are
two facts.
**Probe of the chosen shape** (commit 4695390c on b2330b6d; `proto-full.log`). `AddBackingStore`
publishes `Cache` before the pull and takes it back in the same hold if the pull fails. `Gate`
becomes straight-line code: guess, then the store's gate, where `Cache` is settled, then the
settled cache's gate. Results:
- All 21 probe cases are refused. The cacheless writes get the re-entry text and every other
case gets "A journal entered the cache it journals". After the flush, the attaching cache
and a fresh cache both read `a,later`.
- Caching: the only failures are the two tests that pin the late publish:
`AnAttachLendsTheRegistryAndPublishesTheCacheAfterThePull` (asserts no cache during the
pull) and `ACallWhoseGateChangesTwiceUnderItThrowsInsteadOfLooping` (asserts the old message
text; the tampering reader still throws). The churn, queued-reader and failed-attach tests
pass unchanged.
- Mesh and Networking are green.
- The cultcache-ts suite, which builds and drives the C# InteropPeer, passes on this shape:
175 tests, 171 pass, 0 fail, 4 skipped. The C#, Rust and Python interop tests ran, the C#
edit-in-place one included (`proto-ts2.log`, commit f5f02053). Recipe `clone/r3ts.sh`:
image node:24-bookworm, apt python3-msgpack, dotnet-install for 10.0, rustup minimal.
**Shapes weighed (rationale).**
- (a) Publish `Cache` before the pull, as a616431e did. Chosen. What s2 found was not that an
early publish is unbounded. It was that `Gate` re-read `Cache` *unlocked* after a stale
guess, so a churn of failing attaches could hand it a new stale guess every pass. The fix is
where `Gate` reads on its second step. Under the store's gate, `Cache` is settled: an attach
publishes and takes it back inside one hold of that gate, and no other thread can hold that
gate during the attach. A settled cache is permanent, because there is no detach. So `Gate`
needs at most three acquisitions, with no loop and no counter: the guess's gate, the store's
gate, and then the settled cache's gate, which cannot miss. This deletes a fact rather than
reconciling two. During the attach the store *is* attached, so every entry point, cacheless
or held, and every in-body read sees one answer. A journal's call is then refused by the rule
journals already have (AddJournal: "any hold on this cache while a journal runs throws"),
exactly as it is after the attach. Prior art: `ConcurrentHashMap.computeIfAbsent` installs a
ReservationNode in the bin *before* running the mapping function, so a recursive update on
the same thread finds the owner and throws, and on failure the reservation is removed (see
`docs/research/2026-10-23-store-lock-user-code-prior-art.md` item 4 on main: the re-check
after locking, and the ReservationNode on the empty-bin path). The validate-after-acquire in
`Gate` is the same item.
- (b) Keep the late publish and make the attach's hold the one fact for every entry point.
Rejected. `InStoreHold` knows that this thread holds the store's gate, not *whose* hold it is.
Refusing every same-store re-entry kills `UserCodeInACachelessCallMayCallItsOwnStore` (r2's
R5), so (b) needs an "attaching" marker beside `Cache`. That is the second fact that s3 and
s4 rule out, plus every in-body `Cache == null` read would need to consult it as well.
- (c) Run no user code on the attaching thread under the store's hold during a pull: defer the
pull's journals to after the hold (open calls). Rejected as insufficient on its own. Getters
and constructors still run during the pull's decode, so the window stays open to any user
code that is not a journal. It also moves journal timing, which is a documented contract
("right after an admission is applied").
- Scar check: `git log -S` over CultCache.cs finds `store.Cache = null` and `MaxStaleGuesses`
only in ba658965 and 1468a83a. The early publish was not removed for a correctness reason of
its own: 1468a83a removed it to bound `Gate` and to lend the codec, and the codec loan is gone
(`pulled-variant-codec`). No settled read under the store's gate was built before. The
precedent recipe (no root filter) finds no same decision.
- Not a product fork. A journal's store call during an attach now throws, as it already does
after the attach. Cacheless writes on the attaching thread stay refused, with the same text.
Getters and constructors during the pull see an attached store, as they do during any held
call. It changes the late-publish clause of Defaulted ruling `variants:ruling:pulled-variant-codec`,
so Self defaults a superseding ruling (recommended label `cache-published-for-the-pull`)
before dispatch.
- Residual, not in r3. `Push`, `Delete`, `PushAll` and `CommitBatch` are public, so user code
that is not a journal (another thread, or a getter during any held call) can stage records
behind an attached store's cache. Soul s4 shows this with `other-thread-push`: file `a,b`,
cache `a`. That is true before, during and after an attach, so it is not the window's
two-fact problem. Closing it changes public API (a store's writes taken only from its
cache), so it is a follow-up and an operator question for Self, not part of this cut.
## Body facts: claimant-one-owner r2 (Imagination, 2026-10-27, base fc74cf6f)
Why a revision: Soul beat the same rule three times (s1 claimant choice, s2 compatible ids, s3 shape). Each time a
cacheless write lent a registry let the registry's entry for the written schema id decide ownership beside the entry
the caller passed. Two descriptions of one type reached `Owns`; each fix (h2 schema-id compare, h3 subset of compatible
ids) compared one more facet of the two and left the rest open. r2 removes the second description from the lookup.
### Where the two descriptions met (source read, fc74cf6f)
- `OwnsOnDisk` cacheless arm (`CultCache.cs:4725-4740`): with a lent registry it called
`lent.Owns(id, catalog, declared, written)`, whose lookup is the registry's `_indexes` and whose `entryOf` is
`descriptor.ToCatalogEntry()` (`:824-825`). The claimant for the written schema id was the registry's descriptor,
so `ReadsWhole(persisted, claimantEntry)` (`:848`) judged shape against the registry's entry, and the claimant
precedence (`:756-806`) walked the registry's compatible ids. `written` (`:4734-4736`) then compared the two entries
by schema id plus a subset of compatible ids.
- `WriteDocument` (`:4495-4524`) takes its entry from the registry (`resolvedRegistry.GetRequired<TDocument>().ToCatalogEntry()`),
so its two descriptions are always identical. Only `WriteRecord(record, catalogEntry, holdsIds, registry)` (`:4471`) can
pass an entry that disagrees with the registry's entry for the same id.
- `CopiedHoldIds` (`:4600-4630`) also reads `AttachedRegistry`, to decide the v3 header from copied records. It is a
positive-only header decision, not ownership, and out of this cut.
### Consumer audit (2026-10-27)
- `WriteRecord` and `WriteDocument` are new in the Unreleased 3.0.0 entry; `main` has neither (`git show main:src/GameCult.Caching/CultCache.cs`).
- Per-repo `rg` over every git repo directly under `F:\Projects` for `.WriteRecord(`, `.WriteDocument(`, `WriteSingleFileDocument`
(Library, obj, bin, PackageCache excluded): hits only in CultLib and in vendored CultLib copies under Heimdall and StreamPixels
(`vendor\CultLib`, older pins without these methods). No consumer outside CultLib calls either method.
- Inside CultLib at fc74cf6f: Mesh `WriteSingleFileDocument` calls `WriteDocument(key, document, storedAt, registry)`;
Mesh's raw-payload path calls `WriteRecord(..., registry: null)` (`CultMeshSingleFileDocuments.cs:129, 226`). The only
`WriteRecord` calls with a registry are tests: `CachelessSingleFileTests.cs:453, 660` (a registry `{IdDeck}` that does
not hold the written entry `One` at all, used for the copied-record header), `RestampOnWriteTests.cs:595, 599`, and
Soul's probes. Packages: no TS, Python or Rust counterpart takes a registry (cultnet-rs `write_record` is an example helper).
- Consequence: the registry's documented role in `WriteRecord` is to describe the other records in the store. A shape
that derives the written entry from the registry (or refuses an entry the registry does not hold) breaks
`CachelessSingleFileTests.cs:660` and removes that role.
### Candidate shapes weighed
| Shape | One description? | Public API | Kills s2/s3 | Cost |
|---|---|---|---|---|
| A. Lent registry's entry decides; refuse a passed entry that disagrees | yes, after a door check | signature kept; new refusal | yes | an equality check over a redundant parameter; breaks the registry-describes-others use (`:660`) when the registry lacks the written type unless it falls back to the passed entry, which is two descriptions again |
| B. Passed entry is the only description of its schema id; the registry supplies other types only | yes, by construction | signature kept; doc only | yes (probed) | one lookup class gains the registry; one registry overload deleted |
| C. Overloads: `WriteRecord(record, catalogEntry, holdsIds)` and `WriteRecord(record, registry, holdsIds)` | yes, by type | changes a 3.0.0 signature | yes | loses the registry-describes-others use |
| D. Delete `WriteRecord`'s registry parameter | yes | changes a 3.0.0 signature | yes | loses the copied-record header decision for raw writes |
Picked B. It is the only shape that keeps every documented use of the registry, needs no public signature change,
and deletes the comparison instead of extending it. It is ordinary scoping (an inner binding shadows the outer one
for its name), applied to schema ids.
### Probes (Starfire, local filtered runs, 2026-10-27)
Scratch clone of fc74cf6f at `<scratchpad>/imagination-claimant-r2/clone`, Soul's `SoulClaimantProbe.cs`,
`SoulClaimant2Probe.cs`, `SoulClaimant3Probe.cs` copied into `tests/GameCult.Caching.Tests`.
Base fc74cf6f (`probe-base.log`):
- P7 (registry {N lists own, A, B}; passed entry N with the list varied; record k under O, named like N): lent R / no
registry C for rows b, b2, e, g, h, k, l, l2; lent C for a, c, d, f, i, j. Confirms `subset-refuses-own-drift`.
- P9 (registry {NW: Name, Value, Colour}; passed NW minus Colour): lent C (restamped, Colour lost), no registry R.
Confirms `lent-shape-widens-passed-entry`.
- P6 (bare X1 entry, registry {X1} listing OX, record under OX): lent R, no registry R, cache C. h3 closed the id axis.
- P4: 12 states x 8 registries, violations=0. P1d: a cache {N,L} commits N over L's listed old id (`cache-replaces-other-types-record`).
Prototype B (scratch commit 349ea3f3 on the clone, not for landing; `probe-protoB2.log`): `CatalogEntryLookup` takes
the written entries and an optional registry and drops every registry descriptor whose schema id a written entry has;
the registry's `Owns(schemaId, catalog, declared, mayClaim)` overload is deleted; the cacheless arm always asks
`CultDocumentRegistry.Owns(id, catalog, declared, new CatalogEntryLookup(writes, AttachedRegistry), entry => entry,
claimant => writes has claimant's schema id)`.
- P7: lent == no registry on all 14 rows (all C).
- P9: lent R, no registry R, wide entry C.
- P6: unchanged (R, R, C). P4: identical table to base, violations=0. P1a-f, P2a-d: identical to base.
- `RestampOnWriteTests`, `CachelessSingleFileTests` and the probes: 139/139 passed locally.
- Yggdrasil, 349ea3f3, `ygg-protoB-full.log`: GameCult.Caching.Tests 1180 passed / 0 failed / 2 skipped;
GameCult.Mesh.Tests 342 / 0 / 4.
Negative greps, each tested at fc74cf6f (each matches there, and none may match after the cut, src and tests):
`claimant.CompatibleSchemaIds.All(` (1, the subset), `lent.Owns(` (1), `Func<CultSchemaCatalogEntry, bool> mayClaim) =>`
(1, the deleted overload), `CatalogEntryLookup(writes)` (3, one-argument construction). CHANGELOG and docs:
`never widens what the passed entry declares` (1), `with no compatible id the passed entry does not list` (1).
### Not settled by r2
- `cache-replaces-other-types-record` (s2, PreExisting, cache arm): r2 touches only the cacheless lookup, so it does
not settle whether a cache may replace another registered type's record at a shared key.
- Whether a registry-less `WriteDocument` should consult `CultDocumentRegistry.Shared` for claimants at all (s1
`shared-decides-registryless-write`, closed Fixed by documentation in h2). r2 keeps today's answer.
- `variants:finding:cut-restamp-on-write.s2.cacheless-inverts-registry-claimant` is still in force although r1 was
mapped to close it; Self should close or re-triage it after r2's Soul pass.

View file

@ -0,0 +1,840 @@
# CultCache publication: the cache owns "published once", and order stays data
Status, 2026-09-29: Cuts 1-3 landed on `main`. Cut 3 merged at `ae8576d`. Imagination pass 4 re-took Cuts 4-6
against `3bf1c0c`. Q-P5 is ruled (A). Cut 4 is ready for Hands.
- F4, pinning `Held`'s publish-on-throw guard, is done: Cut 3 pinned it.
- F3 (observer removal on unsubscribe) and F1 (the false "getters run once" claim for conditional commits on variant
stores) were Cut 3 follow-ups; confirm against the Cut 3 merge.
Recorded:
- F2, the Q-P2 hazard. A rethrown observer exception makes a committed write look cancelled or refused. The
only live typed catch around a cache write is AetheriaEve `Aetheria.State.Daemon/Program.cs:3722`. It needs a
fail-fast handler, and no production code installs one.
- F5: subscribe and unsubscribe cost O(n).
- F6: writes after `Dispose` are dropped silently.
Map written by Imagination pass 1, 2026-09-29. The ends are the
operator ruling of 2026-09-30: every committed cache change reaches subscribers
exactly once. This document owns the means.
- **Body.** CultLib `main` `65311c9ad770828adafe40c2c14b2d3da517628e`. Every
`file:line` is against it unless it names the branch.
- **Superseded attempt.** `origin/hands/cultnet-publish-commits`, tip
`28248eb`, base `f9f107a`. Soul pass 3 said do not merge it, and that stands.
- Main has not touched `CultCache.cs`, `CultNetDatabase.cs`, the composition
doc or the networking tests since `f9f107a`: `git diff --stat` is empty.
- `git merge-tree 65311c9 28248eb` is clean.
- **Scope.** The C# cache's publication and CultNetDatabase's use of it. There
is no wire-format change and no other runtime changes (§6).
## Rulings (operator, 2026-09-30)
- **Q-P1 A: order as data.** Each change is published exactly once, on its writer's thread, before the write
returns, and carries its `Sequence`. The shard log is written in cache order by the gate-held journal. Wire
peers drop changes that are stale by `Sequence`. The turnstile (`bcae483`/`4562340`) and the drainer are
both rejected.
- **Q-P2 A:** a throwing observer does not stop the others. Every observer runs, the exception is rethrown to
the writer afterwards, and the observer stays subscribed. The commit stands.
- **Q-P3 A:** a public `CultCache.AddJournal(...)`, with reentry guarded.
- **Q-P4 A:** Cut 6, which drops stale changes per key on the wire, lands in this campaign.
- **Q-P5 A (2026-09-29):** a primary that cannot log a committed change throws a typed `CultNetShardLogException`
naming the shard and the burned sequence, after publication, not derived from `InvalidOperationException`.
## The finding that shapes this map
The direction Imagination was handed was: "ordered delivery belongs in the
cache's own publication, on the writer's thread, so a writer delivers its own
changes before returning". That design **has been built before, and it was
deleted**.
- `bcae483` (2026-09-13 20:09) made the cache a delivery scheduler: tickets, a
delivery gate, turn waits and per-thread queues for reentrant writes.
- `4562340`, 34 minutes later, deleted it: "It deadlocked inside CultLib: a
load's delivery reaching a CultNetDatabaseSubscriptionServer observer that
needs _lifecycleGate, while a subscribe's demand handler holding that gate
writes the cache and waits for the delivery's turn."
- It replaced it with **order as data**: a per-cache `Sequence`, and no
delivery scheduler.
- The regression tests it left, `ObserverWritingTheSameCacheDoesNotWaitOnOtherDeliveries`
(`ConditionalCommitTests.cs:763`) and
`SubscriptionDemandHandlerWritingCacheDuringDeliveryDoesNotDeadlock`
(`NetworkingTests.cs:2244`), are live on main.
This pass rebuilt that scheduler as a prototype. It is the per-cache turnstile
with same-thread trampolining at `79f4639`, and it reproduces the deletion:
- Caching tests: 278 of 279 pass, and `ObserverWritingTheSameCacheDoesNotWaitOnOtherDeliveries`
fails.
- The networking suite hangs, and Yggdrasil's idle watchdog killed it at 900 s.
The blame-hang run that names the hung test is recorded below.
The reason is structural, not an implementation slip. Once a writer waits for
another thread's delivery, any lock that an observer takes and a writer holds
becomes a deadlock:
- total cross-thread order;
- a writer delivering before it returns;
- no writer waiting on another thread.
Pick two. The branch's drainer chose order plus no-wait, and lost
before-return (F3 and F4). The turnstile chose order plus before-return, and
lost no-wait (the `bcae483` deadlock).
**So this map keeps order as data (the `4562340` ruling) and moves the one
order-sensitive consumer, the CultNet mutation log, to the place order is
decided: under the cache gate.**
- Every writer keeps delivering its own changes, on its own thread, before it
returns, exactly as main does. F3 is satisfied by not regressing it.
- The log becomes a **journal** written in the admitting hold, in Sequence
order.
- The reorder buffer and drainer die, and no scheduler replaces them. This
departs from Soul's stated direction on purpose, and **Q-P1** puts it to the
operator.
## Probe evidence
All of this ran on Yggdrasil via `ygg-verify.sh`, from scratch commits in a
throwaway worktree that no branch holds.
- Probe file: `tests/GameCult.Caching.Tests/PublicationProbeTests.cs` in scratch commit `79f4639`. The worktree has been removed. The commit is unreferenced in the local object store, where gc can collect it; it also survives on the Yggdrasil mirror as `refs/verify/79f4639f8f5b95db7ce16f244a081c1ec101a468`.
- `9c5abf5` is the probes on main. `79f4639` is the same probes plus the
turnstile prototype in `CultCache.cs`.
| # | Probe | main `65311c9` | Turnstile prototype `79f4639` |
|---|---|---|---|
| Q1 | A batch `{ok, boom}` where boom's `[CultName]` getter throws | `TargetInvocationException`. `_sequence` goes from 0 to 1, because ok was minted. **Both ok and boom are in memory**, and `GetByName("ok")` finds ok. **Nothing is published**, and the next write gets Sequence 2. F1 is worse than stated: memory is corrupt and unpublished. In a store-backed cache, `land` (`:2325`) already ran, so the store holds it too. | No stall, but memory is still corrupt. |
| Q2 | Two `Watch` observers, the first throws | With the default R3 handler, the second observer sees 2 of 2 and the writer sees nothing. **With a rethrowing handler, the second observer sees 0 of 2 and every writer gets the exception.** The comment at `CultCache.cs:2374`, "OnNext does not throw", is false under fail-fast. | With its own observer array and a per-observer catch, the second observer sees 2 of 2 and the writer gets the exception afterwards. |
| Q3 | Writer A's observer blocks on `a`, then B writes `b` | The recorder sees `b(2)` before `a(1)`. There is no cross-thread order, by design since `4562340`. | Sees `a` then `b`, because B waited for A. |
| Q4 | An observer on `x` writes `y` | `y(2)` before `x(1)`: nested, depth-first, and the nested write returns after `y` is published. | `x` then `y`, and the nested write returns before `y` is published. |
| Q5 | `OnUpdate` on Upsert and on Commit | 0 fires, so loads only. `Watch` sees 2. | Unchanged. |
| Q6 | 8 threads × 1000 writes, plus reentrant writes | 67 ms, 2645 of 8800 cross-thread inversions, and 0 writes returned undelivered. | 516 ms, 0 inversions, 0 undelivered. |
| Q7 | Soul's P2 shape: a handler waits 1 s for another thread's write | Observed in time. | Not observed in time. |
| Suites | Caching, Networking and Mesh | (green on main) | Caching fails `ObserverWritingTheSameCacheDoesNotWaitOnOtherDeliveries`, and Networking **hangs** until the 900 s idle kill. Rerun with `--blame-hang-timeout 90s`: 286 passed, then the test host hung and was dumped. The blame sequence file was not extracted, so **the hung test is not named**. It is consistent with, but not proven to be, `SubscriptionDemandHandlerWritingCacheDuringDeliveryDoesNotDeadlock`, whose own comment says "disposal would hang too". |
## Imagination pass 4: Cut 4 re-taken on the landed journal
Cut 3 landed at `ae8576d`. Its Soul left two questions that Cut 4 had to settle before relying on the
journal: (a) what a throwing journal means for the log, and (b) whether a journal needs the change's origin.
Both have one coherent answer. The one fork, Q-P5, was about who hears of a log failure; the operator ruled it A (2026-09-29).
### Probe evidence (pass 4)
- Probe file: `Pub4ProbeTests.cs` (Networking tests). It was run on Yggdrasil from scratch commits:
- `0d7397d`: main plus the probes;
- `6ec2f73`: the same, merged with the branch `28248eb`;
- `376cd74`: main plus probe C.
- The commits survive on the Yggdrasil mirror. The scratch worktree is gone.
- The probes report facts and assert nothing. `FlakyLogStore` is an in-memory `ICultNetShardMutationLogStore`
that can refuse `Append` or run a hook inside `GetCompactedThrough`.
| # | Probe | main `3bf1c0c` | branch `28248eb` (queue) |
|---|---|---|---|
| A1 | Primary with a durable log. Put 1, Put 2 (its `Append` throws), Put 3. A replica pulls, then retries. | Writer gets `IOException`. The cache holds 1, 2 and 3, but **2 is never published** (2 of 3). Memory log `[1,2,3]`, durable `[1,3]`. The replica pull throws "log has a gap. Expected 2, received 3", and so does **every retry**: the replica is stuck for good. | **Writer sees nothing**: R3 swallowed the drainer's exception. 2 is not published. The replica is stuck in the same way. |
| A2 | The same failure at the tail, then the primary restarts over the same cache and log store. | The replica applied 1. After restart, note 3 is **minted sequence 2**. The replica takes it, reports applied 2, and holds `1,3` against the primary's `1,2,3`: **silent divergence**, a reused sequence. | Same. |
| A3 | A replica whose log `Append` throws once. | The pull throws and the cursor stays at 0. The retry converges (`1,2`), but the memory log is `[1,1,2]`: a duplicate, because memory is written before disk and kept as a `List`. | The pull "succeeds": the exception was swallowed and the cursor went to 2. The durable log is `[2]`, so **entry 1 is lost**, and a chained replica sees a gap. |
| B1 | Primary with a file store. Another writer puts `pulled` into the file, the primary runs `PullAll`, and a replica pulls the log. | `pulled` is published but **not logged**. The replica gets nothing. | Logged. The replica converges on `pulled`. |
| B2 | A store attached after the database exists. | Nothing is logged. | All three hydrated records are logged. |
| S | `CreateShardSnapshotResponse` with a write landing between its document read and its sequence read (hooked in `GetCompactedThrough`). | The snapshot holds `[1]` at log sequence 2. The replica applies it and pulls, and still lacks 2: **divergence**. | Same. |
| C | Compact through 2, restart, then Put 3. | Note 3 is **minted sequence 1**, and `GetLatestMutationLogSequence` returns 1, below the floor of 2. | (not run; the code is unchanged) |
What the probes establish:
- A1, A2, S and C are pre-existing defects on main. Cut 4 inherits them and must not build on them: the
resync path it relies on for (a) is itself broken by S and C.
- On a replica, the pull loop already retries. The cursor advances only after an entry applies
(`CultNetDatabase.cs:1038-1083`), so a replica needs no new mechanism. It needs only to keep the exception
reaching that loop, which the branch's queue broke (A3).
- Loads on a primary must be logged, or replicas diverge (B1).
### (a) A journal that cannot log a committed change: one answer
The facts:
- The log store is a second durable store written after the cache's store. Without an atomic commit across
the two, the log can lag the store. It must never lead: a replica would then apply a change the primary
refused.
- So lag is unavoidable, and it has to be detectable and repaired.
- The protocol already has a repair, and nothing else needs to be invented.
- "History the primary cannot serve" is `compacted_history`: `CultNetDatabaseServer.cs:189-201` answers
`ResyncRequired`.
- `CultNetShardReplication.cs:199-214` then snapshot-resyncs.
**Rule: a primary that cannot log a committed change burns that change's sequence and compacts through it.**
- In the database journal, on a primary, for each change that is not `NoLog`:
1. Mint `S`.
2. Build the entry: the wire message from the door, or `ToLogEntryMessage`.
3. `Append` durably, **then** record it in memory. Disk goes first, so memory never serves what disk lacks.
- If step 2 or 3 throws:
- record nothing;
- raise the database's in-memory floor for the shard to `S`;
- try `store.CompactThrough(shard, S)`;
- still set `_lastWriteSequence` to `S`, because the row was written at `S`;
- carry on with the rest of the admission;
- throw `CultNetShardLogException` once at the end of the journal call (Q-P5 A).
- Every replica behind `S` is then told `compacted_history` and resyncs by snapshot. The sequence gap at `S`
is never served, because the floor covers it.
- This **replaces** the old §4 step 3 claim, "a serialization failure then mints nothing, so the log can never
hold a gap". Minting nothing hides a replication gap behind a dense sequence. That is exactly A2's silent
divergence.
- **Replica:** there is no floor. The journal's exception reaches `ApplyShardLogResponseAsync` and the cursor
does not advance. The next pull re-applies the entry: a fresh instance, so the door matches again, and the
entry is re-recorded under the primary's sequence. The branch's absent-key direct log follows the same
disk-first order.
- **Required fixes** that the rule depends on, all in Cut 4:
- `GetCompactedMutationLogSequence` (`CultNetDatabase.cs:597-602`) returns
`max(store floor, in-memory floor)`.
- `InitializeLogSequencesFromStore` (`:1363-1395`) resumes at
`max(highest retained, store.GetCompactedThrough(shard)) + 1`. This is probe C.
- `GetLatestMutationLogSequence` (`:607-616`) never reports below the floor.
- `CreateShardSnapshotResponse` (`:624-670`) reads `GetLatestMutationLogSequence` **before** `SelectAll`, on
both returns, `:645` and `:668`. The documents then reflect at least that sequence, and replaying later
entries converges: they are whole-document puts and deletes, applied in order. This is probe S.
- `RecordMutationLogEntry` (`:1331-1352`) writes disk first. The in-memory log becomes
`SortedList<long, entry>` per shard, replacing by sequence as the durable store does. This fixes A3's
duplicate.
- **Residual, recorded rather than forked.** Suppose the store refuses both `Append` and `CompactThrough`, and
then the process restarts.
- The durable log does not know about the hole, so A2's tail reuse can recur.
- Fixing that needs a durable write per mint, a sequence reservation, which doubles log I/O under the gate.
- The thrown exception names both failures, so the outage is loud.
- Refusing later writes ("fail closed") is not coherent. The database can refuse only its own doors, while
bare cache writes are journaled regardless: that would be split authority.
### (b) A pulled or loaded change on a primary: no origin needed
- On a primary the log is the shard's **state history**, whoever wrote the state. B1 shows that leaving loads
out of the log leaves replicas permanently diverged.
- The only origins that change what is logged are already carried by the door table, keyed by instance
identity and matched by shape. They are:
- `Replica` (log the primary's entry);
- `NoLog` (prediction, snapshot resync);
- `Put`/`Delete` (the wire message);
- `Predicts`/`Authoritative`.
- A load or pull has no door, so on a primary it is logged like any unattributed commit. On a replica it logs
nothing, like any local write. That is what the branch does, and B1 converges under it.
- Cost:
- B2: a store attached **after** the database logs its hydration.
- `CultNetHost` opens the cache before building the database (`CultNetLocal.cs:146-152`), so production
hydration is not journaled.
- A re-pull admits only the records that changed. In B1 the seed was not re-logged.
- `AddJournal` keeps its signature. If a future consumer (audit, say) needs "load or commit", the fact goes on
`CultCacheDocumentChange` as an additive property, as `Sequence` did. The cache already knows it: `Admit`'s
`source != null` at `CultCache.cs:2420`. It never needs a new `AddJournal` shape.
- This is recorded, not forked: no consumer needs it today.
### The two recorded hazards, settled for Cut 4
- **Q-P2 typed-exception hazard.** Cut 4 makes it frequent. Every cache write on a primary with a durable log
now does log I/O inside its admission.
- AetheriaEve opts into durable logs (`Aetheria.State/AetheriaStateNode.cs:144`).
- Its `AetheriaHangarCommandJournal.ValidateAsync` writes an envelope (`Program.cs:6509`) inside the `catch
(InvalidOperationException)` at `Program.cs:3722`.
- A log failure surfacing as `InvalidOperationException` would therefore produce a "denied" receipt for a
committed envelope.
- Q-P5 A (ruled): the writer gets a typed `CultNetShardLogException` that **does not derive from
`InvalidOperationException`**.
- **Journal deadlock (the Cut 3 note).** The database journal waits only on `_logGate` and on the log store's
own lock.
- Lock order: cache gate, then `_logGate`, then the log store's lock.
- Invariant: **no code holding `_logGate` calls the cache, and no log store calls the cache or waits on a
thread that does.**
- Readers that take `_logGate` read only log state: `GetMutationLog`, `LastWriteSequence`, `CurrentAsOf`.
Readers already inside a cache read, such as the selection evaluator's `ordinalOf`, nest `_logGate` under
the cache gate, which is the same order.
- The store rule goes into the `ICultNetShardMutationLogStore` XML doc (`CultNetShardMutationLogStorage.cs:13-34`),
because a user store runs under the cache's gate.
- A log store that writes the cache already throws, through Cut 3's guard. Cut 4 pins it with a test.
## Authority map
- **Owner.** `CultCache`. The hold that mints a Sequence range also:
1. journals the range under the gate, in Sequence order (new);
2. publishes the range to every observer, isolated, on the minting writer's
thread, after it releases the gate and before it returns (main's model,
now isolated).
Nothing waits on another call's delivery.
- **Inputs.**
- The changes returned by `Apply` (`CultCache.cs:2707`), the single mutation
site.
- The pre-mint projection of names and indexes (Cut 1).
- The journal list, the observer array and `OnUpdate`.
- **Outputs.**
- Journal calls: one per admission, under the gate, in Sequence order.
- `Watch` / `WatchRecord` notifications that carry `Sequence`.
- `OnUpdate` for loads.
- Observer and journal exceptions, rethrown to the writer.
- **Derived state.**
- `Sequence` is order as data.
- CultNet's primary log sequence is minted in the journal, so log order is
cache order.
- A replica's log sequence is copied from the primary entry.
- The database change kind (`Predicted`, `Reconciled`, `Added`, `Updated`,
`Removed`) is decided in the journal.
- `CultNetDatabaseChange.Sequence` (new) is copied from the cache change.
- Wire projections drop stale by it (Cut 6).
- **Forbidden writers.**
- Nothing re-sequences or schedules cache changes. The branch's `_parked`,
`_ready`, `_nextSequence`, `_draining`, `PendingChange` and the drain body
of `OnCacheChange` die, and so do the turnstile, `bcae483` and the
prototype.
- No R3 `Subject` fans out cache changes (Q2).
- `Apply` and `Index` run no user code.
- Doors never read or write prediction state: the branch's `HasPrediction`
and `_predictionGate` die.
- The database's Watch observer never logs; only the journal logs.
- A journal never enters the cache.
- **Shared paths.** Every path goes `Held` → `Admit` → `Resolve` → **Project**
→ `land` → `Apply` → **journal** → leave gate → **publish**:
- loads: `AddBackingStore` `:1909`, `Loaded` `:1940`, `PullAll*` `:1959` and
`:3052`;
- `Commit` / `TryCommit` (`Land`, `:2272`);
- `UpsertAsync`, `UpsertVariantAsync`, `FlattenAsync` and `Remove`
(`:2004-2056`, `Write` `:2252`);
- a variant dependent's re-resolution (`VariantPlan.Dependents`, in the same
`Apply`);
- direct calls on an attached store (`:2924`);
- `Dispose` (`:2142`);
- the database doors, replica apply and snapshot resync, which only write
the cache and hand context to the journal.
The single exception is the replica's absent-key delete, which has no cache
change. It logs directly (§4).
- **Deletion line.**
- In the cache: `Subject<Change> _changes` (`:1809`), the `_changes.OnNext`
call and its false comment (`:2374-2375`), `_changes.Dispose()`
(`:2154`), and the accessor calls in `Index` (`:2798-2804`).
- In the database, on the salvaged branch: the reorder buffer and drainer,
`_predictionGate` and `HasPrediction`, the logging in `Deliver`, and the
`Subject<object> _changes` fan-out.
## The design, question by question
### 1. Validation before minting: a refused change mints nothing
- **Where.** Add **Project** to `Admit` (`:2313`), between `Resolve` (`:2324`)
and `land` (`:2325`).
- **What.** Over `plan.Admitted ∪ plan.Dependents`, it evaluates once per
record: `NameAccessor(document)` and every `IndexAccessors[alias](document)`.
It keeps `(Name, (Alias, Value)[])` per key on the plan.
- **A throwing getter refuses the whole admission.** It raises an
`InvalidOperationException` that names the key, the member and the unwrapped
message; today the caller sees a bare `TargetInvocationException`. The
refusal comes before `land`, so the admission writes no store, touches no
memory and mints nothing.
- **Readers take the projection, not the document.**
- `Index` (`:2793-2806`).
- `RefuseVariantIndexSharing` (`:2659-2660`).
- The variant-name rule in `ResolveOne` (`:2616`). Its ancestor read at
`:2621` reads held documents, pre-land, and may stay.
`Resolve` currently runs the refusal rules before any projection exists.
Hands makes Project the first pass over the resolved plan and moves both
rules after it, so rules read the projection and only the projection reads
documents.
- **Apply becomes user-code-free.** It then does only dictionary and
`ConditionalWeakTable` work on precomputed data.
- **Loads refuse too.** A store whose record's getter throws fails to open,
loudly. That is the rule "hydration failure on open is loud" (composition doc
`:88-92`). Today it half-indexes silently.
### 2. Observer isolation
- **Replace the `Subject<Change>` with the cache's own observer array**,
copy-on-write under a small lock.
- `Watch<T>` (`:1887`) becomes `Observable.Create<Change>(register)` followed
by the existing `Where` and `Select`.
- `Dispose` calls `OnCompleted` on each observer, which is what
`Subject.Dispose` did.
- **Publish loops change → observer, and wraps each `OnNext` in its own
try/catch.**
- R3's `Observer<T>` already catches handler exceptions (`OnErrorResume`, then
the unhandled handler). So an exception reaches the cache only when fail-fast
was asked for: a rethrowing `ObservableSystem` handler, or a custom observer.
- Escaped exceptions join the `OnUpdate` failures. They are rethrown to the
writer after every observer has received every change of the hold. That is
the existing `OnUpdate` policy (composition doc `:77-82`).
- The observer stays subscribed (**Q-P2**). The commit stands either way.
- **Prototype.** At `79f4639` (Q2) this took about 15 lines. Hands keeps that
part and drops the turnstile around it.
### 3. Delivery thread, order and reentrancy
- **Unchanged from main, on purpose.** Each outermost hold publishes exactly its
own changes, on its thread, after it leaves the gate and before it returns
(composition doc `:59-65`, `Held` `:2339`).
- A write from inside a handler is its own outermost hold. It delivers
depth-first before returning, as Q4 shows on main.
- There is no cross-thread order. Every change carries `Sequence`, and
latest-state consumers drop stale changes by it (doc `:68-73`, and the Mesh
mirrors at `CultMesh.cs:1534-1540`).
- **Why this is not a regression of the ruling.** The ruling asks for
exactly-once, not for a total delivery order.
- Exactly-once in the cache comes from the single mint site plus isolation
plus Cut 1 (§1-2).
- Order is needed by exactly one consumer, the shard log. It gets that order
from the journal (§4), not from delivery.
- **The journal is the new mechanism.**
- Signature: `IDisposable CultCache.AddJournal(Action<IReadOnlyList<CultCacheDocumentChange<object>>>)`.
Its name and shape are **Q-P3**.
- It is called in `Admit` right after `Apply` (`:2328-2329`), under the gate,
once per admission, with that admission's changes in Sequence order.
Journals run in registration order.
- A journal that enters the cache, meaning any hold on this cache while a
journal runs, throws `InvalidOperationException`. It is guarded by a flag
checked in `Held`'s `Monitor.IsEntered` branch (`:2341`). This is the
store rule ("a store call must not wait for a write to its own cache"),
enforced.
- Journal exceptions follow the observer policy: the admission stands, it is
still published, and the exception is rethrown to the writer after
publication.
- Adding or removing a journal takes the gate, so registration is atomic with
respect to admissions.
- **Lock order.** Cache gate, then journal-internal leaf locks (the database's
`_logGate`). Observers still run outside the gate, so the `bcae483` cycle
(an observer needing `_lifecycleGate` while a gate holder writes the cache)
cannot form. No journal takes `_lifecycleGate`, and no writer waits on a
delivery.
- **Soul's P2 passes.** A handler waiting on another thread's write sees it,
because nothing orders deliveries (Q7 on main).
### 4. What CultNetDatabase keeps
The database is rebuilt on the journal. It has no queue.
- **Journal (under the cache gate, per admitted change, in Sequence order):**
1. `TakeDoor(change)`, with the shape rule salvaged from the branch
(`:1219-1234`). A put door matches a change whose new document is its
instance and whose previous document is not. A removal door matches a
change whose previous document is its instance and which has no new one.
- The consumption now happens inside the door's own hold, before any
observer runs. So an observer's later write to the door's key or
instance can never wear the context (the `f3343c8` defect), and
unconditional `Dispose`-removal stays correct.
2. Predictions. A `Predicts` door adds the prediction. On an `Authoritative`
door (`ApplyPutAsync`, replica put), if a prediction exists for the key,
the kind is `Reconciled` and the prediction is removed.
- The gate serializes this, so `_predictionGate` and `HasPrediction` are
deleted (F5).
- Soul's P6 cannot happen: the prediction is recorded before
`PutPredictedAsync` returns.
3. Primacy.
- On a shard primary, where the door is not `NoLog`: mint, build, append disk-first and record, with a failed
append burning the sequence and compacting through it. See "(a) A journal that cannot log a committed
change" in Imagination pass 4; the earlier claim that a serialization failure mints nothing is withdrawn.
- On a replica, record the primary's entry from the `Replica` door,
under its own sequence.
- For a local write on a replica, log nothing.
4. Stash `(cache Sequence → CultNetDatabaseChange)` under `_logGate`, for the
publisher.
- **Watch observer (after the gate, on the writer's thread, before it
returns):** it removes the stash entry for `change.Sequence` and publishes it.
- A change with no stash was admitted before the database existed and is
ignored.
- Register the observer first and the journal second, in the constructor, so
a stash can never be orphaned.
- **Log-before-publish** is structural: the journal runs before the gate opens.
- **The replica's absent-key delete (F6)** logs directly under `_logGate`. It
is in order because every earlier entry of the same pull loop was journaled
inside its own hold, before that `UpsertAsync`/`Remove` returned. This holds
even when a pull runs inside a handler, since nested holds journal
synchronously.
- **The in-memory log is keyed and ordered by sequence.** It becomes a
`SortedList<long, entry>` per shard, to match the durable store
(`CultNetShardMutationLogStorage.cs:55-64, 75-89`: that store replaces by
sequence and orders by sequence).
- **Kept from the branch:**
- the door table keyed by instance identity, and the shape rule;
- "only a primary mints; replicas publish";
- log-before-publish;
- the snapshot resync "logs nothing" rule. Its removal door (branch `:733`)
must set `Removal = true`, or the shape rule never matches it.
- **Database fan-out isolation.** The database's `Subject<object> _changes`
(main `:487`) has the Q2 defect one layer up: the subscription server's
per-peer handlers sit on it. Replace it with the same isolated
observer-array pattern as §2 (about 20 lines). Its exceptions reach the
writer through the cache's own per-observer catch.
- **`CultNetDatabaseChange<T>` gains `long Sequence`**, the cache change's
Sequence, as an optional constructor argument, exactly as
`CultCacheDocumentChange` did (composition doc `:57-58`). Without it,
in-process latest-state consumers cannot drop stale changes.
- **Gone:** F3 (no deferred delivery), F4 (no drainer), F7 (no queue to
strand) and F8 (no guard).
### 5. Backpressure and memory bounds
- **No buffer exists.** A writer delivers its own changes, and they are held
only in its `mine` list for the length of its own call.
- **The journal stash** holds at most the changes of holds between their
journal and their publication. That is one hold per writing thread, and each
entry is removed by the same thread that added it.
- **A slow observer slows only the writer whose change it is handling.** That
is main's behaviour. It does not slow other writers, and it does not grow
memory. This replaces the branch's unbounded drainer backlog (F4, Soul P5).
- **The journal runs under the gate, so a slow journal slows every writer.**
- The database journal does in-memory work plus one durable
`mutationLogStore.Append` per logged change. `Append` rewrites the shard
file (`CultNetShardMutationLogStorage.cs:83-89`).
- That is I/O under the gate, which the locking doctrine already allows for
stores ("A store doing I/O on behalf of its cache ... holds the gate",
composition doc `:98-101`). It is still a new cost, so Cut 4's
verification measures commit latency with a file log store.
- The whole-file rewrite is a pre-existing monolith. It is recorded as a
follow-up, not fixed here.
### 6. Cross-runtime impact
The Eyes survey was run at `65311c9`.
- **`cultcache-rs`, `cultcache-ts` and `cultcache-py` have no publication
surface:** no watch, no listener and no Sequence.
- Rust: `lib.rs:1970-1975, 2213-2420`.
- TypeScript: `cult-cache.ts:164-181`, "Observer delivery is not scheduled
here".
- Python: `cache.py:30-45`, which refuses reentrant mutation.
- **`cultnet-rs`, `cultnet-ts` and `cultnet-py` have no CultNetDatabase
equivalent and no cache watcher.** The only server-side shard log is the
Python interop test peer (`interop_peer.py:565, 583`), which mints
`len(log)+1` under its own lock, inline with the write. That is already the
journal's shape.
- **Wire parity is unaffected.**
- Cache `Sequence` never goes on the wire (composition doc `:47-49`).
- `CultNetShardLogEntryMessage` does not change.
- `CultNetDatabaseChangeRawMessage` (`CultNetSchemaMessages.cs:1016`) carries
no sequence, and this map adds none: stale protection for wire
subscriptions is server-side (Cut 6).
- No vector in `tests/vectors/` encodes publication order.
- **No other runtime must change.**
- **Follow-up.** `cultmesh-py`'s `CultMeshDatabase` watch (`node.py:421-505,
1059-1065`) has F2 (a throwing callback skips the rest and reaches the
writer), and it has no Sequence. When Python grows cache publication, it
takes this contract.
## Cuts
Every cut is on CultLib, branch `hands/cultcache-publication` from `main`
`65311c9`, in a worktree Self creates. Cuts 1-3 touch only the cache and land
first. Cuts 4-6 depend on Cut 3. Heavy verification runs on Yggdrasil.
**The negative checks that stay green in every cut** are the `4562340` tests:
- `ObserverWritingTheSameCacheDoesNotWaitOnOtherDeliveries`;
- `SubscriptionDemandHandlerWritingCacheDuringDeliveryDoesNotDeadlock`;
- `SequenceIncreasesInAdmissionOrder`;
- `WatchRecordIgnoresStaleSequence`.
They are the proof that no scheduler has come back.
### Cut 1. Project before mint (cache, behaviour)
- **First.** Commit Q1 as a failing test named for the rule.
- **Deletes first.**
- Accessor calls in `Index` (`:2798-2804`).
- Direct accessor reads in `RefuseVariantIndexSharing` (`:2659-2660`) and
`ResolveOne` (`:2616`).
- **Adds.**
- `Project` in `Admit` (`:2324-2325`).
- A per-key projection on `VariantPlan` (`:2456`).
- The early returns in `Resolve` (`:2477-2478`, `:2530-2534`) still pass
through `Project`, because it is called from `Admit`.
- **Authority.** Project owns "this admission's name and index values", and
the rules and `Index` read them. `Apply` runs no user code.
- **Verification.**
- Tests:
- **A refused getter mints nothing and changes nothing.** `GetWithSequence`
is unchanged, both keys are absent, the name lookup misses, nothing is
published, and the next write is previous+1.
- **Store-backed batch:** the store file is byte-identical, and a reopen
holds neither key.
- **A load whose getter throws** refuses the open and leaves the file
byte-identical.
- **Once per landed record:** a counting getter runs exactly once per
landed record, including a variant dependent re-resolved by a base edit.
- **The refusal names** the key and the member.
- Soul P4.
- Negative grep: `rg -n "NameAccessor\?\.Invoke|IndexAccessors" src/GameCult.Caching/CultCache.cs`
has no hit in `Apply`, `Index` or `Unindex`. `:315` is store-side and
legitimate.
- Stryker.NET on the diff.
- **Ledger.** About −15 and +45.
### Cut 2. Observer isolation (cache, behaviour)
- **Deletes first.**
- `Subject<Change> _changes` (`:1809`).
- `_changes.OnNext` and the false comment (`:2374-2375`).
- `_changes.Dispose()` (`:2154`).
- **Adds.**
- The observer array and `Observable.Create` in `Watch<T>` (`:1887-1897`).
- The per-observer catch in `Publish` (`:2369`), with escapes joining
`failures`.
- `OnCompleted` on `Dispose`.
- **Docs.** Composition doc `:77-82`: escaped observer exceptions follow the
`OnUpdate` policy.
- **Verification.**
- Tests:
- **Q2 with a rethrowing handler:** the second observer receives every
change. The writer gets the escaped exception after delivery, and the
thrower is still subscribed (per Q-P2).
- **Subscription order:** subscribing and unsubscribing during a delivery
leaves every other observer's stream complete.
- **Dispose** completes observers.
- Soul P3.
- The four `4562340` tests stay green.
- Stryker on the diff, especially removing the catch and breaking out of
the loop on the first failure.
- **Ledger.** About −5 and +30.
### Cut 3. The journal (cache, behaviour)
- **Adds.**
- `AddJournal` (Q-P3), with registration under the gate.
- The invocation in `Admit` after `Apply` (`:2328`).
- The no-reentry guard in `Held` (`:2341`).
- Exceptions routed like observer failures.
- **Docs.** A composition-doc paragraph in "Store composition" and in
"Locking": journals run under the gate, in Sequence order, and must not
enter the cache or wait for anything that needs it.
- **Verification.**
- Tests:
- **Order:** 8 threads × 1000 writes. The journal sees contiguous,
strictly increasing Sequences, and each call's changes arrive together.
- **Timing:** the journal runs before any observer of the same change, and
after `Apply` (`Get` inside the journal sees the new document).
- **Reentry:** a journal that writes the cache throws, and the admission
stands.
- **Refusal:** a refused admission journals nothing.
- **Dependents:** a variant dependent is journaled in the same call as its
base edit.
- **Failure:** a throwing journal still lets the change publish, and the
writer gets the exception.
- The `4562340` tests stay green.
- Stryker on the diff.
- **Ledger.** About +50.
### Cut 4. Salvage the branch and rebuild CultNetDatabase on the journal (networking)
- **4a (history).** `git merge origin/hands/cultnet-publish-commits` into the cut branch from main
`3bf1c0c`.
- It is clean: the probe merge `6ec2f73` auto-merged `NetworkingTests.cs` and the composition doc, then
built and ran.
- The merge brings the tests. The queue dies in 4b.
- Self may squash-salvage instead, with the same content.
- **4b (subtraction first).** Delete at branch `28248eb` `CultNetDatabase.cs`:
- `_parked`, `_ready`, `_nextSequence` and `_draining` (`:502-505`);
- `_predictionGate` (`:506-507`);
- the Watch subscription and handshake (`:529-531`);
- `OnCacheChange` (`:1080-1135`);
- `PendingChange` (`:1265-1275`);
- `HasPrediction` (`:1209-1213`) and its calls (`:916`, `:1574`);
- the logging and prediction bookkeeping in `Deliver` (`:1137-1201`).
Main's `PublishCacheUpdate` / `OnUpdate +=` (`:505`, `:1157`, `:1161-1191`) is already gone on the branch.
Hands confirms it is absent after the merge.
- **4c (the journal).** Register the Watch observer first and the journal second (`AddJournal`), in the
constructor. Per admission, under the cache gate, per change in Sequence order:
1. `TakeDoor(change)`, keeping the branch's shape rule (`:1219-1234`).
2. Resolve the shard and kind. Predictions: a `Predicts` door adds its key. An `Authoritative` door
(`ApplyPutAsync`, replica put) holding a prediction for the key makes the kind `Reconciled` and removes
the key.
3. Log, per §(a):
- a replica door records the primary's entry, disk first;
- a primary change that is not `NoLog` mints, builds, appends and records, with failure going to the
floor;
- anything else logs nothing.
4. Stash `(cache Sequence → CultNetDatabaseChange)` under `_logGate`.
5. If step 3 failed, throw once at the end of the call, after all changes are handled (Q-P5).
- **4c, publication.** The Watch observer removes the stash entry for `change.Sequence` and publishes it. A
missing stash means the change was admitted before this database existed, so it is ignored. `Dispose`
removes the journal and the observer and clears the stash.
- **4d (the §(a) fixes).**
- The floor: `GetCompactedMutationLogSequence`, `GetLatestMutationLogSequence`, and resume in
`InitializeLogSequencesFromStore`.
- Disk-first `RecordMutationLogEntry`.
- A `SortedList` per shard.
- The snapshot reads its sequence first (`:660` moves below `:668`'s read).
- `_logGate` around `_lastWriteSequence`/`_nextLogSequences` readers (`:1302`, `:1314`, `:1325`).
- **Door changes.**
- Flags: `Predicts` and `Authoritative`.
- The snapshot removal door (branch `:733`) sets `Removal = true`.
- The replica absent-key delete logs directly and publishes nothing (branch `:1612-1621`).
- Doors neither log nor publish. They register context, write the cache, and dispose the registration.
- **Verification.**
- Every salvaged test passes: `CultNetPublishesCommitsTests` (13) and `CultNetPublicationOrderTests` (S1,
S1b, S2, S3, S4, S6, S7, log-before-publish, in-place re-upsert).
- Soul pass 3's P1-P7 are committed and passing.
- The pass 4 probes become tests named for the rule:
- **A1:** a refused primary `Append` publishes the change. The writer gets `CultNetShardLogException`,
which names the shard and `S`. A replica pull answers `compacted_history`, and a snapshot resync
converges.
- **A2:** the same at the tail with a restart: the replica converges after resync, and no sequence is
reused. The store's `CompactThrough` succeeded.
- **A3:** a refused replica `Append` leaves the cursor unchanged. The retry converges, and the memory log
holds `[1,2]`.
- **B1:** a pull on a primary is logged, and the replica converges.
- **S:** a snapshot racing a write converges.
- **C:** after compacting through 2 and restarting, the next mint is 3.
- New tests:
- a reentrant `database.PutAsync` from inside a database handler is logged with its context and published
once;
- a reentrant `ApplyShardLogResponseAsync` from inside a handler keeps the replica log in primary order;
- a log store whose `Append` writes the cache fails with Cut 3's reentry guard, wrapped, and the admission
stands.
- Commit latency with `CultNetFileShardMutationLogStore`, main against Cut 4. Also measure a `CultMesh` node
with `EnableDurableShardLogs`, where every bookkeeping write is now logged. Report it; do not gate on it.
- Negative grep: `rg -n "_parked|_ready|_draining|_nextSequence|_predictionGate|HasPrediction|PublishCacheUpdate" src/GameCult.Networking`
returns nothing.
- Review check: no `_cache.` call inside a `lock (_logGate)` block.
- The `4562340` tests stay green.
- Test runs use `--blame-hang-timeout 2m`.
- Stryker on the diff: dropping the floor raise, memory-before-disk, and the snapshot read order must each
kill a test.
- **Docs.**
- Composition doc `:50-53`: replace the "streams derive from `OnUpdate`" sentence. CultNet derives from
`Watch` plus one journal, and the log is written in cache order.
- A new paragraph: a primary that cannot log a change compacts through it.
- The log-store rule under Locking.
- **Blockers.** None. Q-P5 is ruled A.
- **Ledger.** Against the branch, about −130 and +95. The extra over the old estimate is the floor, the
disk-first order, the `SortedList`, the snapshot order, and the exception type.
### Cut 5. Database fan-out isolation and Sequence (networking, behaviour)
- **Deletes first.** The database's `Subject<object> _changes` (main `:485`, branch `:487`) and its
`Dispose` (`:1158`).
- **Adds.**
- The isolated observer array, the same pattern as the cache's `Register`/`Publish`
(`CultCache.cs:1929-1944`, `:2470-2503`), behind `Watch<T>`, `WatchAllChanges` and the rest (`:1089-1145`).
- `CultNetDatabaseChange<T>.Sequence` (`:319-371`), as an optional constructor argument. The journal's stash
sets it from the cache change. A change with no cache change behind it carries 0.
- **Docs.** The binary-signature note, as the composition doc gave for `CultCacheDocumentChange` (`:57-58`).
- **Verification.**
- With a rethrowing handler and one throwing database subscriber, the other subscriber and a live
subscription-server peer receive every change. The writer gets the exception after delivery (Q-P2).
- The disposed-database test.
- `Sequence` equals the cache change's.
- **Blockers.** Cut 4.
- **Ledger.** About −10 and +35.
### Cut 6. Stale protection on wire projections (networking, behaviour; Q-P4 A)
- **The rule.** A projection applies a change to a key only if its `Sequence` is above what the projection
last applied for that key. A snapshot or reconcile applies everything at or below the cache sequence it read
**before** reading documents.
- That read is `Cache.GetWithSequence(<any key>).Sequence`, the branch's idiom (branch `:531`). No new API
is needed.
- **Subscription server** (`CultNetDatabaseSubscriptionServer.cs`):
- `SubscriptionProjectionState` (`:630`) gains `long Floor` and `Dictionary<string, long> Applied`.
- The single-row fast path in `Watch` (`:296-340`) reads the change's `Sequence` (reflection, like `Key`).
Under `_lifecycleGate` it drops the change if `Sequence <= max(Floor, Applied[key])`. Otherwise it applies
the change and records it. Removals are included.
- `Reconcile` (`:347`) and the subscribe snapshot (`:168-181`) read `seq0` before `CreateProjectedSnapshot`
(`:402`), set `Floor = seq0`, and clear `Applied`.
- This is sound: every change applied earlier was applied under the same gate, after its admission, so
its `Sequence` is at most `seq0`.
- Hop and unauthorized paths already reconcile from current state, and they set the floor too.
- **`CultNetDatabaseServer.CreateSubscription`** (`:501-513`):
- A per-subscription `(Floor, Applied)` under a per-subscription lock, held across check, record and send.
- `HandleSubscribeAsync`'s snapshot (`:362-369`) takes `seq0` first and sets the floor.
- **Verification.**
- Two threads write one key, with a blocking cache observer that forces the inverted delivery (Q3 shape).
On both servers, the peer's last state for the key equals the cache's.
- A removal overtaken by an older upsert leaves the key absent at the peer.
- The negative check: without the drop, both tests fail.
- **Blockers.** Cut 5, which provides `Sequence` on database changes.
- **Ledger.** About +45.
### Salvage from `hands/cultnet-publish-commits` (revised)
- **Keep:**
- both test files;
- the `NetworkingTests.cs` edits;
- the door table and the shape rule;
- "replicas never mint";
- log-before-publish;
- "the snapshot logs nothing";
- "loads and pulls on a primary are logged" (B1);
- the composition-doc paragraph, rewritten to name the journal.
- **Drop:**
- the reorder buffer and the drainer;
- `_predictionGate`;
- logging from the observer;
- the swallowed exception path (A1 and A3 on the branch).
## Subtraction ledger (estimate)
| Cut | Removed | Added | Other |
|---|---|---|---|
| 1 | ~15 | ~45 | none |
| 2 | ~5 | ~30 | the cache no longer uses R3 `Subject` |
| 3 | 0 | ~50 | one public method (Q-P3) |
| 4 | ~130 vs branch | ~95 | the merge brings +1109/-243, of which about 780 are tests; the floor, disk-first order, `SortedList`, snapshot order and `CultNetShardLogException` are the extra over the first estimate |
| 5 | ~10 | ~35 | the database `Subject` goes, and one public property |
| 6 | 0 | ~45 | none |
- **Source, net against main:** about +245.
- **Source against the branch:** no scheduler anywhere.
- The budget is pressure, not a metric.
## Operator questions
- Q-P1..Q-P4 are ruled (see Rulings). Their option analysis is in the map's git history.
- **Q-P5 A (ruled 2026-09-29): a primary that cannot log a committed change throws a typed
`CultNetShardLogException` naming the shard and the burned sequence, after publication, not derived from
`InvalidOperationException`.**
- The commit stands, the change is published, and replicas behind it snapshot-resync (Imagination pass 4 (a)).
- The writer hears of it. AetheriaEve `Program.cs:3722` cannot mistake it for a denial.
Recorded, not forked:
- (b): no origin on the journal. Loads and pulls are logged on primaries.
- (a)'s restart residual when the store refuses both writes.
- The replica keeps retry-by-cursor.
- A throwing getter refuses loads too.
- Reentrant writes keep main's depth-first delivery. Order is data.
- Journal I/O under the gate follows the store precedent.
## Follow-ups outside this campaign
- **`cultmesh-py` watch.** It has no isolation and no Sequence
(`node.py:1059-1065`).
- **Durable log whole-file rewrite.** `CultNetShardMutationLogStorage.cs:83-89`
rewrites the whole file per `Append`. It is a monolith, and under the gate
its cost is now every writer's.
- **Mesh database-backed handles.** `CultMesh.cs:2362` and `:3396` can drop
stale changes once `CultNetDatabaseChange.Sequence` exists.
- **Skill scar.** The Eureka SKILL.md already says "a ticket system that
serialized delivery was built, deadlocked, and was deleted". The Soul pass 3
direction re-proposed it anyway. The Soul brief template should tell Soul to
check `git log -S` for a proposed mechanism before recommending it.
- **A chained replica after a snapshot.** A replica that snapshot-resyncs has no log entries below the snapshot
sequence. It should raise its own floor to that sequence, so a replica chained on it resyncs instead of seeing
a gap (`ApplyShardSnapshotResponseAsync` `:675-736`). It predates this campaign.
- **AetheriaEve `Program.cs:3722`.** It reads a committed write's `InvalidOperationException` as a denial. Under
Q-P5 A, Cut 4's log failures cannot reach it. Observer and `OnUpdate` exceptions still can, so the Cuts 1-2
recording stands.- **Cut 4 Soul, recorded 2026-09-30 (Self), not fixed in Cut 4:**
- **File log store is not crash-safe (hard prerequisite).** `CultNetFileShardMutationLogStore.Append` and
`CompactThrough` use `File.WriteAllBytes` (truncate, rewrite, no temp+rename, no fsync). A failure mid-write
truncates the log; every later append burns a sequence and a restart cannot construct the database. It also
rewrites the whole file per append, which Cut 4 now does under the cache gate (2.2 ms per write at 100
entries, 33 ms at 950, a 2.49 s stall). An atomic, append-only store must land before any live daemon
enables durable shard logs. Today only AetheriaEve's offline tools do; the live Aetheria daemon passes
`enableDurableShardLogs: false`.
- **A slow log append blocks cache readers** (`CultCache.Get` takes the gate). Accepted with a note in the
store's XML doc.
- **Crash between the store write and the log append** leaves the log lagging the store, and nothing detects
it; a restarted primary reuses that sequence (A2's divergence reached by a plain kill). The spec called this
lag detectable; for a crash it is not.
- **Cut 4 final Soul (4d), recorded 2026-09-30 (Self), not fixed in Cut 4:**
- **F2, read cost.** `CultNetDatabase.Serves` (`:1980-1989`) runs per row in `GetAll`/`GetByName`/`GetByIndex` for every
layout except a single owns-everything shard: a reflection `GetTrackedKey`, a gate lock and a shard re-scan per row.
`database.GetAll` measured 24.8x `cache.GetAll` on 20,000 rows under a key-prefixed shard. Fix: filter by key inside
the cache enumeration (the `rowFilter` path already has keys) and hoist the owns-everything check.
- **F4, dispose drain edges.** An observer that removed its stash entry before the drain can publish after `Dispose`
returns (dropped); a throwing `_cacheJournal.Dispose()` skips the drain. Neither double-publishes or deadlocks.
- **F5, refusal naming.** A write whose schema is owned but whose key is outside the shard's prefix also answers
`unowned_schema` with details `{field:"schemaId"}`; the peer cannot tell a key refusal from a schema refusal.
- **Pre-existing, separate task.** Python emits error code `unsupported_schema_version`
(`cultnet-py/.../interop_peer.py:415`, `cultmesh-py/.../server.py:161`), which is not in the error-code enum; TS and
Rust refuse the whole message.

View file

@ -0,0 +1,307 @@
# CultCache Union Parity: Map
Campaign `cultlib-gaps`, follow-up `cultlib-gaps:follow_up:union-parity-and-unknown-arms` (from
`thing2:follow_up:cultcache-union-parity`), under operator ruling `thing2:ruling:contract-decode-tolerance`
("CultCache refuses unknown union arms in every runtime, the C# reference included"). This file holds body facts,
the model page and rationale; the cut specs and the question are documents in the mind and cite this file by
section and fact number.
Pinned base: `origin/main` at `aaf0d7f6` (2026-10-10). Imagination pass `imagination-cl-union-parity`, session
`self-2026-10-10c-eureka`. Probes live under the session scratchpad, `imagination-cl-union-parity/` (`cs` payload
probe, `cs2` store probe, `rs` payload probe, `py` store walkers, `il` decompiled MessagePack-CSharp). None of it is
committed. Earlier evidence is not repeated here: Eve `docs/thing2-map.md` P12-P13 (the cross-runtime probe) and
`docs/research/cultcache-union-decode-tolerance-prior-art.md` (prior art).
## Cut order
1. `union-refusal-cs`: the C# reference refuses an unknown union arm on read and a subtype the union does not name
on write. Ruled; nothing blocks it.
2. `union-golden`: the recursive-union golden and the arm refusal goldens in `tests/vectors/union-parity/`,
written by the C# reference and held by its tests.
3. `union-derive-rs` and `union-codec-ts`, in either order or together: cultcache-rs and cultcache-ts gain a
union (and, in TS, a slot record) codec that writes the reference shape and decodes the golden byte-identically.
`union-codec-py` follows the same golden for cultcache-py.
4. `slot-tolerance` (C# reference), then `slot-readonly-parity` (Rust, TypeScript, Python): a record carrying a
slot past its reader's declared slots, at any depth, is read-only (ruling
`cultlib-gaps:ruling:extra-trailing-slots`; the refinement is settled by section S). Both change a slot rule the
cuts in step 3 build, so they wait for 1-3 rather than reopening them.
5. `union-parity-release`: changelogs and tags, Breaking where `docs/semver-policy.md` says so.
Thing2's `cut-contract-rs` waits on step 3 (Rust) only: its first step pins cultcache-rs at the commit that lands
`union-derive-rs` and re-runs P12 or the `union-golden` vector both ways.
Why this order: the C# reference is the wire authority, so its refusal and its golden come first and the other
runtimes are held to bytes C# wrote, as `document-variants-c1` was (`docs/runtime-parity-scope.md`). Arms are
ruled and slots are not, so the slot rule is its own cut rather than a blocked step inside every runtime's cut.
## Model page (step 0b)
| Kind | What names it | Over time | Who decides |
|---|---|---|---|
| Union arm | An integer key, `[Union(k, typeof(Arm))]` on the abstract base (C#), assigned by the union's author. The wire carries `[k, armSlots]`; the arm's type name never travels. `nil` is a null reference, not an arm. | **Added:** a key never used before. **Retired:** the attribute is removed or commented out and the key is never reused (Aetheria `Behaviors.cs:160,173`; Aetheria-legacy `Behaviors.cs:37-38`). **Renamed type:** free, the key is the identity. **Unknown to a reader** (a newer writer's arm, or a retired arm in an old store): refused, naming the union and the key (ruling). | The union's author assigns keys. The C# reference defines the shape; every runtime keeps parity with it (doctrine, `runtime-parity-scope.md`). |
| Arm subtype on write | The value's runtime type must be one the union names. | A subtype the union does not name is today written as `nil` (U4). After cut 1 it is refused. | The union's author, by naming the arm. |
| Arm slots, record slots | `[Key(n)]` integers, base-class keys included (an arm is a flat array). Gaps are written `nil`. | **Added:** appended at the next free key. **Retired:** kept as a gap, never reused (Aetheria `ItemInstance.cs:35`, `EntitySerializer.cs` key 5). **Missing in an older store:** the reader's default (C# field initializer; Rust and Python need a declared default). **Extra past the reader's last key, at any depth:** the record is read and is read-only; every write or removal of it is refused, and it is persisted from its original bytes (ruling, S). | The type's author. The slot rule is CultCache's (`src/GameCult.Caching/Contracts/cultcache-schema-compatibility.md`). |
| Serde-enum member (Rust) | serde's external tag: the variant **name** (`{"Name": fields}`, a unit variant the bare string). | Persisted already by Rust consumers (section C). They keep their bytes: the union derive is opt-in and serde enums are untouched. | The Rust consumer. Not a cross-runtime shape. |
## U. Mechanism facts (C# reference, Rust, TS)
- **U1. The miss branch is `Skip` then `null`, in both union formatters MessagePack-CSharp 3.1.7 can hand out.**
`DynamicUnionResolver.BuildDeserialize` (decompiled from `MessagePack.dll` 3.1.7 net9.0 with ilspycmd): after
`ReadArrayHeader() == 2` (else `InvalidOperationException "Invalid Union data was detected"`) and `ReadInt32`, a
non-zero-start key set is mapped through `keyToJumpMap.TryGetValue`, a miss sets the index to `-1`, the `switch`
default calls `reader.Skip()`, and the result local stays `null`. The source generator's `UnionTemplate`
(`MessagePackAnalyzer` 3.1.7, `MessagePack.SourceGenerator.dll`) emits the same: `default: reader.Skip(); break;`
with `result = null`. Its serializer writes `nil` when `typeToKeyAndJumpMap` has no entry for `value.GetType()`.
- **U2. The C# reference decodes an unknown arm to null and does not re-encode to its bytes.** Probe `cs`
(payload built by hand with `MessagePackWriter`, decoded with `CultDocumentMessagePackSerialization.OptionsFor`):
unknown key 9 in a union field gives `Root=NULL`, in a list `Items=[TextNode,NULL]`; re-encode equal: False in
both. The formatter the reference composite hands out for the union is `MessagePack.Formatters.NodeFormatter1`
(DynamicUnionResolver's emitted type; the probe assembly carries no generated resolver).
- **U3. Missing and extra trailing slots, C# payload level.** Same probe: a payload one slot short decodes with
the field initializer (`Added=42`); a payload one slot long decodes with the extra slot skipped. Neither
re-encodes to its bytes.
- **U4. The reference writes an unnamed subtype as nil.** Same probe: `Root = new RogueNode()` (a subclass the
union does not list) serializes to `94 a2 64 32 c0 90 2a`: the root slot is `nil`, and it reads back `Root=NULL`.
Silent loss on the write side, the mirror of U2.
- **U5. Refusal can live at the CultCache layer; no MessagePack-CSharp fork.** Same probe, `strict` options: a
resolver placed ahead of `StandardResolver` returns, for any abstract or interface type with `[Union]`s, a
formatter that peeks (`CreatePeekReader`) the `[key, ...]` header, throws
`MessagePackSerializationException("union Node has no arm 9")` for a key the union does not list, refuses to
serialize an unnamed subtype, and otherwise delegates to the formatter `StandardResolver` would have handed out.
Baseline round-trips byte-identical; both unknown-arm cases and U4 are refused; slot behaviour is unchanged.
Every C# decode path reaches the composite through `CultDocumentMessagePackSerialization.OptionsFor`
(`CultCacheMessagePack.CreateCodec`, `DirectoryMessagePackBackingStore.cs:345`, `CultMesh.cs:2779,3493,3499`,
`CultMeshPrimitives.cs:1158,1230,2357`), so one resolver covers the cache, the directory store and CultMesh.
Consumer resolvers named by `[CultCacheFormatterResolver]` sit ahead of `CultDocumentResolver` today
(`CultDocumentMessagePackSerialization.cs:58-62`); the guard goes first and delegates to the rest of the
composite, so a consumer formatter still encodes the arm while the key check still runs.
- **U6. A top-level extra slot is dropped on write-back.** Probe `cs2`: a C#-written store, mutated as a newer
writer would (catalog member slot 2 added, payload slot 2 `"newer writer data"`), is pulled, the record edited
and flushed. The read succeeds, and the written payload is `['n1', 'edited by the older build']`: the newer
writer's slot is gone. (The schema id was left unchanged, so the migration report said `Kind=Exact`; with a
changed id the documented drift path reports `IgnoredExtraSlots`, `CultCache.cs:840-924`, but writes back the
same way.)
- **U7. Rust at `aaf0d7f6`, before the union cuts.** Probe `rs` (`cultcache-rs` at `aaf0d7f6`, payloads built with `rmpv`, decoded with
`rmp_serde::from_slice`): a serde enum's unknown variant is refused (`unknown variant 'Exploded'`); the C# arm
shape `[0, [...]]` is refused (`expected variant identifier`); a non-nil value in a gap slot is skipped; a missing
trailing slot is refused (`missing field 'added'`) unless the field is `#[cultcache(default)]`; an extra trailing
slot is skipped (`cultcache-rs-derive/src/lib.rs`, the `IgnoredAny` loop after the slot loop). The derive
accepts named structs only (`lib.rs:51-67`) and writes an empty member list into the catalog
(`src/lib.rs:2706`).
- **U8. TypeScript at `aaf0d7f6`, before the union cuts.** No typed codec: with no `formatter` a document encodes as a MessagePack **map** of the
zod-parsed object and decodes raw (`cult-cache.ts:679-692`; `@msgpack/msgpack` `encode({id:'d',version:7})` is
`82 a2 69 64 ...`, a fixmap, where the reference writes an array); positional slots exist only in hand-written
formatters (`test/cult-cache.test.ts:1263-1310`). P13 measured that a hand codec matches whichever encoding its
author chose.
- **U9. Python at `aaf0d7f6`, before the union cuts.** `define_database_entry_type` (`packages/cultcache-py/src/cultcache_py/documents.py:185-276`)
is a slot codec: gaps written `nil`, extra slots ignored, a missing slot defaulted or refused. No union codec.
- **U10. The written contracts.** `cultcache-schema-compatibility.md:75-91` classifies "persisted slot is missing
locally and can be ignored" as soft-migratable drift, reported as `IgnoredExtraSlots`;
`cultcache-persistence-format.md:215-218` says a store reader "never skips a slot it does not understand" for
record envelope slots, and every runtime refuses `extra-slot-full-payload.msgpack`
(`tests/vectors/document-variants-c0`). `docs/persisted-record-encoding.md` says consumers verify canonicality
by re-encoding and comparing bytes. The two contracts disagree about payload slots; the question below is
about that disagreement.
- **U11. Fixture home.** `tests/vectors/<name>/`: committed bytes every runtime's tests read
(`document-variants-c0` by Python `generate.py`, `document-variants-c1` written by C#), consumed by
`tests/GameCult.Caching.Tests/PreCut2StoreFormatTests.cs`, `packages/cultcache-rs/src/lib.rs`,
`packages/cultcache-ts/test/cult-cache.test.ts` and `packages/cultcache-py/tests/test_cultcache.py`. The CultNet
precedent for a reference-written vector is `CULTNET_WRITE_VECTORS=1` (`docs/cultnet-error-contract-cut.md:170`).
## S. Slot read-only probe (any depth)
Imagination pass `imagination-cl-slot-readonly` (session `self-2026-10-10c-eureka`), under ruling
`cultlib-gaps:ruling:extra-trailing-slots` ("b, and probe the any-depth refinement"). Probes live under the session
scratchpad, `imagination-cl-slot-readonly/` (`cs` C# synthetic, `aeth` C# on Aetheria's game database, `rs`, `py`,
`ts`, `il` decompiled MessagePack-CSharp 3.1.7). None is committed. The ruling's test: every runtime's decoder
carries a "slot skipped" mark to the record cheaply, on the one decode path, with no second pass and no per-type
consumer code.
- **S1. Where C# skips, and why a resolver sees it.**
`DynamicObjectTypeBuilder.BuildDeserializeInternalDeserializeLoopIntKey` emits the slot loop's `switch` default as
a direct `MessagePackReader.Skip()` call; the source generator's `FormatterTemplate` writes
`default: reader.Skip(); break;`. `MessagePackReader` is a struct, and neither path raises an event or reads an
option, so the skip itself cannot be observed without a fork. Both fetch every member formatter from
`options.Resolver` (the generated code's `formatterResolver = options.Resolver`), and the union formatters fetch arm
formatters the same way (U1). A resolver ahead of the composite is therefore handed every nested object, list
element and union arm, and can read each one's array header before delegating.
- **S2. The mark arrives in C#.** Probe `cs`: a guard resolver returns, for each concrete int-keyed
`[MessagePackObject]` type, a formatter that reads the array count (from `NextCode` for a fixarray, a peek for
array16 and array32), compares it with the highest `[Key]` plus one (base keys included, MessagePack's member
visibility rule), notes a skip into a `[ThreadStatic]` scope the decode's caller opened, and delegates to the
formatter the rest of the composite hands out. An extra slot at depth 0 (record), depth 1 (nested object), depth 2
(object in an object), in a union arm and in a list element each arrives as one mark naming the type and both
counts; the clean payload gives none. The guard needs `CompositeResolver.Create` around it so formatters are
cached (uncached, it was 40 times slower).
- **S3. C# cost.** Aetheria `GameData/Aetheria.cc` (12.1 MB; 237 of 238 records decodable with Aetheria.Shared's
types; decoded with `OptionsFor` that assembly; best of 9, three rounds): plain 165-209 ms, a second plain composite
(A/A) 174-193 ms, guarded 179-196 ms. That is 3.7-8.9% over the best plain run, inside the A/A spread. Worst case,
synthetic tiny objects (120,000 records, 23.1 MB, about 24 objects each): plain 618-637 ms with A/A within 4%,
guarded 704-705 ms: 11-19%, about 30 ns per decoded object.
- **S4. A live case in Aetheria.** The same probe, run with the AetherDb release binaries (built 2026-09-30) against
today's `Aetheria.cc`, marks 25 records: `WeaponItemData` whose `BehaviorData` arms (`AutoWeaponData`,
`ChargedWeaponData` and others) carry 34 slots where that build declares 21 or 33. The extra slot is
`WeaponData.Tracking` (key 33, Aetheria `8cd71ed6`, 2026-10-07): inside a union arm, inside a list. Today that tool
reads these records and drops `Tracking` from all 25 at its next landing (S5). Refusing nested extra slots would
stop it opening `Aetheria.cc` at all; read-only lets it open the store and refuses writes to those 25.
- **S5. A C# flush re-encodes every held record, not only the edited one.** `PushAll` and an unconditional
`CommitBatch` write `ToPersistedRecord(entry, SerializePayload)` for every entry (`CultCache.cs:3191-3201`, `3355`,
`3384`), under the local descriptor's schema id and catalog entry. So U6's loss reaches every held record with
unread slots at the next flush of anything. Rust (holds payload bytes and decodes on `get`, `lib.rs:2280-2293`),
Python (holds envelopes and decodes at load, `cache.py:257-285`) and TypeScript (holds payload bytes) carry an
untouched record's bytes. In C#, read-only therefore also means persisted from the original record (bytes, schema
id, catalog entry), never from the decoded document.
- **S6. Rust.** Probe `rs`: the derive's slot loop, with the trailing `IgnoredAny` loop counting into a
`thread_local`, marks depth 1, depth 2 and a list element. Depth 0 is the `DatabaseEntry` derive's own trailing
loop (`cultcache-rs-derive/src/lib.rs:258`), the same edit. A plain serde struct nested in a record already refuses
an extra element (`array had incorrect length, expected 2`), so only derived slot types skip. Cost (120,000
records, 21.4 MB): inside the A/A spread (A/A from -3% to +9%; marked against plain from -14% to +13% over six
runs). The added code is one branch per object, never taken on a clean payload. Rust decodes on `get`, so whether
a held record is writable is answered by decoding its held bytes through the same derive, on the write path only.
- **S7. Python and TypeScript.** Probes `py` and `ts`: a slot codec tree passing a context argument (the shape
`union-codec-py` and `union-codec-ts` build) marks all five positions. Python (40,000 records, 11.1 MB, msgpack
unpack included): +14% against an A/A spread of 8%. TypeScript (120,000 decoded records, codec walk only; no
msgpack in the scratch copy): +20% against an A/A spread of 18%. The check is one length comparison per object.
- **S8. Kotlin has no library slot codec.** `CultDocumentCodec<T>.decode` is written per type by the consumer
(`packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt:31-36`), and the package's own Eve
decoders skip extras by hand at 8 sites (`repeat((count - N).coerceAtLeast(0)) { reader.skip() }` in
`eve/EveDocuments.kt`). No slot rule, refusal or read-only, can be held by the library there until it owns slot
decoding. That is a follow-up, not a per-runtime rule.
- **S9. CultMesh decode paths** (`CultMesh.cs:2779,3493,3499`, `CultMeshPrimitives.cs:1158,1230,2357`) go through
`OptionsFor`, so through the guard, with no scope open: unchanged. Whether a Mesh replica that re-publishes a
decoded document drops slots is a follow-up.
- **S10. The refinement is in.** Every runtime with a library slot decoder (C#, Rust, Python, and the TypeScript
codec being built) carries the mark to the record on its one decode path, with no second pass and no consumer
code. The cost on Aetheria's real store is inside the A/A noise (S3); the worst synthetic case is 11-19% in C#.
A nested extra slot makes the record read-only; it is not refused.
## A. Aetheria (C#, the only consumer persisting `[Union]`s)
Aetheria consumes the reference through Unity packages pinned by tag (`Packages/manifest.json:55-57`:
`caching-unity-v1.4.0`, `cultlib-unity-v1.0.60`), so a CultLib change reaches it only when it bumps the pin.
Its only consumer resolver is `MathResolver` (`AssemblyInfo.cs:4`), which supplies no union formatter.
- **A1. Unions inside persisted documents** (Aetheria `aac2cf91`, `Assets/Scripts/ServerShared`):
`BehaviorData` (`Behaviors/Behaviors.cs:147-191`, keys 0-11, 15, 16, 18, 20-24, 28, 31-40; 12 and 26 retired,
14, 17, 19, 25, 27, 29, 30 commented out) in `List<BehaviorData> Behaviors` of the item data documents
(`ItemData.cs:346,368`); `WeaponData` (`Behaviors/Weapon.cs:15-18`); `EntityPack` (`EntitySerializer.cs:172-173`)
in `aetheria.savedzone` contents and its own `Children`; `ItemInstance` and `CraftedItemInstance`
(`ItemInstance.cs:18-31`, a union whose arms are also a union) in entity cargo and docking bays
(`EntitySerializer.cs:186-187`); `Provenance` (`Provenance.cs:96-98`) in `aetheria.provenanceledger`
(`Provenance.cs:11-15`, `Lot.Origin`); `SavedActionBarBinding` (`SavedGame.cs:221-223`) in `aetheria.savedgame`;
the input layout rows and keys (`InputLayout.cs:29-50`) in `aetheria.inputlayout`. `Brush`
(`Environment.cs:73`) was not traced to a persisted document.
- **A2. `GameData/Aetheria.cc` carries no unknown arm.** Walker `py/store_unions.py` over the git-tracked game
database (12.7 MB, `cultcache.store.v1`, 238 records, 13 catalog entries): `Behaviors` arm keys
{0:3, 1:2, 2:10, 3:5, 4:2, 5:2, 6:3, 8:7, 9:23, 11:2, 16:4, 23:7, 24:5, 31:1, 32:1, 34:1, 35:2, 37:4, 38:10, 39:2,
40:1}; none retired or commented out; no `nil` element.
- **A3. The run save carries no unknown arm, and does carry missing trailing slots.** Walker `py/run_unions.py`
over `GameData/run.cc` (2026-09-30): `EntityPack` arms 0 and 1 (3 each), `ItemInstance` arm 2 (12),
`Provenance` arm 0 (64), `SavedActionBarBinding` 9 `nil` (empty bar slots). The `EntityPack` arms carry 21 slots
(orbital) and 20 (ship) against today's 22: the `Boss` slot (key 21, added 2026-10-07 in `db4617d8`) is missing,
so today's Aetheria reads this save only because a missing trailing slot defaults. `py/gaps.py`: no non-nil
retired slot (ItemInstance 2, 9, 10; EntityPack 5). `GameData/stale-2026-09-22/run.cc` is the same shape;
`player.cc` holds only `aetheria.playersettings`. `py/slots.py`: every top-level record matches its own store's
catalog exactly.
- **A4. What changes for Aetheria.** After it adopts a CultLib with cut 1, its current stores load unchanged
(A2, A3). A store written later by a build that has added an arm is refused by a build that lacks it, naming the
union and key, where today the arm silently becomes `null` (a gear item quietly loses a behaviour, a lot loses
its provenance) and the next save writes the loss back. Builds already pinned to 1.0.60 keep the null path; a
shipped binary cannot change. Aetheria-legacy does not use CultCache and has no store.
- **A5. Other C# stores.** `%LOCALAPPDATA%Low/GameCult/Aetheria/aetheria-unity.cc` (2026-08-08, 1.2 KB) is a
`cultcache.store.v2.directory-indexed` header from an older layout, not a union carrier.
## C. Rust, TypeScript and other consumers
Eyes audit for this pass (session `self-2026-10-10c-eureka`), `git grep` at each repo's `origin/HEAD`, Rust reach
by a lexical script (it can over-count; it does not under-count named fields). Aetheria and CultLib's tests excluded.
- **C1. Rust: no `DatabaseEntry` record crosses runtimes as a union, and none uses the reference arm shape.**
Records that reach serde enums, each keeping its bytes because the derive is opt-in:
Huginn `epiphany.pipeline.resolution.v2` (`ResolutionOutcome`, external tag, `eureka-pipeline/src/lib.rs:746`,
live on Yggdrasil); Idunn `idunn.deployment_transaction` and `idunn.admitted_generation` (v4 and legacy forms;
internally tagged `#[serde(tag = ..., deny_unknown_fields)]` enums in `control_plane.rs`, `deployment_plan.rs`,
and two `#[serde(untagged)]` enums kept so older transactions decode, `drivers.rs:259,387`; live at
`/var/lib/gamecult/idunn/control.cc`); CodexConnector `gamecult.codex.replay_record.v1` (`CodexReplayState`,
`lib.rs:1017`); Epiphany's Atlas, model-request, persona and receipt records (internal, adjacent, external and
untagged representations; `.cc` files under `F:\Projects\.epiphany-runtime\` and on Yggdrasil). Unit-only serde
enums (a bare string) are widespread in Idunn, Huginn (`Faculty`, the pipeline's unit enums), CodexConnector and
Epiphany. No enum reaches any record in Odin, Muninn, Ghostlight, Huginn `mind-body`, or cultnet-rs and
cultmesh-rs production records. None of these is read by another runtime as a union today, so none is affected
by the arm refusal; the derive changes nothing for them.
- **C2. TypeScript: no consumer encodes a tagged union.** The only hand formatters are StreamPixels
`apps/service/src/realtime/overlay-state-documents.ts:190,232,277` (positional arrays, no union). Bifrost,
Heimdall, Hermodr, Odin scripts, Stonks, Vili, VoidBot (vendored `cultcache-ts`) and weksa have none.
- **C3. C# outside Aetheria: StreamPixels' overlay declares unions; its tracked store holds none of them.**
`apps/overlay-unity/Assets/Scripts/Assembly-CSharp/` (`8d40541`): `ActivationCondition` (10 arms),
`BattleEffect` (2), `StatusEffect` (4), `ItemInstance` (3), `EquippableItemInstance` (2), reachable from
`streampixels.status`, `streampixels.action_item`, `streampixels.skill` and gear documents. Walker
`py/sp_catalog.py` over `apps/overlay-unity/Data/catalog.cc` (`cultcache.store.v1`, 20 records): only
`outfit`, `skin_color`, `hair_style`, `emote` and `hair_color` records, and no `[int, array]` pair at any depth.
Its only consumer resolver is `MathResolver` (`Catalog/CatalogAssembly.cs:6`). CultLib's own
`GameCult.Networking` `Message` union (`Messages.cs:10`) is a CultNet wire message, decoded with
`MessageSerialization.Options` (`MessageSerialization.cs:7`, `DefaultOptions`), outside `OptionsFor`: the arm
guard does not reach it (follow-up).
- **C4. Python and Kotlin: no union-shaped record.** repixelizer uses `define_database_entry_type` with flat
slots; `cultmesh-kotlin` documents are `data class`es with no sealed type.
So the stores the arm refusal can affect today are Aetheria's (A2-A4, none carrying an unknown arm) and
StreamPixels' overlay catalog (C3, carrying no arm). Every other runtime's persisted enums are serde shapes that
the opt-in derive leaves alone.
## D. Payload depth bound (body at `c028c511`)
One bound, 256 nested MessagePack arrays and maps, on read and on write (ruling
`cultlib-gaps:ruling:payload-depth-limit-256`; prior art `docs/research/payload-depth-limit.md`). The C# reference
landed in `ead5260c`, Python in `5618d8dd`, Rust in `c028c511`. TypeScript is not landed (spec
`cultlib-gaps:cut_spec:cut-union-bounds-ts.r2`, blocked on question `cultlib-gaps:question:ts-payload-opaque-type`).
| Runtime | Owner of the decision (constant and walker) | Doors routed through it |
|---|---|---|
| C# | `CultPayloadDepth.Check` (`src/GameCult.Caching.MessagePack/CultPayloadDepth.cs`), limit `CultMessagePackSecurity.MaximumPayloadDepth`. MessagePack's own graph counter is set to the same value as a backstop only. | Write: `CultDocumentMessagePackSerialization.SerializeUntyped`. Read: `DeserializeUntyped`. The cache store (`SerializePayload`/`DeserializePayload`), the directory store, `CultMesh` and single-file documents all call those two; C# has no separate raw-envelope or prepared-batch door. |
| Rust | `cultcache_rs::check_payload_depth` and `MAX_PAYLOAD_DEPTH` (`packages/cultcache-rs/src/lib.rs`). | Read: `decode_payload` (get, list, `validate_envelope`/`put_envelope`, `finish_prepared_entry`). Write: `put`/`prepare_entry`/`prepare_entry_named` reach it through `finish_prepared_entry`, which decodes what it just encoded. Raw envelope: `put_raw_envelope`, the door CultMesh network puts use (`cultmesh-rs` `lib.rs`, two call sites). Prepared batch: `put_prepared_batch` via `require_payload_depth`. cultnet-rs: every free-form or skippable wire payload decodes through `cultcache_rs::decode_payload` (message decode in `contracts.rs`, `provider_session`, `media_stream_wire`, `replication`, `service_identity`). |
| Python | `_refuse_past_depth_limit` and `MAX_PAYLOAD_DEPTH` (`packages/cultcache-py/src/cultcache_py/documents.py`); a msgpack `StackError` on read maps to the same error. | `DocumentDefinition.encode_payload` (write) and `decode_payload` (read). `CultCache.put`, `put_envelope`, `put_envelopes` and load-time hydration go through them; there is no raw-envelope door. cultmesh-py round trips call the same pair. |
- **Doors deliberately not routed.** cultnet-rs `idunn_lifecycle_brake`, `provider_health_contracts` and
`runtime_authority_contracts` decode records whose positional array length is checked exactly before decoding. A
fixed shape has no undeclared slot to skip and no free-form content, so the derive's depth is its shape (commit
`84bb1e28`). The five media records have no length check and are held by the test instead. C# `CultCacheMessagePack.CreateCodec`
serialises member values and deserialises member types without the document guard, by design (codec comment); only
the document type goes through the guard.
- **Forbidden writers.** No second walker and no second constant: a runtime's decoders, stores and wire layers call
the owner above, never re-count depth or restate 256. An added payload door routes through the owner or names
itself in the exact-length list.
- **Shared path.** `tests/vectors/union-parity/` (`bounds-outcomes.msgpack`, `depth-*.msgpack`; table in its
`README.md`), written by the C# reference and held by `tests/GameCult.Caching.Tests/UnionParityVectorTests.cs` and
`PayloadDepthTests.cs`, `packages/cultcache-rs/tests/union_parity.rs`, `packages/cultcache-py/tests/test_union_parity.py`.
- **Cut line.** `packages/cultnet-rs/tests/payload_depth.rs` and the `service_identity`/`cultmesh-rs` tests pin the
Rust wire doors. cultnet-py and cultmesh-py message and replication decoders call `msgpack.unpackb` directly and
are outside the Python cut; they hold only msgpack's own stack limit.
- **Verification layer.** Vector held by each runtime's own suite (limit 256 accepted, 257 refused, skipped content
counted, non-recursive walk); Rust wire doors by the cultnet-rs test above.
## Rationale
- **Why a guard formatter, not a fork or a re-implementation.** U5 shows a guard that only checks the key and the
subtype and delegates the encoding is enough. It keeps MessagePack-CSharp's union encoding as the one encoder,
costs one small file, and covers every path that already goes through `OptionsFor`. Forking the generated
template or vendoring `DynamicUnionResolver` would put a second encoder in CultLib.
- **Why the Rust derive is opt-in.** Serde enums already persisted by Rust consumers keep their bytes
(`docs/persisted-record-encoding.md`); a changed default would make every such store unreadable or
non-canonical. A new derive with explicit arm keys is how a Rust type says "this crosses runtimes".
- **Why arms use newtype variants over slot structs.** A C# arm is a subclass whose slots include the base class's
keys. A Rust newtype variant over a struct with keyed slots is the same thing, and it needs the slot code the
`DatabaseEntry` derive already has, factored so both derives share one slot generator.
- **Why the slot rule is a separate, asked cut.** The ruling settles arms and asks for extra trailing slots to be
"settled against the same re-encode rule". Two written contracts disagree (U10), and the current behaviour drops
data silently (U6), so the choice is the operator's and it should change every runtime at once.
- **The recommendation on extra slots was refusal; the ruling chose read-only, and S settles its depth.** An extra trailing slot is the slot-level twin of an unknown
arm: data from a newer writer that the reader's types do not describe. The operator ruled refusal for arms
knowing it costs an older reader the newer record; the same reasoning gives refusal for slots, and record
envelope slots are already refused in every runtime (U10). The alternative with precedent is
`variants:ruling:undeclared-read-only` (a record a type can read but does not declare loads read-only): it would
let an older reader see a newer record without overwriting it, but only where a catalog shows the extra slot
(top level), while most evolution here is nested (A3's `Boss` slot sits inside a union arm).
- **Missing trailing slots stay tolerated.** The reader knows a missing slot and its default, so it carries no
bytes it does not understand, and Aetheria's live run save depends on it (A3). A store whose guard compares
re-encoded bytes (Idunn's) refuses such a record by its own stricter rule, which is that guard's purpose.

827
docs/cultlib-gaps-map.md Normal file
View file

@ -0,0 +1,827 @@
# CultLib Gaps: Map
Campaign `cultlib-gaps` (target: `docs/cultlib-gaps-target.md`). This file holds body facts, probes and
rationale for the cuts the mind's `cut_spec`s name; the specs cite it by section and fact number.
Pinned base: `origin/main` at `6bdc23c9` (2026-10-10). Imagination pass `imagination-cl-gaps-cultmath`, session
`self-2026-10-10-ag`. Probes live under the session scratchpad, `imagination-cl-gaps-cultmath/` (`probe1` traced
copy of the solver and the warm/cold sweep, `suite` a scratch copy of `packages/cultmath` with the candidate fix,
`probe2` the sweep against that copy, `probe3` the overload probe). None of it is committed.
## Cut order
1. `bls-release-stop`: BoundedLeastSquares stops on a release only when the release is not real.
2. `bls-input-field`: a validation helper names the invalid input (follow-up `bls-invalid-input-field`). Blocked on
question `cultlib-gaps:question:bls-input-error-shape`.
3. `cultmath-release-0-4-1`: release `org.gamecult.cultmath` with cuts 1 and 2. Version follows question
`cultlib-gaps:question:bls-fix-release-lane`.
4. `cultmath-rotation-algebra`: quaternion algebra, swing-twist and joint clamps (ruling
`aetheria-release:ruling:solver-in-gamecult-animation`). Ships in the CultMath release that the first
GameCult.Animation release pins; that release is mapped with GameCult.Animation.
5. `py-msgpack-default` revision 4 (section P, "Revision 4"): the fix batch (h4) on `hands/py-msgpack-default`
at `a59bbc7d`. `migrate_payloads` is replaced by `convert` (ruling `py-migration-shape-ruled`), and Soul s3's
open codec Lows are fixed. Its merge to main waits on the Brokkr cut (follow-up `brokkr-msgpack-payloads-r4`).
5a. Not mapped: the Python store takes the cross-process sidecar lock the C# and Rust stores take (follow-up
`py-ts-store-cross-process-lock`). It is a store parity cut of its own; `convert` does not need it (P15).
6. Not mapped: the C# leaf-vectors revision (`leaf-vectors-next`) gains the P2 map-key rows, so the Python
key tests read C#-written bytes instead of literals.
7. `decode-stack-safety` (section D): cultcache-rs's one decode door grows its stack at every container level
(ruling `decode-stack-mechanism-ruled`). Independent of 5 and 6; thing2's contract-rs merge waits on it.
8. `expect-persisted-schema` (section E): an observed entry keeps the schema id it was persisted under and
conditional commit compares that (ruling `aetheria-release:ruling:expect-fix-in-cultcache`). C# only. The
release that carries it to Aetheria follows question `cultlib-gaps:question:expect-fix-release-lane`; the
Aetheria pin bump is an Aetheria follow-on, not this cut.
9. `aot-record-ref-generator` (section A, AOT cut 1): a consumer with MessagePack's source generator on can declare
`CultRecordRef<T>` members and dictionary keys. Base: the C2a branch head `a768b821`; Hands starts after C2a merges
to main or rebases onto it. Serves `record-ref-breaks-msgpack-generator`, the first cut of
`cs-aot-safe-registration` (operator 2026-10-27: "I'm not a fan of requiring reflection. ... AoT is a nice thing to
support anyway.").
10. Not mapped (AOT cut 2): the generator reaches consumers that reference only `GameCult.Caching.MessagePack` (today its
analyzer is `PrivateAssets="all"`, so they run every document on Reflection.Emit).
11. Not mapped (AOT cut 3): registration descriptors are generated instead of reflected (`BuildDescriptor`,
`DiscoverMembers`, `CultDocumentResolver`, `CultMessagePackSecurity`), and C2a's element-id walk stops reflecting
(A6), so a NativeAOT consumer round-trips a ref-keyed dictionary.
## A. AOT cut 1: CultRecordRef under MessagePack's source generator (cut `aot-record-ref-generator`)
Imagination pass `imagination-aot-cut1`, session `self-2026-10-27-eureka`, pinned to `origin/hands/variants-c2a` at
`a768b821` (C2a's head; merge base with main `02cf04b5`). Starting evidence: `research/cultmesh-garden-path/cultcache-aot-probe-2026-10-27.md`
(main `eb6a8f2c`). Probes: the scratch console from that note, copied to the session scratchpad
`imagination-aot-cut1/aot-probe/` and committed only as dangling commits on top of `a768b821` (`a0f06b2c`, `6d382961`,
`dc6cf9dc`, `99cae039`, `07b89982`), run on Yggdrasil in `mcr.microsoft.com/dotnet/sdk:10.0` (MessagePack 3.1.7, .NET
10.0.12). Logs `job1.log` to `job5.log` beside the probe. Variants: G = `CultRecordRef<T>` member only, H = ref-keyed
`Dictionary<CultRecordRef<T>, float>` only, B = both. Every variant references `MessagePack` directly (generator on).
### Body facts
- A1. The generator does not discover `CultRecordRefFormatter<T>`. For a generic type it does not know, it collects
the type's definition as an object and names the formatter it would generate, `<Type>Formatter<T>`, nested in
`GeneratedMessagePackResolver`. For a type from another assembly it then generates nothing, so the resolver names
`GeneratedMessagePackResolver.GameCult.Caching.CultRecordRefFormatter<T>`, which does not exist (CS0426). The match
with our formatter's name is a coincidence. Evidence (job1): `[ExcludeFormatterFromSourceGeneratedResolver]` on the
formatter, and an `internal` formatter, both still fail G, H and B with the same line. Upstream issue
MessagePack-CSharp#2133 (open, no maintainer reply) reports the same failure for any open-generic type with a
formatter in another package.
- A2. Consumer MessagePack 3.1.12, the newest stable, fails G, H and B the same way (job1). Upgrading does not fix it.
- A3. `[MessagePackFormatter(typeof(CultRecordRefFormatter<>))]` on the `CultRecordRef<T>` struct stops the
generator naming a formatter for it (the mangled line is gone), but the generator then reports `MsgPack006` (error)
in every consumer: it checks `typeof(X<>)`'s interfaces, which an unbound generic does not list. Adding the
non-generic `IMessagePackFormatter` marker to the formatter does not help (job2 `ta2`). With `MsgPack006`
suppressed by the consumer, G, H and B build and run (job2). Rejected: it needs the formatter moved into
`GameCult.Caching`, which then needs the whole `MessagePack` package instead of `MessagePack.Annotations`, and every
generator consumer still has to suppress an error.
- A4. `[assembly: MessagePackAssumedFormattable(typeof(CultRecordRef<>))]` in the consumer assembly fixes G, H and B
with no library change (job2 `base@ASSUMED`). The generator stops collecting `CultRecordRef<T>`; the generated
document formatter asks the resolver, and `CultDocumentResolver` serves the ref. This is the attribute MessagePack
documents for "a formatter added through a custom resolver at runtime". The same attribute as an MSBuild
`AssemblyAttribute` item in the consumer project (`_Parameter1_IsLiteral`), which is what a package's
`buildTransitive` targets injects, does the same (job3 MG, MH, MB). The attribute declared in the library assembly
does not reach the consumer's generator (job4 LIBATTR: CS0426).
- A5. Generated and dynamic formatters write the same bytes. RefDoc (B) payload is `93A172A17081A170CA3F000000` from
the Reflection.Emit fallback with no generator (job3 BASE) and from the generated formatter (job3 MB). The document
formatter is `GeneratedMessagePackResolver+Probe+RefDocFormatter` (`dynamic=False`); the ref formatter is the
library's `CultRecordRefFormatter<PlainDoc>` through `CultDocumentResolver`.
- A6. NativeAOT (`PublishAot`, probe assembly rooted, linux-x64): G round-trips (payload, upsert, flush, reopen) with
payload `92A172A170`, identical to JIT (job4 G0). B fails, in C2a code, not in the generator path: upsert fails in
`CultElementIds.MapEntries`, whose `MakeGenericMethod(key, value)` has no native code for a struct key (job4 B0). With
that walk reading the map through non-generic `IDictionary` instead, upsert passes under AOT and JIT is unchanged
(job5 B1), but the payload and flush still throw `TargetInvocationException` from a second reflective call on the
serialize path. By source read the candidate is `ResolverClaim`'s `GetFormatterMethod.MakeGenericMethod(type)`
(`CultDocumentMessagePackSerialization.cs:84`), not proven. Both are runtime reflection over element-id paths, which is
AOT cut 3's ground, so cut 1 proves AOT on the member shape and leaves the key shape's AOT round trip to cut 3.
- A7. `GameCult.Geometry` already runs the generator (it references `MessagePack` directly) and carries the per-member
workaround on three members (`CultGeometryDocuments.cs:164,239,328`). Under A4 those attributes are redundant.
### Shape the cut lands, and why
`GameCult.Caching.MessagePack` ships `buildTransitive/GameCult.Caching.MessagePack.targets`, which adds the A4 assembly
attribute to every project that consumes the package. NuGet consumers need no line of their own, which is what a
consumer expects from a library whose type breaks the generator. Unity consumers and in-repo `ProjectReference`
consumers do not get `buildTransitive`: Unity consumers write the one attribute line (README), and in-repo consumers
import the targets file, so they exercise the file that ships. The attribute type comes from `MessagePack.Annotations`,
which `GameCult.Caching` already brings, so the injection compiles in a consumer with or without the generator. Wire
bytes do not change: no formatter changes, only which code path asks for it (A5).
Rejected: a type-level formatter attribute (A3); excluding or hiding the formatter (A1); upgrading MessagePack (A2);
declaring the attribute in the library (A4); keeping per-member attributes, which cannot reach dictionary keys.
## B. BoundedLeastSquares stopping
Finding `aetheria-release:finding:cut-thrust-allocator-core.s1.solver-converged-early` (Soul, Medium): a cold start
reports `Converged` at a non-optimal point, so Aetheria's thrust allocator (tolerance 1e-12) answers differently
warm and cold, by up to .078 throttle.
### Body facts
- **B1. One runtime ships the solver.** `BoundedLeastSquares` exists only in
`packages/cultmath/src/CultMath/BoundedLeastSquares.cs` (C#, netstandard2.1 and net8.0). The type doc says there is
no HLSL mirror; grep over `packages/*` finds no Rust, TypeScript, Python or Kotlin sibling, and no other CultLib
project calls it. Its consumer outside CultLib is Aetheria's `ThrustAllocator`.
- **B2. The defect reproduces against CultMath at base, outside Aetheria.** `probe1` rebuilds Soul's allocator
matrix (Soul's `ProbeAllocator2`, constants H=100, Wy=1000, W=.3, kappa=10, rho=.1, EMax=2) for hull
`Synthetic(8, 7005)` and the 8th intent of `System.Random(5)`, (-0.8505, 0.6739, 0.5587): 15 rows, 12 columns.
Real `BoundedLeastSquares.Solve` at tolerance 1e-12 from x = 0: `Converged`, 13 iterations, cost 36.0313737,
x7 = 1. Re-solving from that answer: 1 iteration, cost 36.0312608, x7 = 0.9222145, which is also the warm answer.
The same literals (dumped with `"R"` floats, `probe1 dump`) reproduce it at tolerance 1e-9 and 1e-12, and not at
the default 1e-6, where the cold solve stops at iteration 10 inside the tolerance and the re-solve takes 0
iterations.
- **B2a. The W5 problem as literals** (`probe1 dump`, float `"R"` round trip; m = 15, n = 12, row-major; the
regression fixture for `bls-release-stop`). Nonzero entries of A:
- row 0: a[0,2] = 100f, a[0,8] = -100f, a[0,9] = 100f
- row 1: 13.882518f, 20.86695f, 0, 14.8965845f, 12.38046f, 22.115599f, -13.706654f, 15.85789f, 0, 0, -100f, 100f
- row 2: -187.72859f, 239.31168f, 158.87828f, -270.31165f, 235.38467f, -14.704258f, 121.61232f, 244.81305f, 0, 0,
0, 0
- rows 3-6: a[3+k, 8+k] = 0.3f (k = 0..3); rows 7-14: a[7+i, i] = 0.1f (i = 0..7)
- b = {0, 67.385445f, 558.70917f, -3f, -3f, -3f, -3f, 0 x 8}; lo = 0; hi = 1f for columns 0-7, 2f for 8-11.
At base, tolerance 1e-12 from x = 0: `Converged`, 13 iterations, x[7] = 1, cost 36.031373704487095; a re-solve
from that answer: 1 iteration, x[7] = 0.92221445f, cost 36.031260771352414.
- **B3. The stopping rule that fires is the progress rule, after a release** (`BoundedLeastSquares.cs:273-278`). A
traced copy of the solver, bit-equal to the real one on this problem, shows the last two passes:
- iteration 12: full step lands; column 2 sits at its lower bound with gradient -0.00205 (violation 2.05e-3,
tolerance 1.5e-7); the solver releases it and records fMark = f.
- iteration 13: the re-solve moves x2 from 0 to 2.05e-7. The cost falls by about 2.1e-10, and the stall band
`1e-15 * fScale` is 2.11e-10, so `f >= fMark - band` holds and the solver reports `Converged`. Column 7 is then at
its upper bound with gradient +0.00145, ten thousand times the tolerance; releasing it is worth 1.1e-4 of cost.
- **B4. Why the progress rule cannot judge a release.** Near a minimiser, a cost difference resolves only about the
square root of what a gradient comparison resolves: a real step of size dx against gradient g lowers the cost by
about g*dx, while the cost itself is a sum of terms of size fScale. Here g*dx (2e-10) sat exactly at the cost's
rounding band. The gradient comparison is far from its own rounding: for column 7, the bound on the double
gradient's rounding error, `(n+1) * eps * (|A^T b|_7 + sum_k |(A^T A)_7k x_k|)` (Higham's inner-product bound,
gamma_(n+1)), is 9.3e-10, and the violation is 1.56e6 times that.
- **B5. What the progress rule is for** (commit 43035bc8, and the tests at `BoundedLeastSquaresTests.cs:774-942`).
It stops two kinds of pass that can never reach the tolerance: a free-set re-solve whose residual is rounding on a
near-singular free set, and a release whose violation is itself rounding (kktTol at or below the noise, including
kktTol = 0 when the box point nearest the origin is stationary). Deleting it outright breaks both: with a noise
gate on every test and no progress rule, 8 of 66 BLS tests fail (`wide`, `integer-tie`, `collinear`, `cond` spin to
the cap; `NearSingularFreeSetsConfirmOnceAndStop` sees 0 confirmations). Exempting releases from the progress rule
with no noise gate fails one: `StalledPassesConvergeInsteadOfSpinningToTheCap("plain")` seed 141, b = 0,
kktTol = 0, cycles release 3 (violation 1.7e-18) / stationarity miss / block on 3 until the cap.
- **B6. The candidate fix, measured.** In a scratch copy of the package (`suite`, diff `suite/clean.diff`, 44 lines):
(a) a bound column is a release candidate only if its violation exceeds both kktTol and the rounding bound of B4;
(b) the progress rule judges only passes that release nothing; a pass that releases sets fMark to +Inf, so a
release is never measured against a cost recorded before it. Results:
- all 66 tests in `BoundedLeastSquaresTests` pass, including `DefaultSolvesAreBitIdenticalToTheirGoldenBits` and
every stall and noise-floor test; the same holds with the bound's factor at 1, 16 and 256 in place of n+1;
- the W5 problem: cold `Converged` in 14 iterations at the warm answer (x7 = 0.92221, cost 36.03126077);
- 400 synthetic hulls x 60 random intents (24,000 calls, warm and cold) at tolerance 1e-12: every call `Converged`,
max iterations 45, warm and cold agree within 6e-8 on every call (the original: one call off by .078 in the first
2,400). At the default tolerance warm and cold still differ, by more than .1 in 10,273 of 24,000 calls: that is
the documented resolution of 1e-6 times the gradient scale, pinned by
`AnAllocatorShapedProblemIsStartIndependentAtATightTolerance` (spread >= 0.5), and not this cut's subject.
- **B7. Released columns the factor drops.** With the fix, 266 releases in the suite land on a column that
`SolveFreeSet` marks dependent (p = 0). The next pass is then a stationarity miss with no release, and the
progress rule stops it, as before. No release in the suite or the 24,000 allocator calls produced a step that moves
the released column outward. A Lawson-Hanson style rejection guard (re-bind the column, try the next candidate)
was prototyped and is not needed by any measured case; it is not in the cut.
### Prior art, and which one the solver follows
- The solver is a primal active-set method on the normal equations: solve the free set, step to the first bound
crossed, release the most violated bound only after a full step lands. That is the outer/inner loop of
Lawson-Hanson NNLS (1974) generalised to two-sided bounds, which is the shape of Stark-Parker BVLS (1995) and of
scipy's `lsq_linear(method='bvls')`. It differs in factorisation (Cholesky of `A^T A`, not QR of `A`) and in
stopping.
- Lawson-Hanson stop on the dual test only (netlib `NNLS`, label 350: `IF (WMAX .le. ZERO)`), and guard roundoff
per candidate: a column whose new norm is negligible against rounding (`DIFF(UNORM+...)`) or whose new
coefficient is not positive (`ZTEST .gt. ZERO`) is rejected with `W(J)=ZERO` and the next candidate tried. There is
no cost-based stop.
- scipy's BVLS (`scipy/optimize/_lsq/bvls.py`, main) measures KKT exactly as CultMath's status doc states
(`g * on_bound` for bound variables, `|g|` for free ones, max over components) and has two stops: optimality below
`tol` (status 1) and a relative cost change below `tol` (status 2), reported as different statuses.
- CultMath reported its cost stop as `Converged`, which its status doc defines as "the KKT conditions hold". The fix
follows Lawson-Hanson: a release is judged on the gradient against its rounding (the role of their `DIFF` test),
and the cost stop is kept only where no gradient test can decide (a free-set residual at rounding level), which is
where scipy's status 2 also lives.
### Rejected alternatives
- Remove the progress rule and stop on the gradient alone: B5, 8 failures.
- Report the cost stop as a new status (scipy's status 2): the W5 answer would still be wrong, just labelled, and a
new enum member is a breaking change for callers that switch on the status.
- Measure progress from the step (the exact model decrease `t(1-t/2)|L^-1 g|^2`) instead of differencing the cost:
exact, but a noise-level gradient still yields a tiny positive decrease, so it needs the same noise threshold, on a
quantity further from the KKT test than the gradient itself.
## I. Naming the invalid input
Follow-up `cultlib-gaps:follow_up:bls-invalid-input-field` (from Soul finding
`cut-bls-kkt-tolerance.s1.invalid-input-names-no-field`): every validation failure returns the same `InvalidInput`.
- **I1.** Validation today is three sites in `Solve`: tolerance (`:147-148`), bounds (`:149-151`), and finiteness of
A and b folded into the `A^T b` build (`:153-161`), which cannot tell A from b.
- **I2.** Precedent: LAPACK reports a bad argument as `INFO = -i`, naming the argument and never its value. .NET
throws `ArgumentException` with a parameter name, which the solver's no-throw, zero-allocation contract rules out.
- Recommended shape (question `bls-input-error-shape`, option `validate-helper`): a public
`BoundedLeastSquares.Validate(m, n, a, b, lo, hi, kktRelativeTolerance)` returning a new enum
`BoundedLeastSquaresInput { Valid, InvalidTolerance, InvalidBounds, NonFiniteMatrix, NonFiniteTarget }`, the first
failure in that order. `Solve` calls it and returns `InvalidInput` for anything but `Valid`, so validation has
one owner and the folded finiteness check in the `A^T b` loop is deleted. Additive: `InvalidInput` and every
existing result stay.
## R. Release 0.4.1
- **R1.** `org.gamecult.cultmath` is at 0.4.0 (`package.json:4`, `CultMath.csproj:13`, tag
`cultmath-unity-v0.4.0`). `scripts/check-changelog-semver.mjs:97-101`: in 0.y.z a minor bump is the breaking lane;
additions and fixes take the patch lane (0.2.4 added `erf` as a patch).
- **R2.** Cut 1 changes results only where the old solver stopped after a release whose violation was above both the
caller's tolerance and the gradient's rounding, that is, where it reported `Converged` in breach of its status doc.
The default path's golden bits are unchanged (B6). The 0.4.0 precedent (ruling `cultmath-0-4-0-breaking-r2`)
declared the snoise tie fix Breaking because two valid functions gave different values; here the old value broke
the documented contract. Question `bls-fix-release-lane` asks which lane; the recommendation is `fixed-patch`
(0.4.1, `### Fixed`).
- The release procedure is the 0.4.0 cut's (`cultlib-gaps:cut_spec:cut-cultmath-release.r1`): Windows-only DLL rebuild
on Starfire, tests on Yggdrasil, public-surface diff, Unity import smoke, Soul before the tag.
## Q. Rotation algebra
Ruling `aetheria-release:ruling:solver-in-gamecult-animation`: CultMath gains quaternion algebra, swing-twist and
joint clamps; the aim-chain solve lives in a new package GameCult.Animation. The interface the consumer needs is the
Aetheria map's section "The rig's interface to GameCult.Animation and CultMath" (Aetheria `origin/master`
`30d02719`, `docs/aetheria-release-map.md:7559-7601`).
### Body facts
- **Q1. What exists.** `quaternion` (`src/CultMath/quaternion.cs`, 95 lines): public float fields x, y, z, w;
`identity`; `LookRotation(float3 forward, float3 up)`; implicit conversions to and from `float4`; scalar `==`.
`math.normalize(quaternion)` (`math.cs:225-235`) returns identity below length 1e-20. No mul, rotate, conjugate,
inverse, axis-angle, slerp, swing-twist or clamp exists. `math` is a partial class (`math.cs`,
`math.Interval.cs`, `math.Affine.cs`, `math.Phacelle.cs`); `math.rotate(float2, float)` exists (`math.cs:240`).
- **Q2. Serialization.** `GameCult.Caching.MessagePack` serialises `quaternion` as the float array [x, y, z, w]
(`CultMathResolver.cs:170-171`) and as an xyzw object in JSON (`CultMathJson.cs:468`). No other runtime
(`cultcache-rs`, `-ts`, `-py`) has a quaternion type. The cut adds static functions only: no field, layout or
formatter changes, so no wire change and no cross-runtime fixture.
- **Q3. Runtimes.** The algebra is C#-only, like `LookRotation`: shaders carry rotations as `float4` (the type's doc),
the mirror tests map shader functions to C# and never the reverse (`HlslSourceCompatibilityTests.cs:114-117`), and
the native core (`native/cultmath-core`) is a Voronoi tone helper. The Unity bridge already converts `quaternion`
(`UnityConversions.cs:16,23`); it needs no change.
- **Q4. An overload trap, probed.** `quaternion` converts implicitly from `float4`, and CultMath has no
`mul(float4, float4)`. `probe3` declares `mul(quaternion, quaternion)` beside scalar and matrix `mul` overloads and
calls it with two `float4`: it compiles and returns the quaternion product. HLSL's `mul(vector, vector)` is the
inner product, and Unity.Mathematics 1.3.3 (the copy in Aetheria's PackageCache) defines `mul(float4 a, float4 b)`
as such (`matrix.gen.cs:129`), so the same call there is a dot product. The cut therefore adds HLSL's vector
`mul(float2|3|4, float2|3|4)` (dot) beside the quaternion `mul`, so a `float4` pair keeps its shader meaning.
- **Q5. Unity.Mathematics as reference, not template.** Its signatures for `mul`, `rotate`, `conjugate`, `inverse`,
`AxisAngle` and `slerp` (`quaternion.cs:99,493,502,630,651,678`) are the consumer's stated convention ("`mul(a, b)`
applies `b` first"). CultMath's design rule is to state semantics in code and not copy Unity.Mathematics
(`docs/design.md:465-469`), so the formulas are written from the definitions below.
### Semantics the cut lands
Quaternions are Hamilton, stored xyzw, unit for rotation; angles are radians, right-handed about the axis.
- `math.mul(quaternion a, quaternion b)`: the Hamilton product; `rotate(mul(a, b), v) == rotate(a, rotate(b, v))`.
- `math.rotate(quaternion q, float3 v)`: `t = 2 cross(q.xyz, v); v + q.w t + cross(q.xyz, t)`; q assumed unit.
- `math.conjugate(q)`: (-x, -y, -z, w). `math.inverse(q)`: `conjugate(q) / dot(q, q)`, no guard (zero in, non-finite
out, as HLSL division).
- `quaternion.AxisAngle(float3 unitAxis, float radians)`: (axis sin(r/2), cos(r/2)); the axis is not normalised.
- `math.slerp(a, b, t)`: shortest arc (negate b when `dot(a, b) < 0`); normalised lerp when the quaternions are
nearly parallel; t is not clamped.
- `math.swing_twist(q, unitTwistAxis, out swing, out twist)`: twist is q's projection onto the axis, normalised
(identity when that projection vanishes, a half-turn swing); `swing = mul(q, conjugate(twist))`, so
`q == mul(swing, twist)` (twist applied first).
- `math.clamp_twist(twist, unitAxis, min, max)`: angle `2 atan2(dot(twist.xyz, axis), twist.w)` wrapped to (-pi, pi];
inside [min, max] the input is returned unchanged, otherwise `AxisAngle(axis, clamp(angle, min, max))`.
- `math.clamp_hinge(q, unitAxis, min, max)`: `clamp_twist` of q's twist about the axis; the swing is discarded.
- `math.clamp_cone(swing, unitAxis, maxAngle)`: the angle between `rotate(swing, axis)` and `axis`; inside the cone
the input is returned unchanged, otherwise the rotation by `maxAngle` about `normalize(cross(axis,
rotate(swing, axis)))` (at a half turn, where that cross product vanishes, about the swing's own axis).
- Preconditions (`min <= max`, unit axes) are documented, not checked, as everywhere in `math`.
### What GameCult.Animation will need from this cut (not mapped here)
- From cut 4: `mul`, `rotate`, `AxisAngle` for `AimChain.Pose` and `EffectorDirection`; `conjugate` or `inverse` for
frame changes; `swing_twist` and the clamps if its joints ever carry cone or twist limits (the presenter's per-axis
boxes do not).
- From cut 1: start-independent `BoundedLeastSquares` at a tight tolerance. `AimChainSolver.Solve` is warm-started
from `current`; without cut 1 its answer would depend on the previous frame exactly as the allocator's did.
- A CultMath release that carries cut 4, which `org.gamecult.animation`'s `package.json` pins. That release is
mapped with GameCult.Animation, so one Soul-verified release pair ships both.
## P. cultcache-py payload format (py-msgpack-default, revisions 2 to 4)
Imagination pass `imagination-py-msgpack-r2`, session `self-2026-10-12-eureka`, against `hands/py-msgpack-default`
at `7f2c7477`. Probes ran from the session scratchpad and were removed afterwards; their results are recorded here (`jobK` ran
the C# reference on Yggdrasil at `7f2c7477`; `p1`-`p3` ran Python 3.13.15 with msgpack 1.2.2 on Starfire). Revision 1's facts
are in `docs/research/py-msgpack-default/README.md`, and Soul's are in verdict `cut-py-msgpack-default.s1`.
### Body facts
P1. **What decides a payload's format at 7f2c7477.** A search of every `packb`, `unpackb`, `json.loads` and
`json.dumps` call in cultcache-py, cultnet-py and cultmesh-py found five places:
- (a) `PayloadCodec.encode`/`decode`, through `DocumentDefinition.encode_payload`/`decode_payload`.
- (b) `_check_payload_tree` (documents.py:75), which holds the map-key rule. `MESSAGEPACK_PAYLOAD_CODEC.encode`
does not apply that rule, so a direct call writes `{1: 'a'}` and its own `decode` refuses the bytes.
- (c) `stores.py:217`, `_infer_schema_version_from_payload`. A v1 record whose schema id is missing from the
catalog is read by unpacking its payload as MessagePack to find a schema stamp.
- (d) `replication.py:186`, the same stamp reader copied into cultnet-py behind a lazy `import msgpack`.
- (e) `stores.py:257`. A legacy envelope whose payload is an inline map or array is repacked as MessagePack.
The other JSON sites are wire messages, schema JSON, CLI output and the cultmesh ready file. None of them is a
document payload.
P2. **The C# reference's object slot, map keys** (`jobK`, `NumericLeafVectorTests.AnySlot` under
`CultDocumentMessagePackSerialization.OptionsFor`, MessagePack `UntrustedData` security):
- Accepted: str, int at every width (negative fixint, u64 max, i64 min), float32, float64, bool, and timestamp
ext. The reference rewrites each one at its own width, for example `9181ff01` becomes `9181d0ffcc01`.
- Refused: nil (inner `ArgumentNullException`); bin, array and map keys (inner `TypeAccessException`, because
UntrustedData has no collision-resistant comparer for them).
- Refused as duplicates (inner `ArgumentException`): `{1:1,1:2}`, `{"a":1,"a":2}`, and `{01:1, cc01:2}`, which
is the same value written at two widths.
- Accepted as two keys: `{1:1, 1.0:2}` and `{1:1, true:2}`. The CLR types differ, so the keys are distinct.
- An `object` value of u64 max or i64 min is accepted and rewritten byte-identical.
P3. **msgpack-python with `strict_map_key=False`** (`p2`, `p3`). Every key kind decodes: nil becomes None, bin
becomes bytes, and a timestamp ext becomes `msgpack.Timestamp`. An array or map key raises TypeError
(unhashable) without a hook. Duplicates are silently last-wins: `{1:1,1:2}`, `{"a":1,"a":2}`, `{01, cc01}`,
`{1, 1.0}` and `{1, true}` each decode to a single key. An `object_pairs_hook` sees every key kind, lists and
dicts included. An exception it raises propagates with `__context__` None. On 100k small maps the hook costs
2.2x the decode time (0.087 s becomes 0.200 s). `packb` raises an untyped `OverflowError` for 2**64 and
-(2**63)-1.
P4. **Hash flooding with non-str keys** (`p1`). This is msgpack-python's reason for `strict_map_key=True`.
Within the 64-bit MessagePack integer range, at most 13 integers share one CPython hash. The full-hash collision
attack on int keys therefore needs unbounded integers, and MessagePack cannot carry them. Keys crafted to share
low hash bits (`k << 43`) took 0.44 s for 200k keys and 1.34 s for 400k. 200k str keys take 0.25 s. The cost is
mildly superlinear, not quadratic. The reference defends itself with UntrustedData's collision-resistant
comparers. Python's dict has no such comparer, and the measured bound makes one unnecessary.
P5. **Schema-stamp recovery.** The C# store deleted its stamp recovery in `a3cc3942` (2026-09-13), so a persisted
schema id that is absent from the embedded catalog is refused ("Persisted schema ... is not present in the
embedded catalog", `CultCache.cs:504-507`). The C# CultNet registry keeps payload-stamp routing for raw wire
records (`CultNetDocumentRegistry.ResolveDescriptorForRawDocument`, `TryResolveDescriptorByPayloadSchema`).
cultcache-py's store copy (P1c) is therefore a divergence from the reference. cultnet-py's copy (P1d) mirrors
the reference. The TS store (`single-file-messagepack-backing-store.ts:336`) also keeps the store copy.
P6. **Legacy envelope arrays.** The C# reference does not read them. TS (`normalizePayload`, line 490) and Python
both repack an inline payload with MessagePack `encode`. A legacy inline payload is a subtree of a MessagePack
document, so it is MessagePack by construction. No JSON-payload writer ever produced one: the 0.3 JSON default
wrote bytes, which land as bin.
P7. **Brokkr** (read-only, `f97d4dc`). The bridge imports cultcache-py from `F:/Projects/CultLib/packages`
(`blender_target.py:19`) and defines 8 types with `define_document_type` (630-637). Merging to that checkout's
main is therefore the switch itself: Soul s1 measured that the next `_open_node` pull refuses the whole Blender
store. The daemon writes JSON into the Blender store and the Unity store with `put_json_document`
(`main.rs:1424`, called at 790-1392 and in tests from 2710). It also has `put_messagepack_document` (1435) and
reads JSON first, then MessagePack (`decode_payload`, 1447). The daemon is Rust and cannot call cultcache-py.
### Authority, and why
- **Payload format.** The definition's `PayloadCodec` owns its bytes: the encoding, and which maps and integers
the format can carry. On read, `MESSAGEPACK_PAYLOAD_CODEC` refuses key kinds and duplicates through its
`object_pairs_hook`; that is the only place they are visible, because a dict cannot show a collapsed duplicate
afterwards. On write, its encode walks the tree first, so a direct `codec.encode` call cannot write bytes
its own `decode` refuses (Soul's single path).
- **What the definition owns.** `DocumentDefinition.encode_payload`/`decode_payload` own the cross-codec rules
(depth 256) and the translation of codec faults into the typed refusals, naming the type. `PayloadCodec.map_keys`
is deleted, and `_check_payload_tree` goes back to checking depth only.
- **The key rule is P2's.** It is not `strict_map_key`'s. Duplicates are refused as the reference refuses them.
- **Residual.** Keys Python holds as one but the reference holds as two (`1` and `1.0`, `1` and `true`) are
refused, because a dict cannot hold both. The refusal is typed (`duplicate_map_key`) and pinned so a change is
seen. Matching the reference here would need a decoded map type that is not a dict. No consumer is known to
write such maps.
- **Prior art for refusing duplicates** (from knowledge, not fetched this pass): CBOR's RFC 8949 section 5.6
expects decoders to reject duplicate keys; I-JSON (RFC 7493) requires the same of JSON; MessagePack-CSharp's
`UntrustedData` refuses duplicates and keys it cannot hash safely (P2). msgpack-python's `strict_map_key` exists
for hash flooding, which P4 bounds.
- **Integer range.** Beyond 64 bits is a format limit, so the MessagePack codec's write walk refuses it with the
code `integer_out_of_range`. The value is never named, and neither is a map key on its path, because in a
free-form tree a key is payload data. Entry slots keep their leaf refusals, which name the slot and the field.
A bigint was never wire-parity data: MessagePack has no integer wider than 64 bits (P3), and the 0.3 JSON
payloads that could hold one were unreadable by the reference (`py-default-payload-json`).
- **The stamp readers.** P1c is deleted and its record is refused as the reference refuses it (P5). P1d stays as
cultnet-py's raw-record router, mirroring C#. It reads a stamp and decides no format: the resolved definition's
codec decodes the payload or refuses it. It loses its lazy import.
- **Legacy envelopes.** P1e is demoted and stays (P6). It extracts the MessagePack bytes the inline subtree
already is. A definition whose codec is not MessagePack refuses the record like any other foreign record.
- **The default codec.** The `DocumentDefinition` field is its only author. `define_document_type`'s parameter
defaults to None and forwards only a codec it was given.
- **Refusal text.** The JSON remedy sentence is dropped from the read refusal: it was false for every entry
definition. Each refusal carries a `code` (`not_one_value`, `map_key_kind`, `duplicate_map_key`,
`integer_out_of_range`) and names the kind or the fault class. It is raised after the `except` block, so
`__context__` holds no decoder exception and no payload.
### Migration: what CultLib owns
Revision 1's README procedure (read under JSON, then put each value) was per record and not atomic. Soul s1
stranded a store with it. Revision 2 replaces the procedure with
`migrate_payloads(store, definitions, *, source_codec=JSON_PAYLOAD_CODEC) -> int` in cultcache-py:
1. It pulls every envelope from the store.
2. For each record whose type is among `definitions`, it decodes under that definition with `source_codec` and
encodes under the definition itself. That is the whole definition path, validation and every refusal included.
3. Any refusal stops the migration before the store is touched. The error names the type, the record key and
the refusal's code, never the value.
4. Otherwise one `push_all` writes the store whole. `SingleFileMessagePackBackingStore` and
`JsonLinesBackingStore` each do that as a temp file and a rename.
5. Records of other types keep their bytes and `stored_at`. The return value is the count rewritten.
It never sniffs, and it cannot leave a store half-migrated. Revision 2 also claimed that a second run is refused;
Soul s2 falsified that (P10), and a concurrent writer strands the store (P11). Revision 3 replaced this shape with a
backup-and-swap (P12), which Soul s3 broke three more ways. Revision 4 deletes the in-place migration: CultLib owns
only `convert`, which writes a new file and never writes its source (P14).
Rejected alternatives:
- A sniffing reader that accepts both formats: P1's one-digit payloads are valid in both formats, and r1's map
forbade readers that try one format and then another.
- A consumer-run loop over `put`: that is the procedure that stranded the store.
### Brokkr landing contract (follow-up `brokkr-msgpack-payloads-r4`)
The merge of `hands/py-msgpack-default` to main waits on three things:
1. Soul holds on the fix batch.
2. A Brokkr branch, Soul-held, is ready in its own campaign. On it, the daemon writes MessagePack
(`put_messagepack_document`) into both stores. The JSON-first read becomes a read of exactly one MessagePack
value with a trailing-bytes check, and the bridge's 8 definitions are unchanged. The Blender store is converted
by `cultcache_py.convert` over those 8 definitions into a new file beside it, because those payloads came from
cultcache-py's old default (P14). The daemon-written JSON receipts in the Unity store are the daemon's own, and
it rewrites them, also into a new file.
3. An operator window: close Blender and stop sync, Self merges CultLib and Brokkr together, build the daemon,
run both conversions, open each new file, swap it into place, then restart. The JSON originals are kept under
another name for as long as the operator wants them.
### Revision 3
Imagination pass `imagination-py-msgpack-r3`, session `self-2026-10-12-eureka`, against `hands/py-msgpack-default`
at `57f85acd`. Python probes ran on Starfire (Python 3.13, msgpack 1.2.2) over a `git archive` of the cultcache-py
and cultnet-py sources at that head, and were removed afterwards. The C# facts are Soul s2's (`jobB`, Yggdrasil),
not rerun here.
P8. **What still decides map keys at 57f85acd.** One search found three MessagePack readers that see document
payload maps. (a) The codec's `_unpack_tree` applies the reference rule through `_checked_map`. (b) The store's
whole-file `msgpack.unpackb(data, raw=False)` (`stores.py:98`) applies msgpack's `strict_map_key=True`. A legacy
envelope file whose inline payload is `{1: 'a'}` is refused with "int is not allowed for map key". Read through
`_unpack_tree` instead, the same file yields the payload `8101a161`, and a v1 snapshot's framing decodes
identically either way. (c) cultnet-py's stamp router (`replication.py:192`) applies `strict_map_key=True`. Read
through the codec, `['x.v1', {1: 'a'}]` and `{'schemaVersion': 'x.v1', 'v': {1: 'a'}}` both route to `x.v1` (C#
routes both, Soul s2 STAMPPROBE). A nil-keyed payload gives `None`, as it does today and in C#. Every other
`unpackb` in cultnet-py and cultmesh-py reads wire messages, cursor files or fixed-slot protocol records, whose
maps are frames rather than payload data. Their bound is follow-up `py-wire-decoders-unbounded`. Unmeasured: C#
reads a map stamp as `IReadOnlyDictionary<string, object>`, so a top-level non-str key may stop C# routing where
Python routes. The resolved definition decides the record either way.
P9. **The two depth counters.** `_check_writable` (documents.py:184) counts every node, scalars included, to
`_PACK_DEPTH_LIMIT = 511`. `_check_payload_tree` counts containers to 256. Commit `eef1cb15` moved the codec
from 256 to 511 so that `test_union_parity.py:650` could build its 257-deep read fixture with
`document.payload_codec.encode(past)`. The fixture needs bytes that are 257 deep, not a codec that writes them:
`msgpack.packb(past, use_bin_type=True)` and `json.dumps(past).encode()` build them, and both writers nest to 511
or further. One container walk, bounded at `MAX_PAYLOAD_DEPTH`, serves both the definition and the codec. It also
ends a cycle, because a cycle reaches 257.
P10. **Rerun.** No byte in a CultCache store records a payload format, and no wire change is wanted for one. A
migration that rewrites the live store in place therefore cannot tell its own output from its input when a
payload is valid in both formats. `b'5'` is the JSON 5 or the MessagePack 53, and Soul s2 saw 53 become 5. No
check on the payload content can separate the two, so the rerun guard has to come from outside the payload.
P11. **Concurrency in the store.** cultcache-py's file stores hold a per-object `threading.RLock` only, so every
`push` is a read-modify-write that loses another process's write. The C# store opens a `.lock` sidecar with
`FileShare.None` (`CultCache.cs:3489-3509`), and cultcache-rs takes an `fs2` lock on its sidecar (`lib.rs:853-859`).
cultcache-ts has none (a search of `packages/cultcache-ts/src` finds no lock). The migration race is one case of a
store that lacks the reference's lock.
P12. **Prior art and the shape chosen** (primary docs fetched 2026-10-12). SQLite `VACUUM INTO`: "the original
database file is unchanged", and the target "must not previously exist, or else it must be an empty file". LMDB
`mdb_env_copy`: the target directory "must already exist and be writable but must otherwise be empty". Realm is
the counter-example to Self's hypothesis. It migrates in place on open, and a rerun is safe because the file
records its schema version, which CultCache's payload format does not (P10). The chosen shape follows `VACUUM
INTO`, with the destination role taken by a mandatory backup:
`migrate_payloads(store, definitions, *, backup, source_codec=JSON_PAYLOAD_CODEC) -> int`
1. Copy the live store's bytes to `backup` with exclusive create (`open(backup, "xb")`). If the backup exists,
the call is refused before anything is read. Every rerun is refused this way, whatever the payloads hold.
2. Read the records from the backup, the frozen copy, and rewrite the listed ones. Any refusal of any exception
class raises `CultCacheError` naming the type, the record key and the code or class.
3. Compare the live bytes with the copied bytes. A difference means a writer ran during the migration, and the
call refuses and says to stop every writer.
4. Replace the live file in one `_replace_all` (temp file and rename).
5. On any failure after step 1, the backup is removed and the live store is unchanged. A count of zero also
removes it.
Probe (`probe2`, this shape over the 57f85acd sources). The first run over `b'53'`, `b'5'` and an unlisted type
gives 53 and 5, keeps the unlisted bytes, and keeps the JSON in the backup. A second identical call raises
`FileExistsError` and leaves the live bytes unchanged. A push between the read and the swap (the s2 interleave)
is refused, with the backup removed and the live store holding the writer's JSON record. A validate that raises
`AssertionError` is named by class and the backup is removed.
What remains and is stated: the window between the compare and the rename, closed by the lock in `5a`. The
precondition is to stop every writer, as it is for SQLite's offline tools. If someone deletes the backup and then
reruns the migration on its own output, P10 recurs. The README says to delete the backup only after the
migrated store has been opened, and never to rerun after that.
P13. **Float32 keys.** `Float32(0.1).value` is `0.1`, so a `Float32` key is not rounded until it is packed.
Decoded, it is the float32-rounded value. Writing `{1, F32(1.0)}`, `{True, F32(1.0)}`, `{1.5, F32(1.5)}` or
`{0, F32(-0.0)}` produces bytes that the codec's own decode refuses as `duplicate_map_key`. `{0.1, F32(0.1)}` and
two NaN keys read back as two keys. The rule that matches decode exactly compares each key's decoded image (a
`Float32` key's image is `struct.unpack('>f', struct.pack('>f', v))[0]`) and refuses a collision on write as
`duplicate_map_key`. The reference reads all four collision maps as two keys (Soul s2 KEYPROBE). Python cannot,
for the same reason it cannot read `{1, 1.0}`, which is the residual already recorded. Refusing on write keeps
Python inside what it can read back, and it is the write half of that residual.
### Revision 4
Imagination pass `imagination-py-msgpack-r4`, session `self-2026-10-14-eureka`, against `hands/py-msgpack-default`
at `a59bbc7d`, under ruling `py-migration-shape-ruled` (convert-to-new-file). Source reads only; nothing was run.
P14. **The shape ruled.** `convert(source, destination, definitions, *, source_codec=JSON_PAYLOAD_CODEC) -> int`
replaces `migrate_payloads`. It creates `destination` with exclusive create (`open(path, "xb")`) before it reads
anything, so an existing file (the source itself, an earlier output, anything) is refused and left alone. It reads
the source once through the source store's `pull_all`, rewrites the listed records in memory, writes the store
bytes into the file it created, and fsyncs it. Any failure after the create removes that file. It never opens the
source for writing. The destination is the same store kind as the source, and its bytes come from the same
per-kind encoder `_replace_all` uses, so the two writers cannot drift. The caller stops writers, converts, opens
the new file and swaps it in. This follows SQLite `VACUUM INTO` and LMDB `mdb_env_copy` (P12) without the backup
role that r3 added to them.
P15. **What the shape makes impossible, and what stays.** The library no longer writes a live store during a
format change. That leaves no code to fix for s2's `migrate-races-concurrent-writer` or s3's
`inflight-writer-strands-migration`, `rerun-guard-is-per-backup-path` and `stopped-migration-has-no-recovery`,
because the backup, the compare and the swap are deleted. The library does not lose a write made during
`convert`. The write lands in the source, and only the caller's swap can drop it. The README makes stopping
writers the caller's precondition, as SQLite's offline tools do. A killed run leaves the source intact and possibly
a partial destination; recovery is to delete the destination and run again. A partial JSON Lines destination can
parse as a smaller store, so the README says the destination is complete only once `convert` has returned.
Converting a store that is already MessagePack can still produce wrong values in the destination (P10: `b'5'`),
but the source is untouched. The cross-process store lock (follow-up `py-ts-store-cross-process-lock`) is still a
store parity gap, but nothing in this cut waits on it.
P16. **The codec decodes only what it writes.** At `a59bbc7d`, `MESSAGEPACK_PAYLOAD_CODEC.decode` is `_unpack_tree`
alone. It returns trees 257 or more deep, `ExtType` values, and timestamps outside .NET `DateTime` (Soul s3); the
definition or the reference refuses every one of them. Revision 4 also runs the write check over the decoded tree
(`_check_carried`, renamed from `_check_writable`). That gives one rule in both directions. The depth walk, the
integer range and the key kinds apply as before. No `ExtType` node is carried (code `value_kind`, kind `ext`).
A timestamp's seconds must lie within `-62135596800..253402300799` (code `timestamp_out_of_range`). That interval
is `DateTime.MinValue` to `DateTime.MaxValue`. It is derived from MessagePack-CSharp's timestamp reader, which adds
the seconds to the Unix epoch and then the nanoseconds as ticks. It was not measured at the seam here, and a
C#-written seam row belongs to follow-up `leaf-vectors-next`. The store's whole-file read keeps `_unpack_tree`
alone, because a store frame is not a payload. The `any` leaf keeps its own ext refusal, which is the slot's rule
for a value handed to it directly.
P17. **NaN keys.** The reference holds a map's keys in a `Dictionary` keyed by CLR type and value, where a NaN
equals a NaN of the same width. It therefore refuses two float64 NaN keys and accepts a float64 NaN beside a
float32 NaN (Soul s3). Python reads both widths as one `float` and cannot tell them apart. Revision 4 gives
`_key_image` one NaN image, used by `_checked_map` on read and by `_check_carried` on write. Any two NaN keys are
refused both ways as `duplicate_map_key`. On the mixed-width pair this refuses something the reference accepts,
the same kind of residual as `1` beside `1.0`. The other choice would read two float64 NaN keys that the
reference refuses.
Revision 5 records this as a deliberate exception to ruling `leaf-acceptance-follows-reference`, because Python
cannot tell the two widths apart (msgpack-python decodes both to one `float`) and no ruling lets the codec read a
pair the reference refuses. Soul s4 probed the C# behaviour: `Dictionary<object,object>{float.NaN, double.NaN}`
serialises as `82 ca ffc00000 01 cb fff8000000000000 02`, the reference reads it back as two keys, and cultcache-py
refuses it on read as `duplicate_map_key` (base `a59bbc7d` accepted that read). Every same-width NaN pair, with
equal or different payload bits and with +NaN beside -NaN, is refused by both runtimes. The exception lives where a
consumer reads it: the cultcache-py README's Wire Parity section, pinned by a test vector (the bytes above).
## D. cultcache-rs decode stack safety (cut `decode-stack-safety`)
Pinned base: `origin/main` at `ae6ef5a8` (2026-10-14). Imagination pass `imagination-decode-stack`, session
`self-2026-10-14-eureka`. Ruling `decode-stack-mechanism-ruled` (grow-stack); prior art
`docs/research/decode-stack-safety.md`. Probes live under the session scratchpad, `imagination-decode-stack/`:
`probe` decodes through a scratch copy of cultcache-rs at the base whose derive calls a hook per derived level
(off, observe the stack used between levels, or `stacker::maybe_grow`), against Eve's `thing-contract` at
`2467e1b`; `wasmprobe` is the wasm run. Yggdrasil rust image (rustc 1.95.0, rmp-serde 1.3.1, serde_stacker
0.1.14, stacker 0.1.25, psm 0.1.32), jobs `job1` to `job5`. None of it is committed.
Shapes: `box` is a thing-contract Box chain (3 containers a level, the bound admits 83), `repeat` a Repeat chain
(2 a level, admits 125), `fat` a 32-slot record of `[u64; 32]` with an `Option<Box<Self>>` slot (1 a level,
admits 255, so the leaf arrays sit at depth 256), `json` a one-slot record holding a `serde_json::Value` nested
255 arrays, `ignored` a one-slot record whose extra trailing slot nests 255 arrays (skipped as `IgnoredAny`).
### Body facts
D1. **Today's abort, reproduced.** `decode_payload` at the bound, first level that aborts (process abort, no
exit code, every time):
| Profile, thread | box (83) | repeat (125) | fat (255) | json, 64 KiB | ignored, 32 KiB |
|---|---|---|---|---|---|
| release, 1 MiB | 44 | 56 | 13 | 115 | 24 |
| release, 2 MiB | ok | 114 | 25 | | |
| debug, 1 MiB | 11 | 14 | 5 | 15 | 8 |
| debug, 2 MiB | 23 | 30 | 10 | | |
One level past the bound is refused typed (`PayloadTooDeep { depth: 257, limit: 256 }`) for every shape and
every variant, before any recursion.
D2. **Recursion points.** Per container level a typed decode passes through: the derive's generated
`Deserialize::deserialize` (`cultcache-rs-derive/src/lib.rs:294-330` for slots, `534-684` for unions), which
calls `deserialize_tuple`; rmp-serde's `Deserializer::any_inner` (`decode.rs:530`), which calls
`visitor.visit_seq`/`visit_map`/`visit_newtype_struct` under `depth_count!` (566, 583, 616); the derive's
`visit_seq`, whose frame holds one `Option<T>` per slot; and `next_element`, which re-enters either a derived
type, serde's own impls (`Vec`, `Option`, `Box`) or another crate's (`serde_json::Value`, `rmpv::Value`).
rmp-serde 1.3.1 has no skip of its own: `IgnoredAny` goes through `any_inner` and a visitor like any value.
`check_payload_depth` is iterative.
D3. **Where the grow must sit.** A `maybe_grow` in the derive's `deserialize` covers derived levels only: `json`
on a 64 KiB debug thread still aborts at 15 with a 32 KiB red zone, and passes only when the first derived
level happens to hand it a whole new segment. A `serde_stacker::Deserializer` over rmp-serde at the door checks
at every `deserialize_*` call it forwards (`maybe_grow` at `de.rs:476, 489, 502, 511, 520`), so
rmp-serde's recursion, serde's impls, foreign `Value` types and `IgnoredAny` skips all re-enter through a check:
every shape passes in both profiles (D4). Consumers decode non-derived recursive types through the door today:
cultnet-rs `replication.rs:525` (`serde_json::Value`) and `contracts.rs:607` (`rmpv::Value`).
D4. **The frame between checks, measured.** Stack used from one derived `deserialize` to the next nested one,
at the bound: release box 13,168 B, repeat 12,192 B, fat 84,944 B; debug box 52,064 B, repeat 49,760 B, fat
207,504 B. The door wrapper checks more often, so its gap is smaller: release, every shape passes at a red zone
of 32 KiB on 128 KiB, 1 MiB and 2 MiB threads; debug, box and repeat pass at 32 KiB, fat fails at 128 KiB and
passes at 144 KiB through 256 KiB (1 MiB thread). So the worst gap constructed is between 128 and 144 KiB,
the debug `visit_seq` of the 32-slot record. serde_stacker's defaults (red zone 64 KiB, segment 2 MiB,
`param.rs:10-11`) fail debug fat. The cut sets 256 KiB and 2 MiB: 1.8x the worst measured gap. A type whose
single `visit_seq` frame exceeds the red zone can still overflow; that is documented, not prevented.
D5. **Same values, same errors.** Through the wrapper, box 83, repeat 125 and a 1,000-node surface decode equal
to the plain decode. The wrapper forwards `is_human_readable` (`de.rs:313`) and returns rmp-serde's own error
type, so `opaque_decode_error` and the derive's refusal texts are unchanged.
D6. **Cost.** No growth (release, repeat 125 on an 8 MiB thread): 322-339 us through the wrapper against 320 us
plain. A shallow 27 KB, 1,000-node surface on 1 MiB: 1.57 ms plain, 1.61-1.80 ms wrapped (run-to-run noise is
about that wide). When the decode does grow, it pays for faulting in fresh segment pages: repeat 125 on a 1 MiB
thread 1.5-1.7 ms; fat 255 on 1 MiB 25 ms against 9.3 ms on a 64 MiB thread. A 1 MiB and a 2 MiB segment cost
the same.
D7. **Windows x86_64: source read, not run.** stacker 0.1.25 does not use psm on Windows: `backends/windows.rs`
grows by fibers (`ConvertThreadToFiber`, `CreateFiber`, `SwitchToFiber`, `SetThreadStackGuarantee`), reads the
limit with `VirtualQuery`, and links a C helper built by `cc`. psm's README marks its own Windows stack switching
disabled; that is the other layer, so the research's conflict (C) is two layers, not a contradiction. Yggdrasil
has no Windows: the rust image carries `x86_64-pc-windows-gnu` (cross-compile, no run). Whether it actually grows
on Starfire is question `decode-stack-windows-check`.
D8. **wasm32: stacker is not a no-op.** psm's `build.rs:53` ships a precompiled `wasm32.o` and sets
`switchable_stack`; stacker then allocates segments from the global allocator with no guard page
(`alloc_stack_restore_guard.rs`). `remaining_stack()` is `None` on wasm, so the first check always grows and
the limit is known from then on. Under wasmtime 48.0.5 (wasm32-wasip1, release), a nested 9-slot record: plain
decodes 48 levels and traps at 64 ("out of bounds memory access" at `0xffffe510`: the stack-first shadow stack
wrapped below zero); wrapped decodes 64, 255, 400 and 600 levels (linear memory 1,088 KiB before, 8,896 KiB
after 255) and traps at 1,000 with "call stack exhausted", the engine's own stack, which stacker cannot grow.
`wasm32-unknown-unknown` builds. This corrects the ruling's premise that wasm keeps the abort: the shadow stack
grows and the engine stack is the host's setting, so the documentation says that instead.
D9. **Dependency.** `serde_stacker` 0.1.14 (dtolnay), pulling `stacker` 0.1.25 and `psm` 0.1.32 (rust-lang),
all `MIT OR Apache-2.0`; plus `cfg-if`, `libc`, `cc` (build) and `windows-sys` on Windows. psm assembles and
stacker compiles C at build time, so a C toolchain becomes a build requirement (gcc in the rust image, MSVC on
Starfire). cultcache-rs is MIT. Written by hand, the wrapper would be serde_stacker's ~650 lines of forwarding.
### Body facts, revision 2 (verdict `cut-decode-stack-safety.s1`)
Imagination pass `imagination-decode-stack-r2`, session `self-2026-10-14-eureka`, on branch
`hands/decode-stack-safety` at `2505c0c4`. Probes live under the session scratchpad,
`imagination-decode-stack-r2/` (`gen.py`, `gen2.py`, `driver*.rs.in`, jobs `jobP1` to `jobP4`), run on Yggdrasil
in the rust image (rustc 1.95.0). None of it is committed. `jobP1` patches a copy of serde_stacker 0.1.14 so
each of its five `maybe_grow` calls (`de.rs:476-520`, the wrapper visitor's container entries) records the
stack pointer, and reads the door's red zone and segment from the environment. Shapes: `wide` is the committed
`Wide32` (32 slots of `[u64; 32]`, 8 KiB inline); `bigN` is 32 slots of an N-array record (32·N KiB inline:
32, 64, 128, 256 KiB), each with an `Option<Box<Self>>` slot; `lim` is 31 slots of a 2-array record (15,880 B).
A caller is one `#[inline(never)]` function holding the `Result<T>`.
D10. **The gap between checks is linear in the record's inline size.** Stack used between two consecutive
checks, the worst over a 6-level decode on a 1 GiB thread (every gap in a shape is the same value):
| Shape (inline) | wide (8 KiB) | big4 (32) | big8 (64) | big16 (128) | big32 (256) |
|---|---|---|---|---|---|
| debug gap | 115 KiB | 451 | 899 | 1,795 | 3,587 |
| debug frames before the first check | 131 KiB | 515 | 1,027 | 2,051 | 4,099 |
| release gap | 48 KiB | 192 | 384 | 768 | 1,536 |
| release frames before the first check | 48 KiB | 192 | 384 | 768 | 1,536 |
So the gap is 14·S + 3 KiB in debug and 6·S in release, for S = `size_of` of the record entered: the frames
between a container check and the next one each hold the record being built or a `Result` of it (rmp-serde's
`any_inner`, the wrapper visitor, the derive's `visit_seq` with one `Option` per slot, the seed). D4's 128-144
KiB debug gap for `fat` is this line at 8 KiB plus a margin. The stack touched below the deepest check is 6 KiB
in debug and 0 in release, so the excursions between checks are the gaps above.
D11. **Why the thread size decided, in two ways.** (a) Before the first check the decode has already used the
second row of D10: `rmp-serde`'s `deserialize_tuple`, `any_inner` and the wrapper's frames sit between
`decode_payload` and the wrapper's first `maybe_grow`, and each holds `Result<T>`. A thread smaller than that
row aborts whatever the red zone: `wide` aborts on every thread up to 144 KiB in debug. (b) Mid-decode, a check
with between R and the gap remaining lets the next gap overrun; after the first grow the positions in each
segment are fixed, so which check first grows (set by the thread size) decides, which is the non-monotonic
pattern Soul measured. Thread sweeps at the bound (threads from 64 KiB in steps of a 64th of the span, plus
1, 1.5, 2, 3, 4 and 64 MiB):
| Profile, shape | R = 256 KiB | R = gap - 48 KiB | R = gap rounded up + 64 KiB |
|---|---|---|---|
| debug wide | 6 of 65 sweep threads abort (64-144 KiB); none at 1-64 MiB | all 65; and at 1-4 MiB | 6 (64-144 KiB); none at 1-64 MiB |
| debug big4 | 39 of 65; and at 1.5, 2, 3, 4, 64 MiB | 29; and at 2, 64 MiB | 24, the lowest from 64 KiB; none at 1-64 MiB |
| release wide | none | 14, spread from 112 to 864 KiB; and at 1, 4 MiB | none |
| release big4 | 9 (64-192 KiB) | 18, including 448-480 KiB | 9 (64-192 KiB) |
| release big8 | 40; and at 1.5, 2, 3, 64 MiB | 20; and at 1.5 MiB | 17, the lowest from 64 KiB; none at 1-64 MiB |
With R at least the gap, the abort counts match the threads smaller than the frames before the first check
(D10's second row plus the harness) in every shape and profile, big16 and big32 included (segments 4·R); with
R below the gap, threads well above those frames abort mid-decode.
D12. **Checking at the top of the door** (`jobP4`). `decode_payload` patched so its whole body runs inside
`stacker::maybe_grow(R, segment, ..)` before the walk and the wrapper, R = 512 KiB, segment 2 MiB. Every
thread from 24 KiB to 1 MiB in 8 KiB steps, then 1.5, 2, 3, 4, 8 and 64 MiB:
| Profile | wide (8 KiB), 1 level | wide, 255 levels | lim (15,880 B), 1 level | lim, 254 levels |
|---|---|---|---|---|
| debug: smallest thread that decodes | 40 KiB | 40 KiB | 72 KiB | 72 KiB |
| release: smallest thread that decodes | 24 KiB | 24 KiB | 40 KiB | 40 KiB |
No thread at or above the smallest one aborts, in any of the four columns, in either profile. lim's gap is
220 KiB in debug and 93 KiB in release (D10's lines give 220 and 93), so R = 512 KiB is 2.3 times the debug
gap of a record at 16 KiB. What a thread needs is now the caller's own frames, which hold the returned value
and do not depend on the payload's depth.
D13. **No red zone or placement removes the bound.** Every frame that holds a value of type T sits between the
check before T's container and the check inside it, and serde's own impls add frames that hold T before T's
first check: `Box<T>`'s `Deserialize` holds the `Result<T>` it boxes, and the `Option` check before it sized
nothing larger than a pointer. A per-type red zone (a forked wrapper sized by `size_of::<V::Value>()`, or a
grow in the derive sized by `size_of::<Self>()`) still leaves that frame of about one to three T outside any
sized check. So the stack a decode needs between checks is bounded only by bounding the inline size of the
records it builds. The derive is the one place that sees every record type it produces: a `const` assertion on
`size_of::<Self>()` refuses an oversized record at compile time. A field of a type the derive did not produce
(a fixed array of arrays, a large tuple, a plain serde struct) behind a `Box` or `Vec` is not reached by it.
D14. **Build closure and wasm, measured by Soul (`soul-decode-stack-s1` jobD, `cargo tree -e normal,build`
at `2505c0c4` against `ae6ef5a8`).** Added on `x86_64-unknown-linux-gnu`: serde_stacker 0.1.14, stacker 0.1.25,
psm 0.1.32, cc 1.8.0, find-msvc-tools 0.1.14, shlex 2.0.1, object 0.39.1 (`Apache-2.0 OR MIT`), all
`MIT OR Apache-2.0` except ar_archive_writer 0.5.3 (`Apache-2.0 WITH LLVM-exception`, build-only, via psm).
On `x86_64-pc-windows-msvc` the same plus libc 0.2.190 and windows-sys 0.61.2 (`MIT OR Apache-2.0`, beside the
crate's own windows-sys 0.59). `fs2 0.4` (8 call sites in `lib.rs`, the store's file locks) has no wasi
backend, so cultcache-rs does not build for `wasm32-wasip1`; D8 measured serde_stacker in a separate crate.
### Authority, and why
The door owns stack safety as it owns the bound: `decode_payload` runs the bound walk, then rmp-serde through
`serde_stacker::Deserializer` with two private constants. The ruling's "every container level the derive and the
door descend through" is met there, because every derived level is entered through the wrapper (D3); a
`maybe_grow` in the derive would be a second owner of the same check, needs a red zone of 128 KiB in release
and 256 KiB in debug for fat where the wrapper needs 32 and 144 KiB (D4), and still misses foreign recursive types. The derive is unchanged.
Revision 2 (D10-D13): the door's first act is a check, so no frame holding T precedes it, and the red zone is
set against the gap of the largest record the derive may produce. That size is a public constant the derive
asserts at compile time, because nothing at decode time can bound a frame that holds a T before T's own check
(D13). The thread's size then decides nothing beyond the caller's own frames, which hold the returned value
whatever the payload's depth (D12). Envelope and store decodes enter through the same grown decoder, without
the payload walk, so what they accept is unchanged. rmp-serde's 1024 counter stays a backstop that never decides. Callers that decode a payload with
`rmp_serde::from_slice` directly bypass the door, the bound and the grow alike; cultnet-rs re-decodes
door-checked `rmpv::Value`s that way (follow-up `rs-redecode-outside-door-r2`).
Rejected inside the ruling: the derive placement (above), and writing the wrapper by hand (D9: established
tooling, one owner upstream).
## E. Expect under compatible schema drift (cut `expect-persisted-schema`)
Pinned base: `origin/main` at `b549d731` (2026-10-21). Imagination pass `imagination-expect`, session
`self-2026-10-19-ag7`. Probe `probe1` (`ProbeExpectDrift.cs`, written into the clone by the job command, never
committed) ran on Yggdrasil, dotnet sdk 10.0, against `b549d731`. Fixture: a record written under
`DriftWide` (`tests.variant_drift.v1`) and read by a cache whose registry holds only `DriftNarrow` (v2) loads
as CompatibleDrift.
### Body facts
- **E1. The defect.** `CultCommitRequest.ConditionsHold` (`CultCache.cs:1677-1696`) compares the durable
record's `SchemaId` with the observed entry's `Descriptor.SchemaId` (`Expect` captures it at `:1772`;
`ExpectUnchanged` reads it at `:1694`). `ToStoredDocument` (`:3212-3268`) resolves a persisted record to the
local descriptor and drops `record.SchemaId`. So the observed side holds the local id and the durable side
the persisted id. Probe: unchanged drifted record, `Expect` + `Upsert` commits **False** on the single-file
store (P1) and on the directory store (P7). Same code at Aetheria's pin `45c2f40` (`CultCache.cs:1147-1207`).
- **E2. What must still refuse, at base.** Another cache commits a changed value (new minted `storedAt`):
Expect False (P4). Another cache reading under v3 (`DriftReused`) flushes the file, rewriting the record under
v3 with the **same** `storedAt`: Expect False (P5).
- **E3. A whole-file write rewrites every record under its local schema and keeps `storedAt`.**
`ToPersistedRecord` (`:3194-3204`) writes `entry.Descriptor.SchemaId` with `entry.StoredAt`, and both the
single-file `PushAll` (`:3361-3375`) and an unconditional or dirty `CommitBatchCore` (`:3393-3399`) write the
whole view. So the contract sentence "every write to a key mints a later storedAt"
(`Contracts/cultcache-store-composition.md:229-231`) is false for a flush: the schema id is the half of the
identity that sees it. The directory store writes only staged keys, each freshly stamped
(`DirectoryMessagePackBackingStore.cs:240-293`), so it never normalises a clean record.
- **E4. Lifetime at base.** After the cache's own `PushAll`, Expect commits True (P2): disk and descriptor now
agree. After *another* cache's normalising flush to the observer's own local schema, Expect commits True (P3);
after a re-pull, True (P6).
- **E5. Re-reads.** Both pull filters skip a record whose `storedAt` and local descriptor id are unchanged
(`CultCache.cs:3323-3327`, `DirectoryMessagePackBackingStore.cs:110-114`). A record normalised P to L by
another writer keeps `storedAt` and the local id, so with the persisted id on the entry the filter must
compare the persisted id, or a re-read never refreshes it and Expect stays a Mismatch forever.
- **E6. Other runtimes.** cultcache-rs `SingleFileMessagePackBackingStore::compare_exchange` and the snapshot
form (`packages/cultcache-rs/src/lib.rs:1352-1393`, `:1427-1450`) compare whole raw envelopes as read from
disk (`schema_id: Some(record.1)`, `:3189`); the expectation *is* the persisted envelope, and the crate has
no compatible-drift resolution, so the split cannot occur. Unchanged. cultcache-ts and cultcache-py have no
conditional commit (`cultcache-persistence-format.md:67-69`; grep finds none); nor do the cultnet and
cultmesh packages or cultmesh-kotlin. Not added here.
- **E7. Release path.** Aetheria pins `org.gamecult.cultlib#cultlib-unity-v1.0.60` and
`org.gamecult.caching.unity#caching-unity-v1.4.0`, both locked at `45c2f400` (2026-09-17). The CultCache
runtime ships as DLLs in `unity/org.gamecult.cultlib` (built by `scripts/build-unity-package.ps1`). Main is
1363 commits past the pin with a `### Breaking` section under `[Unreleased]`
(`unity/org.gamecult.cultlib/CHANGELOG.md`). `release/cultlib-unity-1.0.x` is the pin plus one commit
(1.0.61, Win64 QUIC plugins).
### Authority, and why
The backing store that read or wrote a record's durable bytes owns the schema id it is persisted under. It is set
when the store builds the entry from a persisted record (`ToStoredDocument`), defaults to the descriptor's id
for an entry this cache stamps (it is about to be written under that id), and is reset to the descriptor's id,
under the hold, after a whole-file write the store completed. The commit check, `Expect` and the pull filters
only read it. `Descriptor.SchemaId` stays what the entry decodes as; it is no longer the record's durable identity.
Consequence, recorded rather than ruled: after another writer's whole-file flush rewrites a drifted record
under a schema id this cache did not read it under, an `Expect` captured before is a Mismatch until the cache
re-reads (E4's P3 flips). That is the ruling's "compares that persisted id" applied to a rewrite that kept
`storedAt`; it is fail-closed, the standard retry (re-read, retry) recovers, and it is what refuses E2's P5.
## Observed, not owned by these cuts
- At the default tolerance the solver's answer on allocator-shaped problems depends on the start by up to the whole
box (B6). That is documented and pinned (`AnAllocatorShapedProblemIsStartIndependentAtATightTolerance`); a
consumer that needs one answer passes a tight tolerance, as Aetheria does.

View file

@ -0,0 +1,32 @@
# CultLib Gaps: Target
Date: 2026-10-09. Campaign `cultlib-gaps` in the Eureka mind.
## Why
Consumers of CultLib keep finding capabilities a reasonable consumer of a general
library would expect and that CultLib lacks. Doctrine says a gap is filled in its
owner, never worked around in the consumer. This campaign is the standing home for
those gaps, so each one lands in CultLib as a small cut instead of a local helper in
whichever project hit it.
Known gaps at opening (each a follow-up in the mind, owned by the consumer campaign
that found it):
- CultMath `BoundedLeastSquares.Solve` has a fixed KKT tolerance (1e-6); Aetheria's
thrust allocator needs a tighter one (`aetheria-release:follow_up:cultmath-bls-kkt-tolerance`).
- `cultcache-rs` has no decimal type, its snapshot reads follow symlinks, and its
single-file store stages files with the process umask and no mode option
(`eureka-body:follow_up:cultcache-rs-decimal-and-nofollow`).
## Invariants
- `owner-fills-gap`: a capability a consumer needs from CultLib is added to CultLib, with the consumer adopting it; no consumer-local copy survives the cut that adds it.
- `defaults-unchanged`: a new option defaults to today's behaviour; existing callers and stored bytes do not change.
- `wire-parity`: a CultCache wire change keeps byte parity across runtimes against the C# reference, with a cross-runtime fixture.
- `semver-measured`: a public API change bumps the package version by measured semver and ships as a tagged release consumers can pin.
## Not in scope
- Growing CultCache into a general database (memory `cultcache-scope-restraint`).
- Any consumer's adoption code beyond the pin bump; that stays in the consumer's campaign.

View file

@ -0,0 +1,201 @@
{
"campaign": "cultmath-tapes",
"cut": "glsl-lowering",
"revision": 2,
"title": "GLSL lowering r2: Phacelle as its own MPL-2.0 file so CultMath.glsl is MIT, notices corrected, iv_exp in the fixture, fixture regenerated, glslang and dxc run",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-glsl-lowering",
"base": "3ac7250a",
"depends_on": [
"interval-ops"
],
"first": [
"Follow-up on cut report cultmath-tapes:cut_report:cut-glsl-lowering.h1 (range c9bd003b..5ae207e8, no verdict yet; worktree F:\\Projects\\CultLib-glsl). Rulings in force: operator-split-phacelle, operator-compiler-downloads, self-exp-measure-in-site. Map: docs/cultmath-interval-ground-cut.md, 'Pass 3'. First commit: MERGE 3ac7250a (interval-ops r3's head, which Soul s2 found holds for everything this cut lowers) into hands/cultmath-glsl-lowering. Do not rebase: the branch is pushed, and the git rules forbid force-pushing a shared branch. Probed by Imagination in a scratch clone: the merge conflicts only in packages/cultmath/README.md. Resolve it by keeping r3's Intervals bullet (iv_frustum_ball, its rounding bound, the envelope), then r1's GLSL bullet, so the GLSL bullet sits at :48-53. design.md and HlslSourceCompatibilityTests.cs merge clean.",
"Second commit: regeneration alone (CULTMATH_WRITE_GLSL=1). Probed on the merge: CultMath.glsl gains 15 lines (cultmath_iv_frustum_ball with the r3 widening); the fixture does not change until this revision adds its families; GlslMirrorTests 4/4 and HlslSourceCompatibilityTests green (16 tests). Soul's range is 3ac7250a..HEAD, which carries r1's four commits through the merge, so the whole lowering gets its first verdict. Runs in parallel with interval-ops r4: r4 changes no src, HLSL or README, only NoiseBoundTests.cs and design.md's saving section (:475-512 at 3ac7250a, :533-570 after this merge), disjoint from this cut's :136-193. Do not merge r4's head into this branch.",
"The split (operator-split-phacelle). Transformation step 1 becomes: each #include is inlined in place, except an include whose file is under its own licence, which is lowered on its own into its own output file. The list of such includes is one table in GlslLowering, SeparateFiles = { \"CultMath.Phacelle.hlsl\" -> \"CultMath.Phacelle.glsl\" }, with the reason beside the entry (file-level MPL-2.0). That is an enumerated rule of the lowering, documented as step 1 in design.md; it is not a per-function special case, and nothing else is keyed by name. Lower(root) returns the two texts; Lower(string) stays for the inlined body if that is simpler, but there is one resolver (ReadShaderSource, generalised to accept the include predicate), never a second.",
"shaders/CultMath.glsl: the provenance line says it is generated from CultMath.hlsl and CultMath.Interval.hlsl, MIT, and that cultmath_phacelle is in CultMath.Phacelle.glsl, to be concatenated after this file by consumers that call it. The dropped include line leaves nothing behind. shaders/CultMath.Phacelle.glsl: its own provenance line (generated from CultMath.Phacelle.hlsl, MPL-2.0, requires CultMath.glsl before it), the MPL-2.0 header and attribution exactly as the HLSL carries them, the CultPhasor struct and cultmath_phacelle, guard CULTMATH_PHACELLE_GLSL. Both files pinned by CommittedGlslEqualsLowering; CULTMATH_WRITE_GLSL=1 writes both.",
"Pin tests over both files: NoHlslOnlyTokensSurvive runs on each; EveryPublicFunctionIsLowered takes the union of both files' definitions against the HLSL (minus texture samplers) and additionally asserts cultmath_phacelle and CultPhasor appear only in the Phacelle file; a new PhacelleFileCompilesOnlyAfterTheLibrary is not needed, the compile check below proves it.",
"The fixture (self-exp-measure-in-site). The site measures WebGL2 exp against the fixture, so iv_exp joins it; its C# bits come from the platform exp and differ by OS, so its entry carries tolerance \"ulp-bounded, platform exp\" and names the OS that generated it. GoldenFixtureMatchesCSharp parses the committed JSON (System.Text.Json, BCL) and compares family by family: text-equal for every family except those whose tolerance names the platform, which compare case by case within 1 ulp and print the maximum observed. One fixture, one writer, one tolerance field already in the format; no second fixture. The phacelle family stays in the fixture; the site's parity harness concatenates both GLSL files for that run (the site ships only CultMath.glsl), say so in design.md.",
"Notices: packages/cultmath/THIRD-PARTY-NOTICES.md :35-43 lists the MPL-2.0 files as math.Phacelle.cs, CultPhasor.cs, CultMath.Phacelle.hlsl with its Unity copy, and shaders/CultMath.Phacelle.glsl; CultMath.glsl is MIT and says so in its header. The package licence expressions (MIT AND MPL-2.0) do not change: the NuGet still ships the HLSL include. README :36-37 and :48-53 (after the merge) name both GLSL files and when a consumer needs the second. design.md :136-192: step 1 rewritten, the second file, the wrapper runs, the fixture's platform tolerance. Unity ships neither GLSL file.",
"Compile checks on Starfire (operator-compiler-downloads), one job each, detached with a log, polled in the foreground: tools/get-glslang.ps1 (16.6.0, the pinned SHA already in the script; if the published digest differs, stop and report, do not edit the pin to match); tools/compile-glsl.ps1 gains a -Phacelle switch and runs twice: the MIT-only wrapper over CultMath.glsl calling every function in it, and the full wrapper over both files calling every function including cultmath_phacelle. Negative controls, both must fail: -Body 'float x = lerp(0.0, 1.0, 0.5);' and the MIT-only wrapper with -Body calling cultmath_phacelle (proves Phacelle is not in CultMath.glsl). Then tools/get-dxc.ps1 and dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0. The browser-pane WebGL2 probe of r1 is repeated on the two-file wrapper.",
"Carried from r1 and still out of scope, name them in the report: the fixture's ulp bounds on WebGL2 are measured by the site cut; Stryker is not re-run unless packages/cultmath/src changes in 3ac7250a..HEAD (it should not; the lowering lives in tests; interval-ops r3's Stryker evidence, Soul s2, stands). Deviations r1 reported that stand: the anchored token regex; the iv_exp absence is now resolved by this revision; the HLSL edits (damp splat, pcg3d order) stay."
],
"deletes": [
{
"path": "packages/cultmath/shaders/CultMath.glsl",
"lines": 100,
"note": "the inlined Phacelle block (:510-612 at 5ae207e8: MPL header, CultPhasor, cultmath_phacelle) leaves this file; it is regenerated, not edited."
},
{
"path": "packages/cultmath/THIRD-PARTY-NOTICES.md",
"lines": 3,
"note": ":39-41 the sentence making CultMath.glsl MPL-2.0 as a whole."
}
],
"keeps_moves": [
"Transformation steps 2-9 and their order: unchanged; the cast scanner, saturate lowering, the guard and provenance mechanism stay.",
"Both tracked CultMath.hlsl copies stay one blob; no HLSL edit is expected in this revision (a dialect gap found by the two-file compile is fixed in the HLSL in the common subset, bit-identical, listed in the report).",
"HlslSourceCompatibilityTests' mirror and FindCultMathRoot: the resolver generalises in place; no second include resolver.",
"tests/CultMath.Tests/fixtures/glsl-parity.json: the same file, regenerated; families keep their order so the diff shows only the two additions."
],
"adds": [
"packages/cultmath/shaders/CultMath.Phacelle.glsl: generated, MPL-2.0, guard CULTMATH_PHACELLE_GLSL.",
"GlslLowering: the SeparateFiles table and the two-text Lower; GlslMirrorTests: the second file in CommittedGlslEqualsLowering, NoHlslOnlyTokensSurvive and EveryPublicFunctionIsLowered; GoldenFixtureMatchesCSharp's per-family comparison with the platform tolerance.",
"fixture families: cultmath_iv_frustum_ball (exact) and cultmath_iv_exp (ulp-bounded, platform exp).",
"tools/compile-glsl.ps1: -Phacelle switch (appends CultMath.Phacelle.glsl after the library and includes cultmath_phacelle in the calls).",
"design.md and README: the two files, the concatenation order, the fixture tolerance."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/GlslLowering.cs",
"line": 24
},
"change": "Lower takes the root (or the resolver's two texts) and returns both outputs; SeparateFiles table with the MPL reason; ProvenanceLine per file."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs",
"line": 320
},
"change": "ReadShaderSource accepts a predicate (or returns the separated include's text beside the inlined body); the C# mirror keeps inlining everything."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs",
"line": 30
},
"change": "CommittedGlslEqualsLowering over both files; :45 NoHlslOnlyTokensSurvive over both; :54 EveryPublicFunctionIsLowered union plus the phacelle-only-in-its-file assertion; :64 GoldenFixtureMatchesCSharp per family with the platform tolerance."
},
{
"location": {
"path": "packages/cultmath/tools/compile-glsl.ps1",
"line": 1
},
"change": "-Phacelle switch; the library text is CultMath.glsl, plus CultMath.Phacelle.glsl when set; the call list covers whichever functions are present."
},
{
"location": {
"path": "packages/cultmath/THIRD-PARTY-NOTICES.md",
"line": 35
},
"change": "MPL-2.0 file list: the two C# files, the HLSL include and its Unity copy, shaders/CultMath.Phacelle.glsl. CultMath.glsl is MIT."
},
{
"location": {
"path": "packages/cultmath/README.md",
"line": 48
},
"change": "both GLSL files; a consumer concatenates CultMath.Phacelle.glsl after CultMath.glsl only when it calls cultmath_phacelle."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 150
},
"change": "step 1 rewritten for the separated include; :182-192 the two wrapper runs and the fixture's platform tolerance; the site's parity run concatenates both files."
}
],
"authority_map": {
"owner": "GlslLowering owns the HLSL -> GLSL transformation and now emits two derived files, one per licence; both are pinned by GlslMirrorTests and never edited. The fixture stays owned by GoldenFixtureMatchesCSharp with one tolerance field per family. THIRD-PARTY-NOTICES.md states which files are MPL-2.0 and is the one place that does.",
"inputs": [
"shaders/CultMath.hlsl, CultMath.Interval.hlsl and CultMath.Phacelle.hlsl text at the merged head (3ac7250a merged into the branch)",
"C# math for the fixture values, including the platform exp for iv_exp"
],
"outputs": [
"shaders/CultMath.glsl (MIT) and shaders/CultMath.Phacelle.glsl (MPL-2.0)",
"tests/CultMath.Tests/fixtures/glsl-parity.json with cultmath_iv_frustum_ball and cultmath_iv_exp families"
],
"derived_state": [
"both GLSL files are derived; a hand edit to either fails CommittedGlslEqualsLowering",
"the fixture is derived from C#; the iv_exp family is derived from the generating platform's exp and compared within 1 ulp, every other family bit for bit"
],
"forbidden_writers": [
"no per-function special case in the lowering; the only name-keyed rule is the SeparateFiles table, one entry, licence-justified",
"no second include resolver, no PowerShell or JS copy of the transformation",
"no GLSL in the Unity package; no precision statement in either library file",
"no edit to the pinned glslang or dxc hashes to make a download pass"
],
"shared_paths": [
"the same dotnet test run pins the C# mirror, both GLSL files and the fixture",
"compile-glsl.ps1 is the one wrapper writer for the MIT-only run, the two-file run and both negative controls"
],
"deletion_line": "The Phacelle block leaves CultMath.glsl by regeneration and the MPL sentence leaves the notices before the new file is added. Net: one more generated file and one switch; what it buys is an MIT CultMath.glsl the site can vendor without carrying MPL text, and the compile checks r1 could not run."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green on the merged branch; paste counts; GlslMirrorTests 4/4 with the per-family fixture line printed",
"Starfire: tools/get-glslang.ps1 installs 16.6.0 with the pinned SHA matching; tools/compile-glsl.ps1 exits 0 on the MIT-only wrapper and on -Phacelle; both negative controls fail with the error quoted",
"Starfire: tools/get-dxc.ps1 then dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0; FXC cs_5_0 still green",
"Browser pane WebGL2: the two-file wrapper compiles with an empty log"
],
"tests": [
{
"name": "GlslMirrorTests.CommittedGlslEqualsLowering",
"pins": "CultMath.glsl and CultMath.Phacelle.glsl are exactly the lowering of the committed HLSL; the regeneration path is the same code."
},
{
"name": "GlslMirrorTests.EveryPublicFunctionIsLowered",
"pins": "the union of both files equals the HLSL's cultmath_* set minus texture samplers; cultmath_phacelle and CultPhasor live only in the Phacelle file."
},
{
"name": "GlslMirrorTests.NoHlslOnlyTokensSurvive",
"pins": "no HLSL-only token in either file."
},
{
"name": "GlslMirrorTests.GoldenFixtureMatchesCSharp",
"pins": "every family is C#'s current output bit for bit, except iv_exp within 1 ulp of the platform exp; iv_frustum_ball is present and exact."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "no HLSL edit in this range changed a bit."
}
],
"negative": [
{
"pattern": "Mozilla|MPL|Johansen|CultPhasor|cultmath_phacelle\\(",
"scope": "packages/cultmath/shaders/CultMath.glsl: 0 matches; the header's file-name pointer to CultMath.Phacelle.glsl is the only mention of Phacelle."
},
{
"pattern": "^CultPhasor cultmath_phacelle\\(",
"scope": "packages/cultmath/shaders/CultMath.Phacelle.glsl: exactly 1 match."
},
{
"pattern": "CultMath\\.glsl|Phacelle\\.glsl",
"scope": "packages/cultmath/unity/**: 0 matches."
},
{
"pattern": "\\b(float|u?int|bool)[234]\\b|\\blerp\\(|\\bfrac\\(|\\bsaturate\\(|\\brsqrt\\(|\\basuint\\(|\\basfloat\\(|static const|#include|precision (high|medium|low)p",
"scope": "both GLSL files: 0 matches (the anchored regex r1 settled on)."
},
{
"pattern": "CultMath\\.glsl",
"scope": "packages/cultmath/THIRD-PARTY-NOTICES.md: 0 matches in the MPL-2.0 file list; CultMath.Phacelle.glsl appears there once."
},
{
"pattern": "git diff 3ac7250a..HEAD -- packages/cultmath/src",
"scope": "empty; the lowering and the fixture live in tests, so interval-ops r3's Stryker evidence (verdict s2) stands for this range."
}
],
"operator": []
},
"estimate": {
"lines_added": 320,
"lines_removed": 130,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [
"GLSL ES 3.00 library file (generated, MPL-2.0): shaders/CultMath.Phacelle.glsl"
],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-split-phacelle",
"cultmath-tapes:ruling:operator-compiler-downloads",
"cultmath-tapes:ruling:self-exp-measure-in-site",
"cultmath-tapes:ruling:operator-campaign-home"
],
"questions": []
}

View file

@ -0,0 +1,141 @@
{
"campaign": "cultmath-tapes",
"cut": "glsl-lowering",
"revision": 1,
"title": "GLSL lowering: shaders/CultMath.glsl is generated from CultMath.hlsl by one enumerated textual transformation, pinned by a test, compiled by glslang, with a committed golden fixture for WebGL2 parity",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-glsl-lowering",
"base": "c49c76ee",
"depends_on": [],
"first": [
"Map: docs/cultmath-interval-ground-cut.md, sections 'Where the interval math lives' (route C) and 'The authority map'. Model: the HLSL -> C# mirror in tests/CultMath.Tests/HlslSourceCompatibilityTests.cs (wrapper at :348) and its complete transformation list in packages/cultmath/docs/design.md:92-134. The GLSL lowering follows the same discipline: a complete, enumerated list; anything outside it fails the test rather than being patched around.",
"Read how HlslSourceCompatibilityTests resolves #include \"CultMath.Phacelle.hlsl\" (CultMath.hlsl:506); the GLSL lowering inlines the included file at that line, because GLSL has no #include.",
"Target dialect: GLSL ES 3.00 (#version 300 es), WebGL2. No implicit int/uint -> float conversion; step(vec4, float) and max(vec4, float) differ (max(genType, float) exists, step(genType, float) does not); C-style casts do not exist; lerp/frac/saturate/rsqrt/asuint are mix/fract/clamp(x,0.0,1.0)/inversesqrt/floatBitsToUint; floatN/intN are vecN/ivecN; static const is const. Mixed-signedness shifts are allowed.",
"Where the lowered text does not compile, fix the HLSL into the common subset with a legal, bit-identical HLSL edit (e.g. CultMath.hlsl:114 and :200 step(h, 0.0) -> step(h, float4(0.0, 0.0, 0.0, 0.0)); :471-473 and :502 (float)((uint)hash.x >> 8) before the float multiply). The HLSL -> C# mirror test and the FXC/dxc compiles keep those edits honest. Never special-case the lowering for one function.",
"Casts need a balanced-paren scanner, not a regex: (int)((state >> 28) + 4) at CultMath.hlsl:396 is the case that breaks a regex.",
"Dropped, as the C# mirror drops them: include guards (replaced by CULTMATH_GLSL guards), and the Texture2D/SamplerState functions at CultMath.hlsl:535-573. The GLSL declares no precision; the host shader does (precision highp float; precision highp int; the pcg hashes need 32-bit int).",
"Regeneration: the pin test writes shaders/CultMath.glsl when the environment variable CULTMATH_WRITE_GLSL=1 is set, then asserts equality. No new project, no PowerShell copy of the transformation.",
"Golden fixture: tests export 256 seeded points per function family (snoise float3/float2, snoise_grad, fbm_grad 4 octaves, ridged_grad, cellular, pcg3d/pcg4d, iv_* if present on main by then) with outputs as float32 bit patterns in hex, to tests/CultMath.Tests/fixtures/glsl-parity.json. JSON is tolerated here: the consumer is a browser-pane WebGL2 readback, a xenos boundary. The site cut evaluates it.",
"glslang: tools/get-glslang.ps1 fetches a pinned glslangValidator release the way tools/get-dxc.ps1 does; tools/compile-glsl.ps1 compiles a #version 300 es fragment wrapper that includes shaders/CultMath.glsl and calls every public function once. Run on Starfire, one job, detached with a log, polled in the foreground.",
"Verification on Yggdrasil: dotnet test from packages/cultmath/tests/CultMath.Tests; Stryker --since:c49c76ee from a plain clone (not a linked worktree: Stryker resolves --since paths against the main checkout). Never run verification as a background shell task."
],
"deletes": [],
"keeps_moves": [
"Both tracked CultMath.hlsl copies (packages/cultmath/shaders and unity/org.gamecult.cultmath/Shaders) stay one blob; the common-subset edits land in both.",
"The C# mirror test, the FXC probe and dxc -T lib_6_3 -HV 2021 all still pass on the edited HLSL; the Unity package does not ship the GLSL file.",
"design.md's HLSL -> C# transformation list (92-134) is unchanged; the GLSL list is a new sibling section."
],
"adds": [
"tests/CultMath.Tests/GlslLowering.cs: the HLSL -> GLSL ES 3.00 transformation, one static function string -> string, with the list as named steps in order: inline includes; drop guards and texture functions; type names; static const; casts via the balanced scanner; intrinsic renames (lerp, frac, saturate, rsqrt, asuint/asfloat); float literal passthrough; guards CULTMATH_GLSL added; a provenance header line naming the source file and that the file is generated.",
"tests/CultMath.Tests/GlslMirrorTests.cs: CommittedGlslEqualsLowering (writes when CULTMATH_WRITE_GLSL=1, prints a unified diff on failure); NoHlslOnlyTokensSurvive (regex over the output for float[234]\\(|int[234]\\(|lerp\\(|frac\\(|saturate\\(|static const|\\((float|int|uint)\\)|Texture2D|SamplerState -> 0 matches); EveryPublicFunctionIsLowered (the set of cultmath_* function names in the HLSL minus the texture ones equals the set in the GLSL); GoldenFixtureMatchesCSharp (regenerates the fixture in memory from C# and asserts equality with the committed file).",
"shaders/CultMath.glsl: the committed generated artifact.",
"tests/CultMath.Tests/fixtures/glsl-parity.json: the golden fixture (seed, points, per-function expected bit patterns, tolerance class per function: exact for integer-hash paths, ulp-bounded for transcendental).",
"tools/get-glslang.ps1 and tools/compile-glsl.ps1, mirroring get-dxc.ps1 / compile-hlsl-spirv.ps1 in shape.",
"packages/cultmath/docs/design.md: a section 'GLSL Target: Source Transformations' after :134 with the complete list, the common-subset rule for HLSL, the regeneration command, and that the GLSL is a derived file never edited by hand.",
"packages/cultmath/README.md: one paragraph that CultMath.glsl exists, what dialect it is, and how a WebGL consumer includes it (string concatenation before its own #version-led source; declare highp)."
],
"file_changes": [
{
"location": { "path": "packages/cultmath/shaders/CultMath.hlsl", "line": 114 },
"change": "step(h, 0.0) -> step(h, float4(0.0, 0.0, 0.0, 0.0)); same at :200. Bit-identical in HLSL and C#."
},
{
"location": { "path": "packages/cultmath/shaders/CultMath.hlsl", "line": 470 },
"change": "jitter components and the id at :502: an explicit (float) cast around the shifted uint before the float multiply, so the lowered text has no uint*float."
},
{
"location": { "path": "packages/cultmath/shaders/CultMath.Phacelle.hlsl", "line": 1 },
"change": "whatever common-subset edits the glslang compile demands; each one bit-identical, each one listed in the cut report."
},
{
"location": { "path": "packages/cultmath/docs/design.md", "line": 134 },
"change": "new section: GLSL Target: Source Transformations."
}
],
"authority_map": {
"owner": "GlslLowering (tests/CultMath.Tests) owns the HLSL -> GLSL transformation; shaders/CultMath.glsl is its derived output, pinned by GlslMirrorTests. CultMath.hlsl stays the one hand-written shader source.",
"inputs": [
"shaders/CultMath.hlsl and shaders/CultMath.Phacelle.hlsl text",
"C# math for the golden fixture values"
],
"outputs": [
"shaders/CultMath.glsl (GLSL ES 3.00 library, every public cultmath_* function except texture samplers)",
"tests/CultMath.Tests/fixtures/glsl-parity.json"
],
"derived_state": [
"CultMath.glsl is derived; a hand edit fails CommittedGlslEqualsLowering",
"the fixture is derived from C#; a stale fixture fails GoldenFixtureMatchesCSharp"
],
"forbidden_writers": [
"no per-function special case in the lowering; a dialect gap is fixed in the HLSL in the common subset",
"no second transformation implementation (no PowerShell or JS copy of the rules)",
"no GLSL in the Unity package; no precision statement in the library file"
],
"shared_paths": [
"both tracked CultMath.hlsl copies receive the same edits; the existing one-blob rule covers them",
"the same dotnet test run pins the C# mirror, the GLSL pin and the fixture"
],
"deletion_line": "Nothing is deleted in CultLib. The liability bought is one generated file, one test file, one fixture and two tool scripts; what it retires is every future hand copy of CultMath noise in a GLSL consumer, starting with the site."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green; paste counts",
"Yggdrasil: dotnet stryker --since:c49c76ee from a plain clone; triage every survivor by name",
"Starfire: tools/compile-glsl.ps1 exits 0 on the wrapper over shaders/CultMath.glsl; a negative-control wrapper with one lerp( call must fail (records that the compile check sees errors)",
"Starfire: FXC cs_5_0 and dxc -T lib_6_3 -HV 2021 still compile both HLSL copies after the common-subset edits"
],
"tests": [
{
"name": "GlslMirrorTests.CommittedGlslEqualsLowering",
"pins": "the committed GLSL is exactly the lowering of the committed HLSL; the regeneration path is the same code."
},
{
"name": "GlslMirrorTests.NoHlslOnlyTokensSurvive",
"pins": "the list is complete: no HLSL-only token survives in the output."
},
{
"name": "GlslMirrorTests.EveryPublicFunctionIsLowered",
"pins": "no function is silently dropped except the texture samplers."
},
{
"name": "GlslMirrorTests.GoldenFixtureMatchesCSharp",
"pins": "the fixture the site will read on WebGL2 is C#'s current truth."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "the common-subset edits changed no bit of any HLSL function."
}
],
"negative": [
{
"pattern": "lerp\\(|frac\\(|saturate\\(|float[234]\\(|int[234]\\(|static const|\\((float|int|uint)\\)|Texture2D|SamplerState|#include",
"scope": "packages/cultmath/shaders/CultMath.glsl: 0 matches."
},
{
"pattern": "precision (high|medium|low)p",
"scope": "packages/cultmath/shaders/CultMath.glsl: 0 matches (the host declares precision)."
},
{
"pattern": "CultMath\\.glsl",
"scope": "packages/cultmath/unity/**: 0 matches; the Unity package does not ship the GLSL."
},
{
"pattern": "git diff c49c76ee..HEAD -- packages/cultmath/shaders/CultMath.hlsl | grep '^[-+]' | grep -v '^[-+][-+]'",
"scope": "every changed HLSL line is listed in the cut report with the reason (dialect gap it closes); none changes a value, proven by the mirror test and the existing goldens."
}
],
"operator": []
},
"estimate": {
"lines_added": 1100,
"lines_removed": 8,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": ["GLSL ES 3.00 library file (generated)", "glsl-parity.json fixture"],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [],
"questions": []
}

View file

@ -0,0 +1,219 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 2,
"title": "Interval ops r2: envelope-then-noise fields, scenarios with authored empty space including the void, the tile march on cell ranges, iv_frustum_ball, wasted probes removed, dxc run",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "c9bd003b",
"depends_on": [],
"first": [
"Follow-up on cut report cultmath-tapes:cut_report:cut-interval-ops.h1 (head c9bd003b, worktree F:\\Projects\\CultLib-iv). Rulings in force: operator-tile-amortized-march (the bound stays; one probe per screen tile), operator-compiler-downloads. The operator's challenge, verbatim in the map's Pass 3 section: r1 measured a uniform noise slab with no empty space to skip, the worst case for intervals, and the density that matters is an analytic envelope (a pre-distortion SDF) modulated by noise, where the envelope's interval is tight and proves whole ray lengths empty in one probe. Map: docs/cultmath-interval-ground-cut.md, 'Pass 3'. r1's ops and bounds stay; this revision touches math.Interval.cs, CultMath.Interval.hlsl (both tracked copies), NoiseBoundTests.cs, design.md and README only.",
"The field, from Aetheria (Volumetric.cginc:165-221 and :91-102; Zone.cs:369-397 GetHeight, :426 PowerPulse; Settings.asset). SDF s(p) = p.y + h(p.xz), h >= 0 the well map: h = PowerPulse(|p.xz| / (2R), 2) * 64 (the zone bowl) + sum over wells of PowerPulse(|p.xz - c_b| / r_b, 16) * d_b, with PowerPulse(x, e) = (1 - 4x^2)^e for x in [0, 1/2] and 0 beyond, r_b = 500 M^0.25, d_b = 30 M^0.175 (Aetheria's GravityRadius/GravityDepth curves). Noise only where s < S: fade(s) = 1 - smoothstep(0.75 S, S, s); displaced height s' = s + A fade(s) n(p) with n the warped two-phase coverage noise plus half the detail octave (r1's field shape); density = max(0, (F - s') / B) (Aetheria's floor: FloorOffset F = -20, FloorBlend B = 10; the patch band and the 1e-9 fill are left out). Above the safety distance density is exactly 0, and so is the noise cost: Aetheria's dense march gates the noise pointwise (`if (dist < _SafetyDistance)`); the test's dense march does the same.",
"The composed bound over a tile's frustum slice [z0, z1], Envelope over the slice's exact box, not the ball: with slopes m in [m_c - s, m_c + s] per axis and z in [z0, z1], y = iv_mul([z0, z1], m_y) and x = iv_mul([z0, z1], m_x) (plus the tile's translation o); h's interval over the xz box is the sum over bowls of [pulse(far), pulse(near)], near and far the least and greatest distance from the box to the bowl's centre (pulse is monotone decreasing in distance, so each term is exact; the only slack is the sum). iv_s = iv_add(y, iv_h); iv_fade = the ordered endpoints of fade over iv_s (monotone); iv_n = iv_snoise_ball(ball.xyz * F0, ball.w * F0) with ball = iv_frustum_ball(..., warp = D); iv_s' = iv_add(iv_s, iv_scale(iv_mul(iv_fade, iv_n), A)); the slice is provably empty iff iv_s'.lo >= F. When iv_fade.hi == 0 the noise term is identically zero and iv_snoise_ball is not evaluated: the envelope alone proves the slice empty at no snoise cost. That short-circuit is what tiles compound.",
"Where the warp lives in the bound. Aetheria warps only the noise argument (triNoise3d(pos + fl * ...)); the surface height is read at the unwarped xz and the displacement is applied to y afterwards. So the envelope is evaluated on the pre-distortion SDF over the unwarped slice (no D), and only the noise ball is enlarged by D (iv_frustum_ball's warp argument). A warp applied to the envelope's argument would be unsound without enlarging the box by D; the field is defined so that it is not. The test EnvelopeBoundEnclosesDensity pins this: 2,000 seeded slices x 64 warped points, the composed bound encloses density(p) with no tolerance (every op is monotone or already widened in r1; if a seeded case fails at an endpoint, widen in the implementation, never the test).",
"iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp) -> float4(centre, radius), in C# and both HLSL copies, is the one new CultMath function: the noise ball of a tile over a depth segment. Camera: pinhole at the origin looking +z, focal length f pixels; a pixel's ray is p(z') = (m z', z'); a tile's slopes lie in m_c + [-N/(2f), N/(2f)]^2 (full footprint, so sub-pixel jitter is covered), s = N / (sqrt(2) f). Centre c = (m_c z_m, z_m), z_m = (z0 + z1) / 2; radius r = ((z1 - z0) / 2) |(m_c, 1)| + z1 s + warp. Proof: a point of slope m_c + e (|e| <= s) at depth z' is c + (z' - z_m)(m_c, 1) + z'(e, 0), the terms bounded by the two radius terms, and the warp moves it by at most D (WarpedPointsStayEnclosed). The slice is a convex polytope, so every point between rays is enclosed too. The two vectors are never parallel, so the inequality is strict and float32 cannot cross it.",
"(Tile ball, continued.) Test TileBallEnclosesEveryRaySegment: 2,000 seeded tiles (N in {1, 4, 8, 16}, f in [500, 3000], |m_c| <= 1.1, segments across each scenario's z range), 64 points each with sub-pixel offsets, both depth ends and a flow of length <= warp; no tolerance. Hand mutations that must die, listed in the file header: z0 for z1 in the lateral term; |m_c| for |(m_c, 1)|; the warp dropped. A moving camera needs nothing more: every frame's pre-pass evaluates iv_frustum_ball from that frame's camera and never carries a ball, a mask or a probe result across frames; the only cross-frame state is the TAA history, which is colour, not a soundness input. The test says so by drawing each tile's camera independently.",
"Implementation review of r1's march (NoiseBoundTests.cs:344-376), each item fixed here: (1) every probe was charged one snoise per ray; the tile shares it (ruling). (2) Coarse probes were attempted where the ball could never prove emptiness: iv_snoise_ball's hi is n(c) + L r clamped to 1, so a probe with L r F0 >= 1 + cutoff is a guaranteed failure; at r1's StepMax = 2 that is L r F0 = 3.7, so every coarse probe was a wasted evaluation (the 0.74x row). r2 attempts a noise probe only when L * radius * F0 < 1 - (the cutoff the probe needs), otherwise the level is 'maybe' at no cost. (3) Growth was capped at StepMax = 8 Step0, so a ray through clear space paid Depth / StepMax probes; r2 grows to the remaining range (the hierarchy is over cell-index ranges, halving on maybe, doubling on skip, capped only by the grid's end).",
"(Review, continued.) (4) The dense sample evaluated the detail octave even where occupancy was 0; r2's dense sample evaluates coverage first and the detail only when the envelope and coverage leave density > 0, as Aetheria's `if (dist < _SafetyDistance)` does; the dense baseline gets cheaper and the comparison honest. (5) The finest-level probe before integrating a cell stays: amortized over the tile it costs 1/N^2 and can skip 3 snoise per ray. (6) The step never regrew after a dense cell until a skip; with (3) it regrows from the next skip, which is enough.",
"The march, on cell ranges. The dense grid is a scenario-owned list of cell boundaries z_0 < ... < z_J (uniform for the slab scenarios; Aetheria's quadratic spacing z_j = near + (j/J)^2 (far - near), J = 256, for the Aetheria-like ones, from CloudShader.shader:116). MarchTile takes a range [j0, j1): the envelope probe over the slice (z_j0, z_j1); if empty, skip and double the next range; else if the noise probe is affordable (item 2) and proves empty, skip; else if the range is more than one cell, halve; else the cell is dense. It runs to the grid's end once per tile (a pre-pass does not know any ray's transmittance) and yields the dense cells; each of the N^2 rays integrates exactly those cells in order with the dense march's own Integrate and early-out at 0.02. Counted separately: snoise evaluations and envelope evaluations (pointwise in the dense march; interval probes in the tile march), per ray with probes divided by N^2.",
"The cost model, printed and reasoned about in snoise units: a pointwise envelope evaluation or an envelope probe costs e = 0.2 (a handful of pulses or a height-map fetch against ~150 flops of snoise; stated as an estimate, both counts printed so any weight can be applied). Per scenario the test prints: dense snoise and envelope per ray, tile snoise and envelope probes per ray, the combined cost ratio, and the probe-free ceiling (dense cost over the cost of the dense cells alone). The 2x contract is on the combined cost at N = 8 for scenarios (a) and (c); (b) and (d) are printed as the worst cases and not asserted.",
"Scenarios (IV-REPORT lines, each with 64 tiles at N in {1, 4, 8, 16}, warp in {0, D}): (a) height fog: h = 0, camera at y = S + 30, rays with slope m_y in [0, 0.5] (level and up), Aetheria's grid, expected near-total skip (0 snoise either way; the envelope count carries the ratio). (b) inside the fog: h = 0, camera at y = F - 10, rays level, expected near 1x: every ray saturates within a few cells and the pre-pass still probes the whole grid. (c) Aetheria-like: the zone bowl R = 2000 and four wells of mass {100, 1000, 10000, 1000} at seeded positions inside R, camera at a seeded xz in the bowl at y = max(0, S + 10 - h(camera)), rays gazing across with m_y in [-0.25, 0.1], Aetheria's units (noise scale 414 so F0 = 1/414, D = 15 * 8 / 2 = 60, A = 20 with snoise in [-1, 1]), Aetheria's grid. (d) the uniform slab: r1's field and parameters, kept as the reported worst case. The headline is (e) at N = 8, 200 tiles, printed with both its numbers; (a) and (c) are asserted.",
"(e) the void, the shipped scene (rulings operator-ground-void-brush, operator-void-true-cavity; site-ground r3): a cloud volume everywhere, carved by one negative pseudo-Gaussian sphere, parametrised by the hollow radius Rh (density exactly 0 inside) and the ramp width S (density rises to full over [Rh, Rh + S]); the brush (1 - (d / Rc)^2)^e with CARVE gives Rh = Rc sqrt(1 - CARVE^(-1/e)) and S from the exponent, so (Rh, S) are the authored knobs and (Rc, e) derived. Noise displaces the wall only: fade(d) = smoothstep(Rh - S, Rh, d); d' = d + A fade(d) n(warp(p)), A = 20, F0 = 0.01 (wavelengths 100 and 25), D = 10; K = 1/30 (optical depth 1 per 30 units of full density), termination at T < 0.02. Shipped point: Rh = 198, S = 50, the sun on the hollow's bottom, the camera 120 behind it along the orbit tangent and 45 above it (186 from the centre, 'low'). Rays over the whole frustum (vertical FOV 60, f = 935, so theta_px = 1/f). Every ray crosses empty interior and ends in a wall.",
"Footprint LOD (the operator's challenge, map 'Footprint LOD'): theta_px = 2 tan(fov/2) / H; the footprint at distance z is z theta_px. Step: Delta(z) = max(k z theta_px, min(S / 4, lambda_fine(z) / 2)), k = 4: never finer than four footprints, otherwise the finer of the two content limits (four samples across the ramp, two per wavelength of the finest live octave). Octaves: octave i of wavelength lambda_i carries weight w_i(z) = 1 - smoothstep(lambda_i / (2m), lambda_i / m, z theta_px), m = 4: one smooth blend per octave, no popping; TAA hides per-frame sparsity. The weights are part of the field, so the bound encloses the truncated field exactly: the noise interval over a slice is the sum over octaves of iv_mul([w_i(z1), w_i(z0)], iv_snoise_ball(c F_i, r F_i)), w_i monotone in z. At the shipped parameters (wavelengths 100 and 25, z <= 2 Rc) every weight is 1 in the frustum and the content limits set the step; the footprint bites when S < 4 k z theta_px.",
"(e) runs three marches per tile and prints them: the fixed-step reference (Delta_fixed = min(10, S / 4) everywhere, the pointwise envelope gate, the baseline a march that does not know where the wall is must pay); the masked fixed-step march (the same cells, so transmittance agrees exactly: the soundness pin, as (a)-(d)); and the masked LOD march, the shipped one (steps from Delta(z) inside the unmasked range only, octave weights applied), whose transmittance must be within 0.02 of the reference (printed; an approximation by design, not a soundness claim). Printed per configuration: steps per pixel and snoise per pixel for all three, the combined cost ratio reference / LOD, probes per tile. The grid for (e): Rh in {100, 200, 400, 1000} x camera offset in {centred, mid (0.5 Rh), low (0.94 Rh, the shipped)} x S in {10, 50, 150}, 32 tiles each at N = 8; the shipped point with 200 tiles.",
"Predictions to confirm or refute, N = 8, combined cost: (a) above 50x. (b) 0.8-1.0x. (c) 2-4x: clear cells collapse to a few probes per tile, fog cells cost the same on both sides; the noise ball proves nothing at Aetheria's scale (L r F0 >= 1.6), so item 2 keeps it unevaluated and the saving is the envelope's. (d) 1.3-1.5x: the ceiling at L = 10.099261. (e), from Imagination's model (scratchpad cavity_model.py): the LOD march needs about 6 steps and 12 snoise per pixel at every grid point with S <= 50 (the content limits S / 4 and the detail half-wavelength 12.5 set the step, not the footprint); S = 150 needs 14 steps and 36 snoise. Against the fixed-step reference at the shipped point: steps 35 to 6 (5-8x fewer), combined cost 1.7x, snoise alone 1.25x; cost 3.2x at S = 10, 1.4x at S = 150, 2.5x at Rh = 1000; camera offset barely matters. (e)'s contract is steps per pixel (Q8 retired). If (a) or (c) misses 2x, skip naming saving-2x-scenarios. Never tune the fields, the grids or e.",
"Stryker on Yggdrasil from a plain clone: --since:c9bd003b -m math.Interval.cs; report timeouts separately from kills (a timeout is not a demonstrated kill); if the saving test is what times mutants out, exclude it from Stryker's test selection (a measurement, not a pin) rather than raising the timeout. Compile checks on Starfire, one job, detached with a log, polled in the foreground: FXC cs_5_0 over every cultmath_iv_* including the new function, both tracked copies; then tools/get-dxc.ps1 (pinned, ruling operator-compiler-downloads) and dxc -T lib_6_3 -HV 2021 on both copies. Carried from r1 and still out of scope, name them in the report: the Unity CultMath.dll is not rebuilt; the Unity HLSL copies stay one blob with packages/cultmath/shaders; public asfloat(uint) stays. Branch from c9bd003b; if main moved by docs only, say so as r1 did."
],
"deletes": [
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 33,
"note": ":344-376 MarchIntervals, the per-ray adaptive march with its StepMax cap and unconditional probes, replaced by MarchTile on cell ranges."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 10,
"note": ":401-408 the doc comment naming the r1 fork and :424-425 the Assert.Skip naming it; the fork is answered. A skip survives only on a shortfall in (a) or (c), naming saving-2x-scenarios."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 12,
"note": ":319-330 Density: the unconditional three-snoise sample is replaced by the envelope-gated sample (coverage first, detail only when density can be nonzero)."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 15,
"note": ":423-437 the saving paragraph with r1's numbers (0.93x, 1.83x, 0.74x), replaced by the scenario paragraph with r2's printed numbers."
}
],
"keeps_moves": [
"Every iv_* op, both ball bounds, SNOISE_LIPSCHITZ and its provenance tests: untouched. The ruling keeps the bound; only the field model, the march and its test change.",
"SavingField's noise shape (F0 0.35 in the slab, the warped two-phase coverage, the detail octave, period 4) and (d)'s parameters: kept, as the worst case; scenario (d) must reproduce r1's dense count where the detail gating does not bite (print it).",
"MarchDense and Integrate: unchanged except that Density is envelope-gated; the dense grid becomes the scenario's boundary list.",
"IntervalTests, WarpedPointsStayEnclosed, TightnessReport, MeasureLipschitz, LipschitzConstantPinsSampledGradients: unchanged.",
"HlslSourceCompatibilityTests: no change expected; the new function must find its C# twin through the existing mirror."
],
"adds": [
"packages/cultmath/src/CultMath/math.Interval.cs: iv_frustum_ball after iv_fbm_ball, doc comment carrying the derivation and that the caller scales by F0 and adds its translation.",
"packages/cultmath/shaders/CultMath.Interval.hlsl: cultmath_iv_frustum_ball, same text in both tracked copies.",
"tests/CultMath.Tests/NoiseBoundTests.cs: Scenario (grid, wells, camera, ray slopes, units), SavingField with the envelope (PowerPulse wells, fade, floor), EnvelopeBound (the composed interval, test-side; iv_* ops only), MarchTile, MarchLod (footprint step and octave weights, the shipped march), the per-tile Run; TileBallEnclosesEveryRaySegment; EnvelopeBoundEnclosesDensity; IntervalSkipHalvesEvaluations prints the scenario x N x warp grid and asserts (a) and (c) at N = 8.",
"packages/cultmath/docs/design.md, 'Intervals': the frustum-ball paragraph after :401; the envelope-then-noise paragraph (how a consumer composes the bound, the short-circuit, where the warp lives); the rewritten saving paragraph with the four scenarios and the headline.",
"packages/cultmath/README.md :38-43: iv_frustum_ball named; one sentence that the bounds compose with an analytic envelope."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/src/CultMath/math.Interval.cs",
"line": 152
},
"change": "after iv_fbm_ball: public static float4 iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp)."
},
{
"location": {
"path": "packages/cultmath/shaders/CultMath.Interval.hlsl",
"line": 91
},
"change": "after cultmath_iv_fbm_ball: float4 cultmath_iv_frustum_ball(...); both tracked copies."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 294
},
"change": "SavingField becomes envelope-then-noise over a Scenario; Density is envelope-gated; MarchTile replaces MarchIntervals; Run draws tiles and N^2 rays each, counting snoise and envelope evaluations separately."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 409
},
"change": "IntervalSkipHalvesEvaluations: scenarios (a)-(e), N in {1, 4, 8, 16}, warp in {0, D}; the (e) grid Rh x offset x S with three marches; assert >= 2x combined cost for (a) and (c) and >= 2x fewer steps per pixel for (e) at the shipped point, else Skip naming saving-2x-scenarios."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 89
},
"change": "TileBallEnclosesEveryRaySegment and EnvelopeBoundEnclosesDensity beside WarpedPointsStayEnclosed."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 401
},
"change": "the frustum-ball and envelope-composition paragraphs; :423-437 the saving paragraph rewritten with r2's printed numbers only."
},
{
"location": {
"path": "packages/cultmath/README.md",
"line": 38
},
"change": "iv_frustum_ball named; the envelope composition in one sentence."
}
],
"authority_map": {
"owner": "CultMath owns the interval ops, the noise bounds and now the frustum ball; a consumer (the test's SavingField, the site's pre-pass, Aetheria's march) owns its envelope and composes the bound from iv_* ops. The saving test owns the proving-ground numbers per scenario; the site's browser measurement owns the shipped gain. The bound (SNOISE_LIPSCHITZ, iv_snoise_ball) is unchanged.",
"inputs": [
"the r1 interval surface, unchanged",
"a tile's geometry (centre slope, depth segment, footprint per depth, warp bound) and a scenario's grid, wells, camera and units; Aetheria's shapes and settings as read from its checkout, cited by file:line in the test"
],
"outputs": [
"iv_frustum_ball / cultmath_iv_frustum_ball: a ball enclosing every point of every ray of the tile over the segment, warped",
"printed numbers per scenario: dense and tile snoise and envelope counts per ray, combined cost ratio, probe-free ceiling, at N in {1, 4, 8, 16}"
],
"derived_state": [
"the tile ball and the envelope box are derived from the tile's geometry; no radius is read from a texture, a uniform or a tuned constant",
"whether a noise probe is attempted is derived from L, the radius and the cutoff (item 2), never from a mode flag",
"the probe-free ceiling is derived from the same run; it states what no amortization can beat"
],
"forbidden_writers": [
"no change to SNOISE_LIPSCHITZ, iv_snoise_ball, the noise shape or e; the scenarios may not be tuned toward 2x",
"no per-ray probe in the tile march; no early-out of the pre-pass on any ray's transmittance",
"no warp on the envelope's argument; the warp enlarges the noise ball only",
"no new CultMath function for the envelope: PowerPulse, the box and the composition are consumer-side, built from iv_* ops",
"no tape, prune, affine or choice token in the interval files; no number in design.md or a comment that the committed test did not print in this run"
],
"shared_paths": [
"the dense march and the tile march integrate the same cells of the same grid with the same Integrate; the transmittance check is the proof that nothing provably empty was there",
"the one composed bound (EnvelopeBound) serves the enclosure test and the march; no second composition",
"the existing mirror test covers the new HLSL function; the Unity copy stays one blob"
],
"deletion_line": "r1's per-ray march, its unconditional probes, its growth cap and the unconditional three-snoise dense sample go first; the tile march on cell ranges and the gated sample are written in their place. Nothing in src is deleted. The liability added is one geometry function and two tests plus the scenario model; it buys the tile-march soundness invariant in three runtimes and a saving number measured where there is empty space to find."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green (MeasureLipschitz/MeasureMonotonicity excluded by trait); paste counts and every IV-REPORT line",
"Yggdrasil: dotnet stryker --since:c9bd003b -m math.Interval.cs from a plain clone; 0 survivors; timeouts listed separately from kills",
"Starfire: FXC cs_5_0 kernel over every cultmath_iv_* including cultmath_iv_frustum_ball, both HLSL copies",
"Starfire: tools/get-dxc.ps1 then dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0"
],
"tests": [
{
"name": "NoiseBoundTests.TileBallEnclosesEveryRaySegment",
"pins": "every sample point of every ray of an N x N tile over a depth segment, sub-pixel jitter and warp included, lies inside iv_frustum_ball."
},
{
"name": "NoiseBoundTests.EnvelopeBoundEnclosesDensity",
"pins": "the composed bound (envelope box, fade, noise ball enlarged by the warp) encloses density at every warped point of 2,000 slices, no tolerance; intervals-enclose holds for the composition the site and Aetheria will use."
},
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "scenarios (a) and (c) at N = 8 cost at most half the dense combined cost with transmittance within 1e-3; (e) at the shipped point: the masked LOD march takes at most half the fixed-step reference's steps per pixel, the masked fixed-step march agrees exactly, the LOD march within 0.02; the (e) grid and every scenario, N and warp printed with both counts and the ceiling."
},
{
"name": "IntervalTests.EveryOpEnclosesItsPoints (existing)",
"pins": "r1's enclosure for every op still holds."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "cultmath_iv_frustum_ball matches iv_frustum_ball bit for bit."
}
],
"negative": [
{
"pattern": "tape|prune|affine|choice",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches (case-insensitive)."
},
{
"pattern": "snoise_grad|fbm_grad",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches."
},
{
"pattern": "PowerPulse|pulse|envelope|Scenario",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches; the envelope is consumer-side."
},
{
"pattern": "Assert\\.Skip",
"scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios; 0 if (a), (c) and (e) meet their contracts."
},
{
"pattern": "10\\.099261|0\\.35f",
"scope": "git diff c9bd003b..HEAD -- packages/cultmath/src: the constant is not touched; the slab's F0 stays in (d)."
},
{
"pattern": "git diff --name-only c9bd003b..HEAD",
"scope": "only packages/cultmath/**; both tracked CultMath.Interval.hlsl copies change together."
}
],
"operator": []
},
"estimate": {
"lines_added": 420,
"lines_removed": 80,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-ground-void-brush",
"cultmath-tapes:ruling:operator-void-true-cavity",
"cultmath-tapes:ruling:operator-compiler-downloads",
"cultmath-tapes:ruling:operator-campaign-home"
],
"questions": []
}

View file

@ -0,0 +1,268 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 3,
"title": "Interval ops r3: the frustum ball widened by its derived float32 bound, the analytic constant-density body, one shipped void, (c) held to its ceiling, LOD weights exercised, Stryker kills demonstrated",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "43183276",
"depends_on": [],
"first": [
"Follow-up on cut report cultmath-tapes:cut_report:cut-interval-ops.h2 (head 43183276, worktree F:\\Projects\\CultLib-iv) and Soul's verdict cultmath-tapes:verdict:cut-interval-ops.s1 with its five findings, each resolved below in order. New rulings in force: operator-wells-contract-vs-ceiling ((c)'s contract is at least 90% of the probe-free ceiling, not 2x) and operator-analytic-wall-body (where the envelope interval proves the density constant, one closed-form Beer-Lambert step). Map: docs/cultmath-interval-ground-cut.md, 'Pass 4'. r2's ops, field models, tile march and scenarios stand; this revision touches math.Interval.cs, both tracked CultMath.Interval.hlsl copies, NoiseBoundTests.cs, SnoiseBall tests in the same file, design.md and README only. Nothing new in src beyond the radius widening.",
"Finding 1, frustum-ball-float-crossing (Medium). The ball is widened, in C# and both HLSL copies: radius += (|c.x| + |c.y| + |c.z| + radius) * 2^-20 (the literal 9.5367431640625e-7, exact in float32), after the r2 radius. Derivation, u = 2^-24: the centre c = (m_c z_m, z_m) carries at most two roundings per component (z_m = fl(z0 + z1) * 0.5, then the product), so |c_float - c_exact| <= 2u |c|_1; the radius carries at most seven first-order roundings (z1 - z0; the axis sqrt at 3u; the product; z1 * footprint; the sum; + warp), so r_exact <= r_float (1 + 7u); the widening's own addition rounds once more. 2^-20 = 16u covers 8u (|c|_1 + r) twice over. fma contraction removes roundings and only tightens, so dxc's HLSL and any driver's GLSL stay covered. The sentence 'the two vectors are never parallel, so the inequality is strict and float32 cannot cross it' leaves math.Interval.cs:179-180, the HLSL and design.md:412; the rule and its derivation take its place.",
"(Finding 1, continued.) TileBallEnclosesEveryRaySegment gains Soul's extreme families beside the r2 domain, 2,000 cases each, 8 footprint corners at both depth ends with the flow pointed outward along the corner's direction from the centre, the distance computed in double from the float ball and the float ray (point (m z, z) formed in double from the float m and z), no tolerance: z0 == z1; thin (z1 - z0 = 1e-6 z1); |m_c| up to 1000; far (z up to 1e7); z0 = 0 with z1 down to 1e-3; f down to 1; warp up to 1e6. The derivation pin becomes two-sided: r_exact <= ball.w <= r_exact + 2^-19 (r_exact + |c|_1), r_exact in double, so a ball that forgets the widening fails and a ball that grows past the rule fails. The hand mutations of r2 stay; a fourth, the widening dropped, must fail the degenerate family alone. WarpedPointsStayEnclosed is unchanged.",
"Finding 2, aetheria-far-plane-miscited (Low). FogField's grid becomes Quadratic(0.3, 2048, 256), cited as the Main Camera (Aetheria d3075eb0 Assets/Scenes/ARPG.unity:31777 m_Name Main Camera, :31830 far clip plane 2048; VolumeCloudRenderer.cs:36 RequireComponent(Camera)); the comment at NoiseBoundTests.cs:594 no longer names the Tint camera's 1000. Soul re-measured (c) at far 2048: 1.39-1.51x at warp D, 2.46-2.74x at warp 0, ceiling within 1-2% of the ratio; the verdict on (c) does not move. Print every (a)-(c) row again at 2048.",
"Finding 3, shipped-void-two-scenes (Medium). ONE shipped void, authored as (Rh, S) per the map's own rule, everything else derived; the map, the site-ground r3 spec and the test agree on it: Rh = 198, S = 50, so Rc = Rh + S = 248 and e = -ln CARVE / ln(1 - (Rh / Rc)^2) = 0.40 (CARVE 1.5); fade band [Rh - S, Rh], A = 20, F0 = 0.01, D = 10, K = 1/30. This is the scene r2 measured (2.21x) and the Footprint-LOD rule's; the map's site section (Rc 640, e 4, a 442-wide ramp) was the other scene and is superseded. The honest choice changes the look (a 50-wide noisy wall with a kinked outer edge, not a soft pseudo-Gaussian wall), so it is Q9 in the map, a fork for the operator with (a) recommended. Hands codes (a); another ruling moves only the shipped constants and the site section. VoidField's doc comment derives (Rc, e) from (Rh, S) and names it the one shipped void.",
"Finding 4, lod-weights-never-exercised (Low). VoidField takes a focal length. One configuration reaches the weights: '(e) LOD far' Rh = 4000, S = 50, camera low, f = 467 (1080 rows at a 98 degree vertical FOV): theta = 1/467, so the fine weight (wavelength 25) is below 1 past z = 1460 and 0 past 2920, the coarse weight (100) below 1 past 5840, and the grid (|camera - c_v| + Rc + 6/K, about 7990) reaches both; 4 z theta exceeds 12.5 past z = 1460, so LodStep is footprint-set there too. It joins EnvelopeBoundEnclosesDensity's field list (the truncated check is no longer vacuous) and the saving test prints it, not asserted. Hand mutation that must fail the truncated check on it: w(z0) for w(z1) in WeightBound.",
"(Finding 4, continued: the deviation.) Recorded in the test's doc and design.md: the bound takes each octave weight over [w(z1), 1], not the spec's [w(z1), w(z0)], because one Bound serves the full field (w = 1) and the truncated one (w in [w(z1), w(z0)], w decreasing in z). [w(z1), 1] contains both, iv_mul is monotone in its interval argument, so the composed interval is a superset of either field's and encloses both. The slack is only in far tiles whose step the footprint already sets.",
"Finding 5, stryker-timeout-survivor (Low). Mutant 2525 (iv_sqr a.x >= 0 to a.x > 0) is equivalent: at a.x == 0 both branches return the same ends ((0, h) either way, (0, 0) when a.y == 0); the report says so by its diff, since numbers are per run. New enclosure cases kill the mirror-only survivors semantically: SnoiseBallEnclosesPoints and FbmBallEnclosesPoints draw radii LogUniform(1e-3, 1e3), so L / r in iv_snoise_ball (too tight once r > 1: at r = 100 the mutant's bound is 0.2 wide while snoise over the ball spans [-1, 1]) dies to an enclosure test, not only to the HLSL mirror. The mutants Soul listed as mirror-only (iv_scale s >= 0 to s > 0; the two iv_abs mutants) are hand-run and each either named equivalent with the one-line argument (signed zero or a boundary both branches map alike) or killed by one added point case in IntervalTests.",
"(Finding 5, continued.) Stryker runs again as r2 did, --since:43183276; every timeout in math.Interval.cs is hand-run (patch from the mutation report, dotnet test minus IntervalSkipHalvesEvaluations) and the report lists killed, survived and equivalent by diff. The report claims only demonstrated kills; a timeout is not a kill.",
"The analytic body (ruling operator-analytic-wall-body), consumer-side in the test and the site, no new CultMath function. When it applies: the Scenario's Bound over the remaining range [z, z_end] of the grid, gated, is a point interval with positive value: the envelope with the free noise interval [-1.5, 1.5] (coverage in [-1, 1] plus half the detail) proves fade = 1 and d_lo + A (fade n)_lo >= Rc, so every point of every ray of the tile over the rest of the grid has density exactly K for any noise value; or, generically, lo == hi > 0 from whichever path the bound took. Only then does the march integrate T *= exp(-K ext (z_end - z) |dir|) in one step and terminate (the grid end is past opacity by construction). For (a)-(d) no Bound is a positive point (the fog's density varies with s'), so the step never applies there, and the test asserts bodyStart = -1 for every (a)-(d) tile.",
"(Analytic body, continued.) MarchTile returns (dense cells, bodyStart, bodyDensity). As r2 on cell ranges; when a single cell is found not provably empty and its own gated bound is a positive point (the cell is provably constant), one more probe over [grid[j], grid[^1]]: if that is a positive point too, bodyStart = j, bodyDensity = its value, and every remaining cell is appended dense without probing (adding cells is always sound; it also drops the body's probes). The mask's meaning is unchanged, so the masked fixed-step march's exact agreement with the reference stands as the soundness pin. MarchLod: inside unmasked runs as r2; on reaching a cell >= bodyStart it takes the one analytic step over [z, grid end], counts one step, and stops.",
"(Analytic body, the grid end.) Every void configuration's grid ends at |camera - c_v| + Rc + 6/K (was 5/K): the body starts at d >= Rc + A 1.5 = Rc + 30 plus box slack, so 6/K leaves at least 4.9 of optical depth, T <= 0.0075 < 0.02, and 'unfinished rays 0' still prints. This is the grid end, not the field; the reference stops by opacity and its count does not move.",
"(Analytic body, the pin.) New test AnalyticBodyMatchesFineMarch over the (e) grid, the shipped void and the LOD-far configuration, 2,000 seeded tiles at N in {1, 4, 8, 16}: (1) wherever MarchTile reports bodyStart b: the Bound over [grid[b], grid end] is a positive point; the full and the truncated Density at 64 random (pixel, sub-pixel, cell >= b, depth in cell) points equal bodyDensity exactly, no tolerance; (2) the analytic factor exp(-bodyDensity ext L) over the body equals the fixed-step product of the body cells' Integrate factors (no early-out, same cells) within 1e-4 relative; (3) for every (a)-(d) tile bodyStart = -1. Hand mutation that must fail (1): in VoidField.Compose, CarveRadius for HollowRadius in the lo test (the body claimed from the ramp). The site's mirror of this (site-ground r3): the pre-pass writes the body-start cell in the texel's fourth word, the main pass takes the same one step; its browser check is the dense-reference agreement already in that spec.",
"Contracts after r3, asserted at N = 8 in IntervalSkipHalvesEvaluations, else one Assert.Skip naming saving-2x-scenarios with every shortfall: (a) at least 2x combined cost (the lower of warp 0 and D), measured 16384x. (c) at least 0.90 of its probe-free ceiling (Ratio / Ceiling, the lower of warp 0 and D), measured 0.99 at warp D and 0.96 at warp 0 in r2; the ruling's reading is that the ceiling is what intervals control and the fog band's cost is the field's. (e) at least 2x fewer LOD steps per pixel than the fixed-step reference at the ONE shipped void (200 tiles), the masked fixed-step march agreeing exactly and the LOD march within 0.02 of the reference. (b), (d) and the (e) grid incl. LOD-far: printed, not asserted. Nothing is tuned toward any contract: the fields, the grids (beyond the stated 6/K end), e = 0.2 and the seeds stay.",
"Predictions to confirm or refute (Imagination's rule from r2's printed rows: the LOD steps that evaluated noise are about snoise / 2.8 plus two pre-band steps; the rest were constant-body steps, which the analytic step makes one). Shipped void: steps per pixel 36.34 (ref) to about 12.5 (was 16.45), 2.6-3.3x fewer; snoise per pixel about 26.6, unchanged; combined cost about 1.38x (was 1.34x); envelope probes per tile about 50 (was 59.1). Grid, low camera, ref/LOD steps: S = 10 about 5-6x at Rh 100-400 and 11x at Rh 1000 (was 2.1-3.4x and 6.8x); S = 50 about 2.5x, 2.8x, 3.5x, 5.2x for Rh 100, 200, 400, 1000 (was 1.8, 2.2, 2.9, 4.5); S = 150 unchanged (rays end inside the 150-wide ramp before the body is proven). Rh 1000 centred S 10: about 30x (was 12.4x). LOD-far: printed, none predicted. If the shipped point lands under 2.6x, say which assumption failed: the pre-band's 0-snoise steps, or a body start later than Rc + 30 by box slack.",
"Compile and mutation checks as r2: Yggdrasil dotnet test (counts and every IV-REPORT line pasted); Stryker --since:43183276 -m math.Interval.cs from a plain clone with every timeout hand-run as in Finding 5; Starfire FXC cs_5_0 over every cultmath_iv_* on both HLSL copies and tools/get-dxc.ps1 then dxc -T lib_6_3 -HV 2021 on both, exit 0 (Soul could not re-run these for r2; they are re-run here because the HLSL changes). Carried and still out of scope, named in the report: the Unity CultMath.dll is not rebuilt; the Unity HLSL copies stay one blob with packages/cultmath/shaders; public asfloat(uint) stays. Main moved by docs only since 43183276's base (Imagination passes 3-3d and 4); say so. glsl-lowering r2 rebases onto this revision's head, not r2's (its fixture family for cultmath_iv_frustum_ball changes with the widening); its spec now says so."
],
"deletes": [
{
"path": "packages/cultmath/src/CultMath/math.Interval.cs",
"lines": 2,
"note": ":179-180 the sentence 'The two vectors are never parallel, so the triangle inequality is strict' and its float32 claim; replaced by the widening rule and its derivation."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 1,
"note": ":412 'the two vectors are never parallel, so the bound is strict'; replaced by the rule."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 1,
"note": ":594 the Tint camera citation (ARPG.unity:635-636, far 1000); replaced by the Main Camera's :31830, far 2048."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 3,
"note": ":1181-1194 the 2x test on ratios[c] and the skip message's wording; replaced by the 0.90-of-ceiling test and a message naming each shortfall."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 12,
"note": ":461-497 the r2 numbers (1.41x, 16.45, 2.21x, the (c) shortfall paragraph); replaced by r3's printed numbers and the three contracts."
}
],
"keeps_moves": [
"Every iv_* op, iv_snoise_ball, iv_fbm_ball, SNOISE_LIPSCHITZ and its provenance tests: untouched. iv_frustum_ball's centre and the r2 radius terms: untouched; only the widening term is appended.",
"FogField, SlabField, WarpedNoise, Compose, the gated probe (BestBall), MarchTile's range hierarchy, the masked fixed-step march and Run: kept; MarchTile gains the body return and MarchLod the one analytic step.",
"VoidField's field, Density, Bound, Weight, WeightBound and LodStep: kept; the constructor gains the focal length and the grid end moves to 6/K; the shipped point stays Shipped() = (198, 50, shipped camera).",
"TileBallEnclosesEveryRaySegment's r2 domain, EnvelopeBoundEnclosesDensity, IntervalTests, TightnessReport, MeasureLipschitz, LipschitzConstantPinsSampledGradients: kept; the first two gain cases.",
"HlslSourceCompatibilityTests: the mirror count stays 53; cultmath_iv_frustum_ball's new text must match the C# bit for bit."
],
"adds": [
"math.Interval.cs and both CultMath.Interval.hlsl: the widening line in iv_frustum_ball / cultmath_iv_frustum_ball, and the rule in the doc comment.",
"NoiseBoundTests.cs: the extreme families and the two-sided derivation pin in TileBallEnclosesEveryRaySegment; VoidField focal parameter and the LOD-far configuration; MarchTile's (bodyStart, bodyDensity); MarchLod's analytic step; AnalyticBodyMatchesFineMarch; the (c) ceiling contract; radii to 1e3 in the two ball tests; the WeightBound deviation in the doc comment; the shipped-void derivation in VoidField's doc comment.",
"IntervalTests.cs: at most three point cases for the mirror-only mutants that are not equivalent (Finding 5).",
"design.md 'Intervals': the widening rule and derivation; the analytic-body paragraph (condition, pre-pass word, one step, the pin); the WeightBound deviation; r3's printed numbers under the three contracts; the one shipped void with (Rc, e) derived.",
"README.md :41: one clause that iv_frustum_ball's radius carries its float32 rounding bound."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/src/CultMath/math.Interval.cs",
"line": 183
},
"change": "iv_frustum_ball: after the r2 radius, radius += (abs(cx) + abs(cy) + abs(zm) + radius) * 9.5367431640625e-7f; doc comment :168-182 states the rule and derivation in place of the strictness sentence."
},
{
"location": {
"path": "packages/cultmath/shaders/CultMath.Interval.hlsl",
"line": 110
},
"change": "cultmath_iv_frustum_ball: the same widening line; both tracked copies (packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Interval.hlsl identical blob)."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 128
},
"change": "TileBallEnclosesEveryRaySegment: the seven extreme families, outward flow at corners, double distance, the two-sided pin; the file header lists the fourth hand mutation (widening dropped) and the WeightBound and Compose mutations."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 58
},
"change": "SnoiseBallEnclosesPoints and FbmBallEnclosesPoints: radius LogUniform(1e-3, 1e3)."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 594
},
"change": "FogField: Grid = Quadratic(0.3, 2048, 256), Main Camera citation."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 829
},
"change": "VoidField: focal parameter (default 935); LodFar() = (4000, 50, low, f 467); grid end reach + Rc + 6/K; doc comment with the (Rh, S) -> (Rc, e) derivation, the one shipped void, the [w(z1), 1] deviation."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1009
},
"change": "MarchTile returns (cells, bodyStart, bodyDensity) with the one confirming probe; MarchLod takes the analytic step at bodyStart; Run and RunStats carry it; ReportVoid prints body steps."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1160
},
"change": "IntervalSkipHalvesEvaluations: (c) asserted as Ratio >= 0.90 Ceiling; the LOD-far row printed; the (e) grid printed with the analytic body; AnalyticBodyMatchesFineMarch added beside it."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 403
},
"change": ":403-430 the frustum-ball paragraph gains the widening rule; :412 strictness sentence deleted; :461-497 the saving section rewritten with r3's printed numbers, the three contracts, the analytic body and the shipped void."
},
{
"location": {
"path": "packages/cultmath/README.md",
"line": 41
},
"change": "iv_frustum_ball: 'its radius carries its own float32 rounding bound'."
}
],
"authority_map": {
"owner": "CultMath owns the interval ops, the noise bounds and the frustum ball, whose enclosure promise now covers its own float32 rounding (the widening is part of the function, not of any consumer). The consumer (the test's Scenario, the site's pre-pass) owns the envelope, the composed bound, the body proof and the Beer-Lambert step. The saving test owns the proving-ground numbers; the operator owns the shipped void (Q9) and the contracts (the two rulings).",
"inputs": [
"the r2 surface unchanged; a tile's geometry and warp; a scenario's grid, wells, camera, focal length and units, Aetheria's Main Camera far plane 2048 cited by line",
"the one shipped void as (Rh, S) = (198, 50); every other void constant derived from it"
],
"outputs": [
"iv_frustum_ball / cultmath_iv_frustum_ball: a float32 ball that encloses every exact point of every ray of the tile over the segment, warped, under float32 evaluation of the ball itself",
"per tile: the dense cells and, when the envelope proves the rest constant, the body-start cell and its density; printed numbers per scenario including the body steps"
],
"derived_state": [
"the widening is derived from the computed centre and radius (2^-20 of |c|_1 + r), never a tuned epsilon; a consumer adds nothing for the ball's own rounding and remains responsible for its world transform",
"(Rc, e) are derived from (Rh, S): Rc = Rh + S, e = -ln CARVE / ln(1 - (Rh/Rc)^2); no void constant is authored twice",
"the body start is derived from the same Bound the mask is derived from, by one more probe; the main pass reads it, never decides it",
"the (c) contract value is derived from the run (0.90 of its own ceiling); no absolute target is written for (c)"
],
"forbidden_writers": [
"no change to SNOISE_LIPSCHITZ, iv_snoise_ball, iv_fbm_ball, the noise shape, e = 0.2, the seeds or any field toward a contract; the grid end moves to 6/K for termination only",
"no analytic step from a pointwise density or from the noise ball alone: only a point-interval Bound over the whole remaining range proves the body",
"no early-out of the pre-pass on transmittance; no probe in the main pass (the body-start cell is read, its constant is one pointwise envelope value)",
"no second shipped void anywhere: the map's site section, the site-ground spec and VoidField.Shipped() name (198, 50) and derive the rest; no number in a doc or comment that the committed test did not print in this run",
"no new CultMath function; PowerPulse, the box, the composition and the Beer-Lambert step stay consumer-side; no tape, prune, affine or choice token in the interval files"
],
"shared_paths": [
"the masked fixed-step march still integrates exactly the dense march's cells (the soundness pin); only MarchLod takes the analytic step, and AnalyticBodyMatchesFineMarch ties that step to the fixed-step product over the same cells",
"one Bound serves the enclosure tests, the mask, the body proof and the gated probe; no second composition and no second constancy test",
"the widening lives once in C# and is mirrored bit for bit into both HLSL copies by the existing mirror test; GLSL follows by the lowering (glsl-lowering r2 rebases onto this head)"
],
"deletion_line": "The false strictness claim leaves the C# doc, the HLSL and design.md before the widening is written; the Tint camera citation leaves before the Main Camera's is written; the (c) 2x assertion and the second shipped void leave before the ceiling contract and the one void are written. Net in src: one line and a doc comment. In tests: one new pin test, seven case families, one configuration, the body return; what they buy is intervals-enclose holding for the ball's own float32 evaluation, the analytic body the ruling asked for with its soundness pinned, LOD weights actually exercised, and a mutation report that claims only what it demonstrated."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green (MeasureLipschitz/MeasureMonotonicity excluded by trait); paste counts and every IV-REPORT line, (a)-(d) at far 2048, the (e) grid with body steps, LOD-far, the shipped headline",
"Yggdrasil: dotnet stryker --since:43183276 -m math.Interval.cs from a plain clone; every timeout hand-run; killed, survived and equivalent listed by diff; 0 undemonstrated",
"Starfire: FXC cs_5_0 kernel over every cultmath_iv_* including cultmath_iv_frustum_ball, both HLSL copies, exit 0",
"Starfire: tools/get-dxc.ps1 (pinned) then dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0, no diagnostics"
],
"tests": [
{
"name": "NoiseBoundTests.TileBallEnclosesEveryRaySegment",
"pins": "every exact point of every ray of the tile, corners at both depth ends with an outward flow, lies inside the float32 ball, in the r2 domain and in the seven extreme families, distance in double, no tolerance; the radius is the derivation plus at most 2^-19 (r + |c|_1) and at least the derivation."
},
{
"name": "NoiseBoundTests.EnvelopeBoundEnclosesDensity",
"pins": "the composed bound encloses the full and the footprint-truncated density at every warped point of 2,000 slices, including the LOD-far configuration where octave weights are strictly between 0 and 1 and at 0."
},
{
"name": "NoiseBoundTests.AnalyticBodyMatchesFineMarch",
"pins": "a body start is reported only where the Bound over the whole remaining range is a positive point and every sampled density there equals it exactly; the analytic factor equals the fixed-step product within 1e-4 relative; no (a)-(d) tile reports a body."
},
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "(a) at least 2x combined cost; (c) at least 0.90 of its probe-free ceiling; (e) at the one shipped void at least 2x fewer LOD steps per pixel, the masked fixed-step march exact, the LOD march within 0.02; every row printed including body steps; one Skip naming saving-2x-scenarios on any shortfall."
},
{
"name": "NoiseBoundTests.SnoiseBallEnclosesPoints and FbmBallEnclosesPoints",
"pins": "the ball bounds enclose their functions for radii up to 1e3, so a bound tightened for r > 1 (L / r) dies to an enclosure test."
},
{
"name": "IntervalTests.EveryOpEnclosesItsPoints (existing, plus point cases)",
"pins": "r1's enclosure for every op still holds; any non-equivalent mirror-only mutant from Finding 5 has a point case that kills it."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "the widened cultmath_iv_frustum_ball matches iv_frustum_ball bit for bit; 53 functions compared."
}
],
"negative": [
{
"pattern": "never parallel|cannot cross|strict",
"scope": "math.Interval.cs iv_frustum_ball doc comment, CultMath.Interval.hlsl, design.md 'Intervals' frustum-ball paragraph: 0 matches."
},
{
"pattern": "635-636|far 1000|Tint",
"scope": "NoiseBoundTests.cs: 0 matches; ':31830' and '2048' appear in FogField's citation."
},
{
"pattern": "Rc = 640|e = 4\\b|Rc ~ 220|e ~ 0\\.6|about 220|about 0\\.6",
"scope": "NoiseBoundTests.cs and design.md: 0 matches; the shipped void is derived from (198, 50) as Rc = 248, e = 0.40."
},
{
"pattern": "tape|prune|affine|choice|PowerPulse|pulse|envelope|Scenario|body|Beer",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches (case-insensitive); the analytic body is consumer-side."
},
{
"pattern": "Assert\\.Skip",
"scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios; 0 if (a), (c) and (e) meet their contracts."
},
{
"pattern": "10\\.099261|0\\.35f",
"scope": "git diff 43183276..HEAD -- packages/cultmath/src: the constant is not touched; the slab's F0 stays in (d)."
},
{
"pattern": "git diff --name-only 43183276..HEAD",
"scope": "only packages/cultmath/**; both tracked CultMath.Interval.hlsl copies change together and stay one blob."
},
{
"pattern": "git diff 43183276..HEAD -- packages/cultmath/src | grep '^[-+]' | grep -v '^[-+][-+]' | grep -v '///' | wc -l",
"scope": "at most 4 changed code lines in src: the widening line in iv_frustum_ball and nothing else."
}
],
"operator": []
},
"estimate": {
"lines_added": 260,
"lines_removed": 40,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-wells-contract-vs-ceiling",
"cultmath-tapes:ruling:operator-analytic-wall-body",
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-measure-with-empty-space",
"cultmath-tapes:ruling:operator-footprint-lod-march",
"cultmath-tapes:ruling:operator-ground-void-brush",
"cultmath-tapes:ruling:operator-void-true-cavity",
"cultmath-tapes:ruling:operator-compiler-downloads",
"cultmath-tapes:ruling:operator-campaign-home"
],
"questions": [
"Q9 in the map: the one shipped void. (a, recommended) Rh 198, S 50, derived Rc 248, e 0.40, the scene r2 measured; (b) the site section's Rc 640, e 4; (c) Rh 198, S 100. Hands codes (a)."
]
}

View file

@ -0,0 +1,292 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 4,
"title": "Interval ops r4: LOD agreement against a converged march at the body start, BodyDepth pinned, (c) against an oracle ceiling, WeightBound pinned, design.md from r4's own run",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "3ac7250a",
"depends_on": [],
"first": [
"Follow-up on verdict cultmath-tapes:verdict:cut-interval-ops.s2 (range 43183276..3ac7250a, worktree F:\\Projects\\CultLib-iv), four findings, one item each below. Touches packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs and packages/cultmath/docs/design.md only: no src, no HLSL, no README. Question cultmath-tapes:question:wells-ceiling-oracle decides only (c)'s assertion lines (item 9). Self dispatches with its ruling in the brief. Line numbers are at 3ac7250a. Every probe number quoted here is from Imagination's scratch probe at 3ac7250a, with the test's own seeds, tiles and N = 8. It is never committed; r4's run reprints its own numbers.",
"Finding lod-agreement-blind-past-body (High), measured. Comparing with the 10-unit fixed-step march cannot pin LodStep. At the shipped void, the LOD march's T at the body start differs from it by up to 0.300 of max(T, 0.02). LodStep doubled differs by 0.309. Both are unconverged midpoint quadratures of a ramp with 25-unit detail. A converged march separates them: h = 0.5, which agrees with h = 1 to 0.0065. Relative optical-depth error per ray, max at the headline: fixed-step 0.061, LOD 0.086, LodStep x2 0.177, LodStep x0.5 0.036. With the pre-body integration dropped, T is wrong by 26.5x max(T, 0.02). So the LOD march is 1.4x less accurate than the dense march before the body. Report it; it is not tuned.",
"The agreement rule. MarchLod gains `float cutoff`; Run's costed march passes 0.02, as now. It returns (Transmittance, AtBody). AtBody is T when z first reaches the body start, before the body step, or the final T when there is no body. New helper ConvergedDepth(field, tile, slope, stop, h, lod) returns the optical depth in double, by midpoint quadrature at step h. It runs over EVERY grid cell before `stop`, never the mask's, so it trusts no pre-pass, using the field's Density.",
"(Agreement, continued.) In Run with lod, per ray, with stop = bodyStart, or the cell count (grid.Length - 1) when there is none:\n- tauConv = ConvergedDepth(..., stop, 0.5, lod: true).\n- tauLod = -ln(max(AtBody, 1e-30)) from a second MarchLod at cutoff 0.\n- tauRef = -ln of the product of Integrate over cells < stop, lod: false, with no early-out.\nRunStats keeps MaxLodDepthError and MaxRefDepthError, each max |tau - tauConv| / max(tauConv, 1e-3). The converged march and the cutoff-0 march count into a throwaway Counts and no steps, so cost rows do not move.",
"The (e) agreement contract replaces 'LOD within 0.02 at the end': at the shipped headline, MaxLodDepthError <= 2 x MaxRefDepthError. The footprint march may be at most twice as wrong as the dense march it is measured against. This mirrors the 2x step contract and is Imagination's default; Self may put it to the operator. Measured 0.086 <= 0.122. LodStep x2 and the dropped pre-body integration fail it. The end-of-ray assertion :1455 is deleted. End-of-ray |dT| stays printed. Every (e) row and LOD-far prints both errors and their ratio, which is printed and not asserted. LOD-far's tauRef carries the deliberate octave truncation, so its ratio is not comparable.",
"BodyFactor becomes BodyDepth (:1198-1201). It returns the optical depth Density x ext x (grid end - z) x |dir| as a float. MarchLod multiplies by exp(-BodyDepth(...)). AnalyticBodyMatchesFineMarch part (2) deletes its own tau and analytic (:1375-1377). It compares BodyDepth(field, tile, ray, grid[b], lod: true, counts) with the double sum over body cells of -ln(Integrate(cell, lod: true)), within 1e-4 relative. Probe: in the T domain the comparison underflows at LOD-far (float exp of about -256 is 0, relative error 1); in the depth domain it does not. With |dir| dropped, the depth error is |dir| - 1, at least 0.00145 at the shipped camera, so the mutant dies on every tile.",
"Finding wells-ceiling-contract-vacuous (Medium), measured at N = 8. Warp 0: ratio 2.744x, mask ceiling 2.855x (0.961), oracle ceiling 7.263x (0.378), oracle-empty cells skipped 0.952, skip-nothing pre-pass 0.136. Warp D: 1.391x, 1.401x (0.993), 7.165x (0.194), 0.842, 0.139. At warp D about 80% of the masked cost lies in cells where every ray's sample is 0, so design.md's 'where the rays need them' is false. The ruling's reason needs a ceiling the pre-pass cannot move. Hence the question.",
"Measurement, whatever the ruling. Run draws a tile's N^2 slopes first, then marches. The random stream reorders, so every IV-REPORT row reprints. The oracle mask is the cells whose Density at mid-depth is > 0 for at least one of those slopes, evaluated into a throwaway Counts. The oracle march runs each ray over the oracle cells only, with the same Integrate and early-out, costed into a new RunStats.Oracle. Assert its T equals the dense march's exactly (probe max |dT| 0). RunStats.Ceiling (:1259-1260) goes. In its place: OracleCeiling = Dense.Cost / Oracle.Cost; Efficiency = Oracle.Cost / (Masked.Cost + Probes.Cost); ProbeOverhead = Masked.Cost / (Masked.Cost + Probes.Cost); CullFraction = oracle-empty cells the mask skips / oracle-empty cells. Report prints all four.",
"(c)'s assertion, at N = 8, the lower of warp 0 and D, per the ruling on wells-ceiling-oracle. :1437 records the chosen value. For the cull floor, Assert.True fails rather than skips.\n- oracle-target-cull-floor (recommended): hard CullFraction >= 0.5, plus Efficiency >= 0.90 in the shortfall list.\n- oracle-target-only: the shortfall alone.\n- cull-fraction: CullFraction >= 0.90 in the shortfall list.\n- overhead-plus-floor: ProbeOverhead >= 0.90 in the shortfall list, plus the hard floor.\nThe contracts line names the measured quantity. Note: under options 1 and 2 the test skips today, naming saving-2x-scenarios with (c)'s shortfall; (a) and (e) are still evaluated and listed.",
"Finding weightbound-mutation-survives (Low). VoidField gains public (float2 Coarse, float2 Fine) WeightBounds(float z0, float z1) beside Weights(z) (:992). WeightBound (:1035) stays private and is what WeightBounds calls. New Fact WeightBoundEnclosesOneSignedNoise on LodFar, 2,000 cases: z0 ~ U(1460, 0.95 x grid end), z1 ~ U(z0, grid end), 16 depths in [z0, z1] including both ends. Each octave weight lies in its bound. For the one-signed noise interval n = [0.25, 1] and n_s in {0.25, U(0.25, 1), 1}, w(z) n_s lies in iv_mul(bound, n). Probe: the spec's mutant [w(z0), 1] misses w(z1) x 0.25 in 2000 of 2000 fine-octave cases on (1460, 2920). The header (:14-20) moves that mutant to this test.",
"Finding design-saving-section-stale (Medium). Rewrite design.md :475-512 from r4's final run, where every figure appears in an IV-REPORT line the report pastes. Delete :422-423's '6e-4 at the shipped scales' sentence, which no test prints. Contents:\n- the tile march and its marches;\n- (a)-(d) at far 2048, with (c)'s oracle ceiling, efficiency, cull fraction and probe overhead;\n- (e) at the shipped void, Rh 198, S 50, Rc = Rh + S, e = -ln 1.5 / ln(1 - (Rh/Rc)^2): steps, snoise, cost, body tiles, the two depth errors;\n- the analytic body (condition, one probe, one step, the pin);\n- the WeightBound [w(z1), 1] deviation and why it encloses;\n- the contracts as asserted, (c) in the ruling's words.\n'The test skips' appears only if this run skips. No r2 or r3 number survives.",
"Site-ground's dense-reference browser check has the same blind spot (its converged mean luminance is compared after the walls' bodies). It is follow-up cultmath-tapes:follow_up:site-ground-dense-check-blind-past-body, owned by the next site-ground spec. Not this cut.",
"Mutation evidence, cheap by design. src is untouched, so Stryker has nothing in scope and is not run; the negative check proves the premise. Run ONE tools/mutation-table.sh job on Yggdrasil against the final head, test filter FullyQualifiedName~NoiseBoundTests, with these rows:\n- MarchLod `transmittance *= Integrate(field, tile, slope, z, next, lod: true, counts);` -> `transmittance *= 1.0f;` must fail IntervalSkipHalvesEvaluations;\n- LodStep's return wrapped in `2.0f * (...)`: same test;\n- BodyDepth without its sqrt factor: AnalyticBodyMatchesFineMarch;\n- WeightBound `).x, 1.0f)` -> `).y, 1.0f)`: WeightBoundEnclosesOneSignedNoise;\n- MarchTile `if (bound.y <= 0.0f)` -> `if (bound.y <= 0.0f && !field.Name.Contains(\"wells\"))`: IntervalSkipHalvesEvaluations, under the floor options; a survivor under oracle-target-only, by that option's design;\n- Compose `CarveRadius` -> `HollowRadius` in the lo test: AnalyticBodyMatchesFineMarch (carried).\nNo per-mutant hand loops, no Starfire jobs.",
"Budget: under 200k Hands tokens. Read NoiseBoundTests.cs :480-1469 and design.md :344-515 once; no other file. Verification is one full CultMath.Tests run on Yggdrasil after the code commits (paste counts and every IV-REPORT line), the mutation-table job, and one fix round. If the headline agreement fails at r4's own numbers, do not move the factor or the step: report the measured errors as a fork."
],
"deletes": [
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 1,
"note": ":1455 the end-of-ray assertion 'the footprint-aware march differs from the reference by'; blind past the body (finding lod-agreement-blind-past-body)."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 4,
"note": ":1198-1201 BodyFactor and its comment; replaced by BodyDepth, the one owner of the body's optical depth."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 3,
"note": ":1375-1377 the test's own tau, its exp and the T-domain comparison; replaced by BodyDepth against the cells' summed depth."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 2,
"note": ":1259-1260 RunStats.Ceiling and 'What no amortization can beat'; the mask-derived ceiling stops being a contract owner."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 38,
"note": ":475-512 the saving section with r2's rows (1.41x, 16.45, 2.21x, 1.34x) and '(c) does not, so the test skips'."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 2,
"note": ":422-423 'At the shipped scales (|c|_1 near 600, r near 5) it adds 6e-4.'; printed by no test."
}
],
"keeps_moves": [
"packages/cultmath/src, both CultMath.Interval.hlsl copies, README.md and every other test file: untouched.",
"Fields, grids (6/K end), seeds, e = 0.2, LodStep, Density, Bound, Compose, MarchTile, Integrate, the masked fixed-step march's exactness asserts, the (a) and (e) step contracts and the Skip mechanism: unchanged.",
"TileBallEnclosesEveryRaySegment, EnvelopeBoundEnclosesDensity, the Lipschitz tests, TightnessReport, IntervalTests, HlslSourceCompatibilityTests: unchanged.",
"AnalyticBodyMatchesFineMarch parts (1) and (3): unchanged; part (2) changes domain and owner only.",
"design.md :344-474 (Intervals up to the saving section, with the r3 frustum-ball paragraph minus the 6e-4 sentence) and every other section: unchanged."
],
"adds": [
"NoiseBoundTests.cs: BodyDepth; MarchLod's cutoff and (Transmittance, AtBody) return; ConvergedDepth; Run's slope-first draw, oracle mask, oracle march and the cutoff-0 agreement march; RunStats Oracle, OracleCeiling, Efficiency, ProbeOverhead, CullFraction, MaxOracleDifference, MaxLodDepthError, MaxRefDepthError; Report and ReportVoid fields; the (e) depth contract; (c)'s assertion per the ruling.",
"NoiseBoundTests.cs: VoidField.WeightBounds(z0, z1); Fact WeightBoundEnclosesOneSignedNoise; the header's mutation list updated.",
"design.md: the saving section rewritten from r4's run."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 14,
"end_line": 20
},
"change": "Header mutation list: w(z0) for w(z1) now fails WeightBoundEnclosesOneSignedNoise. Add: the dropped pre-body integration and LodStep x2 fail IntervalSkipHalvesEvaluations's depth agreement; BodyDepth without |dir| fails AnalyticBodyMatchesFineMarch."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 992
},
"change": "Beside Weights(z): public (float2 Coarse, float2 Fine) WeightBounds(float z0, float z1) => (WeightBound(CoarseWavelength, z0, z1), WeightBound(FineWavelength, z0, z1))."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1198,
"end_line": 1201
},
"change": "BodyFactor -> BodyDepth(Scenario field, Tile tile, float2 slope, float z, bool lod, Counts counts): the float optical depth Density x Extinction x (Grid[^1] - z) x sqrt(m.x^2 + m.y^2 + 1), with no exp."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1205,
"end_line": 1235
},
"change": "MarchLod(..., float cutoff, ref long steps, ref long bodySteps) returns (float Transmittance, float AtBody). Both loop guards use cutoff. At z >= body: AtBody = transmittance, then transmittance *= exp(-BodyDepth(...)), steps++, bodySteps++, return. Without a body, AtBody = final T."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1236
},
"change": "New ConvergedDepth(Scenario field, Tile tile, float2 slope, int stop, float h, bool lod): sum over cells c < stop of ceil(len / h) midpoint samples, Density(field, tile, slope, z, lod, throwaway) x Extinction x dz x |dir| in double. Every cell, not the mask's."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1237,
"end_line": 1265
},
"change": "RunStats:\n- Counts Oracle; OracleEmpty and OracleEmptySkipped; MaxOracleDifference; MaxLodDepthError; MaxRefDepthError.\n- OracleCeiling, Efficiency, ProbeOverhead and CullFraction as the spec's 'first' defines them.\n- Ceiling deleted.\nRatio and LodRatio are unchanged."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1270,
"end_line": 1305
},
"change": "Run, per tile:\n- MarchTile.\n- Draw N^2 slopes.\n- Oracle mask over all cells (mid-depth Density > 0 for any slope; throwaway Counts); count oracle-empty cells and those the mask skips.\n- Per ray: dense, masked and oracle marches (oracle into stats.Oracle; MaxOracleDifference).\n- With lod: the costed MarchLod (cutoff 0.02), then a cutoff-0 MarchLod and ConvergedDepth at h 0.5 into throwaway Counts, and tauRef with no early-out; update both depth errors."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1307,
"end_line": 1318
},
"change": "Report prints oracle ceiling, efficiency, cull fraction (with counts), probe overhead and max oracle |dT| in place of 'ceiling'. ReportVoid adds the depth errors of LOD and fixed-step against the converged march and their ratio; the end-of-ray |dT| stays."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1332
},
"change": "AnalyticBodyMatchesFineMarch part (2): depth = BodyDepth(field, tile, ray, grid[b], lod: true, counts); sum = the double sum over c in [b, last) of -Math.Log(Integrate(field, tile, ray, grid[c], grid[c + 1], lod: true, counts)); assert |depth - sum| <= 1e-4 x sum, naming field, b and both values. The doc comment says so."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1430,
"end_line": 1440
},
"change": "(a)-(d) loop: Assert MaxOracleDifference == 0 beside the existing <= 1e-3. :1437 records the ruling's (c) quantity (Efficiency, CullFraction or ProbeOverhead), the lower of warp 0 and D at N = 8, and the CullFraction floor value when the ruling has one."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1453,
"end_line": 1466
},
"change": "Headline:\n- MaxDifference == 0 stays.\n- :1455 is replaced by Assert MaxLodDepthError <= 2 x MaxRefDepthError, with both values in the message.\n- Under a floor option: Assert CullFraction >= 0.5 for (c) at both warps (hard).\n- Shortfalls: (c) per the ruling.\n- The contracts line names the quantity and prints the (e) depth errors."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1402,
"end_line": 1414
},
"change": "IntervalSkipHalvesEvaluations doc comment:\n- the (e) agreement is the converged depth, 2x the fixed-step's;\n- (c) per the ruling, citing it;\n- the oracle is defined;\n- 'within 0.02' leaves."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 304
},
"change": "After EnvelopeBoundEnclosesDensity: Fact WeightBoundEnclosesOneSignedNoise, as item 10 of 'first'. Its message names z0, z1, z, the octave, the weight and the bound."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 475,
"end_line": 512
},
"change": "The saving section is rewritten from r4's IV-REPORT lines (contents in item 11 of 'first'). The next paragraph, 'Consumers: ...' at :514, is kept."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 422,
"end_line": 423
},
"change": "Delete the sentence 'At the shipped scales (|c|_1 near 600, r near 5) it adds 6e-4.' Leave the rest of the paragraph."
}
],
"authority_map": {
"owner": "The saving test owns the proving-ground numbers and their oracles. The converged march (h = 0.5) is the accuracy oracle before the body. The oracle mask (cells some ray of the tile samples nonzero) is (c)'s ceiling oracle. BodyDepth is the one owner of the body's optical depth, in the march and in its pin. CultMath src is untouched. The operator owns the contracts: the rulings and the answer to wells-ceiling-oracle.",
"inputs": [
"the r3 fields, grids, seeds, tiles and marches at 3ac7250a",
"the ruling answering cultmath-tapes:question:wells-ceiling-oracle"
],
"outputs": [
"IV-REPORT rows: the LOD and fixed-step depth errors against the converged march; (c)'s oracle ceiling, efficiency, cull fraction and probe overhead",
"design.md's saving section, every figure from those rows"
],
"derived_state": [
"(c)'s ceiling is no longer derived from the pre-pass's own mask; it is derived from the oracle mask, which the pre-pass cannot move.",
"Dense/Masked is no longer a contract owner; Masked/(Masked+Probes) is printed as probe overhead (an owner only under overhead-plus-floor).",
"The fixed-step march is no longer the LOD march's accuracy oracle; it is a peer measured against the converged march, and its error sets the LOD's allowance (2x).",
"End-of-ray |dT| is display-only.",
"The body's optical depth is derived once, by BodyDepth; the test no longer re-derives it."
],
"forbidden_writers": [
"no assertion compares the LOD march with the fixed-step march directly, or after the body",
"AnalyticBodyMatchesFineMarch does not compute the body's depth itself; it calls BodyDepth",
"neither the oracle mask nor ConvergedDepth reads MarchTile's output",
"no change to src, the HLSL, the fields, grids, LodStep, seeds, the 0.2 weight or the 2x factor toward a contract",
"no figure in design.md that this run's IV-REPORT lines do not print"
],
"shared_paths": [
"MarchLod is one function for the costed march (cutoff 0.02) and the agreement march (cutoff 0)",
"one Run computes dense, masked, oracle, LOD and converged for every scenario; the headline and the grid print the same fields",
"the site's mirror of the march inherits the same check through follow-up site-ground-dense-check-blind-past-body"
],
"deletion_line": "The end-of-ray LOD assertion, BodyFactor, the test's own tau, RunStats.Ceiling, the (c) Ratio/Ceiling special case, design.md's r2 rows and the unprinted 6e-4 sentence all leave before their replacements are written. Net: about +170 / -60 in tests and docs, nothing in src. What that buys: (e) can now fail on a wrong pre-body march, the body depth can fail on a wrong |dir|, (c) can fail on a pre-pass that proves nothing, and the WeightBound mutant dies."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests -c Release green or skipped only by saving-2x-scenarios as the ruling implies (MeasureLipschitz/MeasureMonotonicity excluded by trait); paste counts and every IV-REPORT line",
"Yggdrasil: one tools/mutation-table.sh job at the final head with the six rows in 'first' item 13; the table pasted; every row CAUGHT by the named test except the wells row under oracle-target-only",
"No Stryker run and no Starfire job: no src or HLSL change in 3ac7250a..HEAD (negative check)"
],
"tests": [
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "At the shipped void the footprint march's optical depth at the body start is within 2x the fixed-step march's error of a converged march; the oracle march equals the dense march exactly; (c) is asserted per the ruling against a ceiling the pre-pass cannot move; (a) and (e) step contracts as before."
},
{
"name": "NoiseBoundTests.AnalyticBodyMatchesFineMarch",
"pins": "BodyDepth, the depth MarchLod uses, equals the summed fixed-step depth of the body cells within 1e-4 relative, in the depth domain (no underflow), so a body depth without |dir| fails on every tile; parts (1) and (3) unchanged."
},
{
"name": "NoiseBoundTests.WeightBoundEnclosesOneSignedNoise",
"pins": "Each octave weight at every sampled depth in [z0, z1] lies in WeightBounds(z0, z1), and w(z) n lies in iv_mul(bound, n) for a one-signed n, so a lower end above w(z1) fails."
},
{
"name": "NoiseBoundTests (rest) and CultMath.Tests (rest)",
"pins": "unchanged and green."
}
],
"negative": [
{
"pattern": "git diff --name-only 3ac7250a..HEAD",
"scope": "exactly NoiseBoundTests.cs and design.md under packages/cultmath."
},
{
"pattern": "git diff 3ac7250a..HEAD -- packages/cultmath/src packages/cultmath/shaders packages/cultmath/unity",
"scope": "empty."
},
{
"pattern": "BodyFactor|Ceiling =>|\\.Ceiling\\b|MaxLodDifference <= ",
"scope": "NoiseBoundTests.cs: 0 matches."
},
{
"pattern": "Math\\.Exp\\(-\\(double\\)tau\\)|var tau = density",
"scope": "NoiseBoundTests.cs: 0 matches (the test does not re-derive the body)."
},
{
"pattern": "1\\.41x|16\\.45|2\\.21x|1\\.34x|6e-4|does not, so the test skips|where the rays need them",
"scope": "packages/cultmath/docs/design.md: 0 matches."
},
{
"pattern": "Assert\\.Skip",
"scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios."
}
],
"operator": []
},
"estimate": {
"lines_added": 170,
"lines_removed": 60,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-wells-contract-vs-ceiling",
"cultmath-tapes:ruling:operator-analytic-wall-body",
"cultmath-tapes:ruling:operator-footprint-lod-march",
"cultmath-tapes:ruling:operator-measure-with-empty-space",
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-shipped-void"
],
"questions": [
"cultmath-tapes:question:wells-ceiling-oracle"
]
}

View file

@ -0,0 +1,146 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 1,
"title": "Interval ops: cultmath_iv_* over float2(lo, hi) in C# and HLSL with enclosure tests, a committed Lipschitz constant for snoise, and ball bounds for snoise and fbm",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "c49c76ee",
"depends_on": [],
"first": [
"Map: docs/cultmath-interval-ground-cut.md, sections 'Ownership', 'Interval culling along the ray' and 'The proving-ground contract'. Parked target: docs/cultmath-tape-target.md, sketch step 1 (:59-64) and 'Open before unparking' (:106-115). This cut is step 1 and nothing after it: no tape, no pruning, no min/max choice tracking, no affine arithmetic (map D1).",
"Common rules from the Asura cut map (Asura docs/cut-map.md, 'Common rules for every addition', :453-489): C# in packages/cultmath/src/CultMath as a partial of math (the Phacelle pattern: math.Phacelle.cs beside math.cs), HLSL in a new shaders/CultMath.Interval.hlsl included from CultMath.hlsl the way CultMath.Phacelle.hlsl is (:506), names prefixed cultmath_, both tracked HLSL copies one blob, bodies inside design.md's transformation list (:92-134), THIRD-PARTY-NOTICES untouched (nothing is ported; Fidget's ideas are used, not its code).",
"Representation: an interval is float2(lo, hi), lo <= hi, both finite. The empty interval does not exist; a caller that needs 'provably empty' tests hi < cutoff. Point intervals are iv_point(x) = float2(x, x).",
"Op set, each with the C# twin the mirror test demands: iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale(iv, float), iv_abs, iv_min, iv_max, iv_sqr, iv_sqrt (lo clamped at 0), iv_exp, iv_clamp(iv, float, float), iv_saturate, iv_smoothstep(float e0, float e1, iv) (monotone: endpoints), iv_lerp(iv, iv, float t). No others until a consumer names one.",
"Noise bounds: CULTMATH_SNOISE_LIPSCHITZ (static const float, provenance in the comment: measured max |snoise_grad| over 1e6 seeded points plus 1e4 gradient-ascent refinements, times 1.10; the test that measures it is committed as a slow, explicitly-run test and the constant is pinned by a fast test). iv_snoise_ball(float3 c, float r) = [snoise(c) - L r, snoise(c) + L r] intersected with [-1, 1]. iv_fbm_ball(c, r, octaves, lacunarity, gain) = the octave sum of iv_snoise_ball(c * f_i, r * f_i) scaled by a_i, octaves clamped to [0, 16] like fbm_grad.",
"Write the enclosure property first, as the shared harness: for each op, 10,000 seeded random intervals (widths from 1e-4 to 10, centres in [-20, 20]) and 16 points inside each; assert f(x) in op(I) with no tolerance (the bound is computed in the same float32 arithmetic; where rounding could exclude an endpoint, widen the op by one ulp in the implementation, not the test, and say so in design.md). For the ball bounds: 2,000 balls, r in [1e-3, 2], 64 points inside each.",
"The saving test: a representative coverage field (one snoise octave at F0 = 0.35, cutoff 0.25) and 1,000 seeded rays through a slab of depth 12; march once at the dense step and once with the adaptive interval march from the map (Δ0, ΔMAX = 8 Δ0, ΔDENSE = Δ0/2, grow on skip, halve on maybe); count snoise evaluations; assert the integrated transmittance agrees within 1e-3 and the evaluation count is at least 2x lower. Print both counts and the ratio; the cut report quotes them.",
"The warp enclosure test (the site's march rests on it): for 2,000 seeded (centre c, radius r, warp bound D) triples and random flow vectors with |flow| <= D, every warped point c' = x + flow (x in the ball of radius r) lies in the ball of radius r + D, and snoise(c') lies in iv_snoise_ball(c, r + D). This is a property of the existing bound, not a new function; it is written as a named test so the domain-warp use is pinned and documented in design.md.",
"The saving test's field is domain-warped: coverage = sum over two phases of snoise(p + flow * (phase_k - 0.5) * PERIOD) with a bounded flow, matching the map's field shape, so the interval radius includes D.",
"Tightness: a committed test prints the mean width ratio (interval width / true range over the 64 points) for iv_snoise_ball at r = 0.05, 0.25, 1.0 and for iv_fbm_ball at 4 octaves. No assertion beyond finiteness; the number is for the report and for a later affine cut to beat.",
"Verification on Yggdrasil: dotnet test from packages/cultmath/tests/CultMath.Tests; Stryker --since:c49c76ee from a plain clone. Starfire: FXC cs_5_0 kernel calling every iv_* function and both ball bounds; dxc -T lib_6_3 -HV 2021. Detached with a log, polled in the foreground; never a background shell task. Numbers in comments or docs come from committed tests only.",
"If glsl-lowering has merged to main before this branch does, rebase and regenerate shaders/CultMath.glsl (CULTMATH_WRITE_GLSL=1) so the pin test is green on the merge; if not, the lowering cut regenerates after this one. Either order works because the GLSL is derived."
],
"deletes": [],
"keeps_moves": [
"snoise, snoise_grad, fbm_grad, ridged_grad, cellular, phacelle: untouched. The bounds call snoise (value) only; snoise_grad is used by the Lipschitz measurement test, not by any bound (map D2).",
"design.md's invariant 8 section (136-) is unchanged; intervals are not value-and-gradient primitives and do not claim the float4(grad, value) layout.",
"HlslMirrorTests' ban on erosion kernels stands; nothing named interval touches it."
],
"adds": [
"packages/cultmath/src/CultMath/math.Interval.cs: public static partial class math with iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale, iv_abs, iv_min, iv_max, iv_sqr, iv_sqrt, iv_exp, iv_clamp, iv_saturate, iv_smoothstep, iv_lerp, iv_snoise_ball, iv_fbm_ball, and the constant SNOISE_LIPSCHITZ; each with a doc comment stating the enclosure it guarantees and the ulp widening where one is needed.",
"packages/cultmath/shaders/CultMath.Interval.hlsl: the HLSL mirrors with the same names prefixed cultmath_, and CULTMATH_SNOISE_LIPSCHITZ as static const; included from CultMath.hlsl after the Phacelle include (:506). Both tracked HLSL copies.",
"tests/CultMath.Tests/IntervalTests.cs: the enclosure harness over every op; iv_mul sign cases (all four quadrant combinations and a zero-straddling factor); iv_sqrt with lo < 0; iv_abs straddling zero gives [0, max(|lo|,|hi|)]; iv_smoothstep monotonicity; iv_min/iv_max against the pointwise min/max; a mutant that shrinks any bound by one ulp dies (hand-mutation list in the file header for the float arithmetic Stryker cannot reach, as in 2a-ii-a).",
"tests/CultMath.Tests/NoiseBoundTests.cs: iv_snoise_ball and iv_fbm_ball enclosure; LipschitzConstantPinsSampledGradients (fast, 1e5 points); MeasureLipschitz (slow, explicit trait, prints the max and the refined max); TightnessReport; the saving test (InvalSkipHalvesEvaluations) with its printed counts.",
"packages/cultmath/docs/design.md: a section 'Intervals' after the invariant 8 section: representation, the op list, the ulp-widening rule, the Lipschitz provenance and margin, what the ball bounds guarantee and what they do not (nothing about tapes), and that the site's ground shader and Aetheria's nebula raymarch are the consumers.",
"packages/cultmath/README.md: one paragraph under the noise entry naming the interval family and its purpose (empty-space culling with a proof)."
],
"file_changes": [
{
"location": { "path": "packages/cultmath/shaders/CultMath.hlsl", "line": 506 },
"change": "add #include \"CultMath.Interval.hlsl\" after the Phacelle include; both tracked copies."
},
{
"location": { "path": "packages/cultmath/src/CultMath/math.cs", "line": 602 },
"change": "no change to snoise; a comment pointer from snoise to SNOISE_LIPSCHITZ in math.Interval.cs so the next kernel change knows the constant must be re-measured."
},
{
"location": { "path": "packages/cultmath/docs/design.md", "line": 136 },
"change": "new 'Intervals' section after invariant 8."
},
{
"location": { "path": "packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs", "line": 348 },
"change": "none expected; confirm the mirror resolves the second #include and that every cultmath_iv_* finds its C# twin (a mirror without a twin fails Assert.Contains MirrorOnly)."
}
],
"authority_map": {
"owner": "CultMath owns interval arithmetic and the noise bounds in C# and HLSL (and GLSL by derivation). The Lipschitz constant is owned by the committed measurement test; the kernel of snoise owns its value.",
"inputs": [
"the existing snoise(float3) value function; snoise_grad for the measurement test only",
"seeded random points and intervals in tests"
],
"outputs": [
"cultmath_iv_* and the two ball bounds, each enclosing its function over its input region",
"three printed numbers for the report: the Lipschitz max, the tightness ratios, the evaluation saving"
],
"derived_state": [
"CULTMATH_SNOISE_LIPSCHITZ is derived from the snoise kernel; a kernel change must re-run MeasureLipschitz and re-pin",
"shaders/CultMath.glsl (if the lowering has landed) is derived and regenerated, never edited"
],
"forbidden_writers": [
"no interval op without the enclosure test and a C# twin",
"no bound that reads snoise_grad; no value-only/gradient twin of any iv_* function",
"no tape, bytecode, pruning, choice flag or affine form in this cut; names containing tape, prune or affine do not appear",
"no number in a comment or design.md that a committed test did not print"
],
"shared_paths": [
"the same IntervalTests enclosure harness runs every op; a new op is one entry in its table",
"the existing HLSL mirror test covers every HLSL interval function with no new comparison path"
],
"deletion_line": "Nothing is deleted. The liability is one C# file, one HLSL file, two test files and a doc section; it buys the invariant intervals-enclose for every GPU consumer, and it is step 1 of the parked tapes campaign landed in advance."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green (MeasureLipschitz excluded by trait); paste counts",
"Yggdrasil: dotnet stryker --since:c49c76ee from a plain clone; 0 survivors in math.Interval.cs except listed equivalents; hand mutations for float3 arithmetic listed and killed",
"Starfire: FXC cs_5_0 kernel over every new function; dxc -T lib_6_3 -HV 2021 on both HLSL copies"
],
"tests": [
{
"name": "IntervalTests.EveryOpEnclosesItsPoints",
"pins": "intervals-enclose for every op, no tolerance."
},
{
"name": "NoiseBoundTests.SnoiseBallEnclosesPoints / FbmBallEnclosesPoints",
"pins": "the ball bounds enclose snoise and fbm over 2,000 balls x 64 points."
},
{
"name": "NoiseBoundTests.WarpedPointsStayEnclosed",
"pins": "a domain warp bounded by D is enclosed by enlarging the ball radius by D; the site's interval march is sound."
},
{
"name": "NoiseBoundTests.LipschitzConstantPinsSampledGradients",
"pins": "no sampled |snoise_grad| exceeds the constant; a constant lowered by 1% fails."
},
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "the adaptive march needs <= half the evaluations of the dense march for the same transmittance within 1e-3."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "every cultmath_iv_* and both bounds match C# bit for bit."
}
],
"negative": [
{
"pattern": "tape|prune|affine|choice",
"scope": "packages/cultmath/src/CultMath/math.Interval.cs and shaders/CultMath.Interval.hlsl: 0 matches (case-insensitive)."
},
{
"pattern": "snoise_grad|fbm_grad",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches; the bounds use values only."
},
{
"pattern": "spherical_erosion|AdvancedErosionFilter",
"scope": "packages/cultmath: 0 matches (HlslMirrorTests' standing rule)."
},
{
"pattern": "git diff --name-only c49c76ee..HEAD",
"scope": "only packages/cultmath/** (and shaders/CultMath.glsl if regenerated); no other package or src tree."
}
],
"operator": []
},
"estimate": {
"lines_added": 900,
"lines_removed": 0,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [],
"questions": []
}

View file

@ -0,0 +1,246 @@
{
"campaign": "cultmath-tapes",
"cut": "site-ground",
"revision": 3,
"title": "Site ground r3: A cloud volume carved by one negative pseudo-Gaussian sphere, a sun at the hollow's bottom lights the walls and orbits, the camera follows low inside; pre-pass, camera-aware TAA",
"repo": "GameCult/gamecult-site",
"branch": "main",
"base": "535d170",
"depends_on": [
"site-masthead:cut_spec:wash-home-only.r1",
"cultmath-tapes:cut_spec:glsl-lowering.r2",
"cultmath-tapes:cut_spec:interval-ops.r3"
],
"first": [
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'Pass 3' (the void scene, the composed bound, the pre-pass, the budget), then 'The shader design' for what pass 3 does not restate (sampled wash, palette input, pausing, contrast). Rulings in force: cultmath-tapes:ruling:operator-ground-void-brush (the scene), operator-tile-amortized-march (the march), site-masthead:ruling:operator-chromeless-column; wash-home-only's authority map. Re-pin base to main after wash-home-only lands. Stardust and DoF stay the next cut; leave the named composite slot. r2's token, readback, controller, vendoring, parity and fallback items stand unless this revision names them.",
"The original, read once (Aetheria at e6181c11, 2021-03-13, Assets/Scenes/Main Menu.unity; Prefabs/RPG/Planets/Sun.prefab; Materials/Brushes/Gravity Well.mat, Main Menu Sun Fog Tint.mat, Main Menu Ambient Fog Tint.mat, Sector Boundary Fog.mat; Materials/Fog Marching Main Menu.mat; Shaders/Raymarching/VolumeRender.shader at that commit; Shaders/Brushes/PowerBrush.shader). What survives, in the original's units: the brush is PowerBrush (1 - x^2)^16 with depth 40 over radius 256 (half depth at r = 52, pseudo-Gaussian sigma about 45), the sun's own gravity-well child; a wide boundary bowl (depth 75, power 0.25, radius 1500) is flat across the scene and acts as a constant 75; floor offset -16.46, floor blend 39.6, so the fog top is at y = -91 outside the void and dips to -131 at the sun; the sun sphere (radius 12.5) sits at y = -130, the bottom of the void.",
"What survives (continued): the sun is a child of 'Center', which rotates 5 degrees per second about y (72 s period), at orbit radius 175; the camera is a Cinemachine vcam following the sun (framing transposer, distance 150, damping 10, handheld noise amplitude 1 at frequency 0.25) and looking at 'Look Target' (0, -130, 0), the orbit centre at the sun's depth; the saved camera sits at y = -27, 64 above the fog top. The lighting was authored, not marched: the sun's 'Fog Tint' brush (1 - x^2)^3, depth 2, radius 128, colour (1, 0.4, 0.1) orange, painted into the tint map, times a scene-wide ambient tint (0.18, 0.59, 1) blue, depth 0.01. Stardust: 256^2 particles, spacing 2, floor -60, ceiling 0, height exponent 800 / (ceiling + wellDepth) so particles climb higher where the well is deeper (the void fills with stardust). Noise: strength 150, frequency 0.01, safety 50. Not surviving: the 2021 d() details, the tint map's runtime contents, the video.",
"The field (ruling operator-void-true-cavity: a spherical hollow inside a cloud volume that surrounds it on every side, not the 2021 bowl). Units are the original's. Outer volume: an unbounded uniform fill, chosen over a shell because every ray then ends in a wall and the march terminates by opacity, never by a far plane; Z1 = |camera - c_v| + Rc + 6/K (614 at the shipped point) bounds the pre-pass range and is past opacity for every ray, so the analytic body step below always terminates. The carve is the pseudo-Gaussian brush g(d) = (1 - (d / Rc)^2)^e, d = |p - c_v|, density = K max(0, 1 - CARVE g(d')), authored by two knobs: the hollow radius Rh (density exactly 0 inside; Rh = Rc sqrt(1 - CARVE^(-1/e))) and the ramp width S (density full at Rh + S; set by the exponent: e = 4 gives S = 1.3 Rh, e = 0.25 gives S = 0.12 Rh).",
"Shipped (Q9 in the map, option (a) pending the operator): Rh = 198, S = 50, so Rc = Rh + S = 248 and e = -ln CARVE / ln(1 - (Rh / Rc)^2) = 0.40; CARVE = 1.5, K = 1/30: a wall is opaque within about 150 units of full density. The ramp and the noise fade band are both S wide; nothing void-shaped is authored twice (interval-ops r3, finding shipped-void-two-scenes). Noise displaces the wall only: fade(d) = smoothstep(Rh - S, Rh, d), d' = d + A fade(d) n(warp(p)), A = 20, F0 = 0.01.",
"Scene motion. The sun orbits the origin at radius 175 in xz with period 72 s (5 degrees per second, the original's Center pivot). The cavity centre is c_v = sun + (0, Rh - 12.5, 0): the sun sphere (radius 12.5) rests on the hollow's bottom and the hollow moves with it. The camera follows the sun 120 behind it along the orbit's tangent and 45 above it, so it sits 186 from c_v, 12 inside the hollow (the handheld drift is 2 units, two low-frequency snoise on time), low in the cavity, looking at the sun's position plus (0, 60, 0) so the lit lower wall and the dark upper wall share the frame. The 2021 follow distance 150 and height 25 do not fit a hollow a camera can stand in; 120 and 45 are the re-derivation, and the report says so. worldAt(t) in ground.js is the one owner of sun, cavity and camera, uploaded as uniforms; the shaders never integrate motion.",
"The flow field, authored around the void: flow(xz) = SWIRL * tangent(xz - sun.xz) * (1 - (r / Rw)^2)^2 + GLOBAL * (snoise(xz Ff + o1), snoise(xz Ff + o2)) (the swirl is largest mid-slope and zero at the rim and the centre; the global term is r2's). The flow texture is a world-space window of side 4 Rw centred on the sun, quarter of the canvas resolution, re-rendered every frame; warp, hue, motion vectors and (next cut) the stardust read it, so D4 holds with a moving scene. D = FLOW_MAX * PERIOD / 2 as before, with FLOW_MAX the authored maximum of the swirl plus the global amplitude.",
"The march (tile pre-pass, interval-ops r2's design). Pre-pass at W/8 x H/8: per tile, the loop over the slab's cell ranges (uniform cells, StepDense = min(10, S / 4) = 10, 62 cells to Z1 = 614 along +view depth; the mask has 96 bits): the envelope probe first (exact box: y and xz ranges by cultmath_iv_mul of the depth and slope intervals, d's interval is the box's least and greatest distance to c_v, exact; g, fade and the carve are monotone in it), then the noise ball (cultmath_iv_frustum_ball with D, cultmath_iv_snoise_ball) only when fade.hi > 0 and L r F0 leaves room to prove anything; writes the 96-bit mask of cells not proven empty to an RGBA32UI texel's first three words, and in the fourth word the body-start cell: the first cell whose own bound is a positive point and whose slice to Z1 is one too (the envelope with the free noise interval proves density exactly K for the rest of the range), 0xFFFFFFFF when none (interval-ops r3, ruling operator-analytic-wall-body).",
"(March, continued.) Main pass: texelFetch the mask, then march the unmasked range with the footprint step Delta(z) and the octave weights (next item), the per-frame sample count capped by the budget S_budget and the start jittered so TAA completes what one frame does not; masked cells are skipped exactly; at the body-start cell take one analytic Beer-Lambert step, T *= exp(-K (Z1 - z) |dir|) with K the field's pointwise envelope value there (no probe in the main pass), and stop; integrate transmittance and the depth-weighted mean distance, apply the light. Every tile's interior cells retire in a few envelope probes and its wall cells are marked; there are no sky tiles. Behind a fully transmissive pixel the resolve shows --gamecult-nebula-base; by construction only an unfinished ray (budget exhausted before the wall, measured, must be under 1%) reaches it.",
"Footprint LOD (the operator's challenge, map 'Footprint LOD'): theta_px = 2 tan(fov/2) / H; the footprint at distance z is z theta_px. Step: Delta(z) = max(k z theta_px, min(S / 4, lambda_fine(z) / 2)), k = 4: never finer than four footprints, otherwise the finer of the two content limits (four samples across the ramp, two per wavelength of the finest live octave). Octaves: octave i of wavelength lambda_i carries weight w_i(z) = 1 - smoothstep(lambda_i / (2m), lambda_i / m, z theta_px), m = 4: one smooth blend per octave, no popping; TAA hides per-frame sparsity. The weights are part of the field, so the bound encloses the truncated field exactly: the noise interval over a slice is the sum over octaves of iv_mul([w_i(z1), w_i(z0)], iv_snoise_ball(c F_i, r F_i)), w_i monotone in z. At the shipped parameters (wavelengths 100 and 25, z <= 2 Rc) every weight is 1 in the frustum and the content limits set the step; the footprint bites when S < 4 k z theta_px.",
"Where the scene sits and what to turn (Imagination's model, scratchpad cavity_model.py; the test and the browser confirm): interval-ops r2 measured the shipped point at 16.45 LOD steps and 26.6 snoise per pixel against 36.3 fixed steps (2.21x); r3's analytic body is predicted to bring the steps to about 12.5 (2.6-3.3x), snoise unchanged; the wall's step is set by S / 4 and the detail half-wavelength, not by distance; the browser confirms or refutes. Cheaper without losing the mood, in order: (1) a lower per-frame S_budget, which LOD and the mask make nearly lossless (S_budget = 4 converges in about three frames at the walls); (2) a thinner ramp S (S = 10 is 3.2x on cost but reads as a surface; 25-50 keeps the fog); (3) a longer detail wavelength or no detail octave near the far wall (the 12.5-unit half-wavelength is the floor for S >= 50); (4) a larger hollow (Rh = 1000: 2.5x) costs nothing per pixel and changes the composition. Camera offset barely matters at these scales.",
"Lighting, the cheapest version that keeps it moody (the original's own mechanism, made 3D): at each sample, light = SUN_COLOUR * (1 - (d / Rl)^2)^3 for d = |p - sun| < Rl, Rl = Rh = 198 (the original's 128 scaled with the hollow), plus AMBIENT * ambientTint; colour = density * (light + ambient) * hue(flow). No light march: zero extra snoise, one length and one pow per sample. The lower wall within Rl of the sun glows and the upper wall stays dark because the pulse is zero beyond Rl, which is the moody split; the sun inside the hollow is occluded by nothing, so the sun itself is a small additive disc drawn in the resolve at its projected position, occluded by the accumulated transmittance along its ray. Not in this cut, measured before it is ever added: one envelope-only transmittance sample toward the sun per primary sample (self-shadowing by the bowl's geometry), about +25% march cost; it is desktop-only if ever.",
"TAA with a moving camera. ground.js uploads this frame's and the previous frame's view-projection. For each pixel the resolve reconstructs the world point at the march's depth-weighted mean distance (written by the main pass into a second history channel, R16F), moves it back by the field motion (p + flow(p) * dt: the pattern moves at -flow), projects it with the previous VP and fetches history at that uv. The same 3x3 min/max clamp, alpha = max(1/frames, 1/32), out-of-bounds alpha 1; history reset on resize, first frame and budget step. Transmittance, colour and the depth channel are all reprojected by the one motion vector. ?ground=debug-mv shows |mv| with the camera moving and the flow frozen, and with the flow moving and the camera frozen, so both terms are seen separately.",
"Stardust distribution and the sun disc are the next cut (site-stardust), authored to the void: particles spawned in the bowl's volume with the original's height rule (more headroom where the well is deeper), lit by the same pulse, moving with the swirl. This cut leaves the composite slot and the depth history they need.",
"Fit with the tokens. Inputs: --gamecult-nebula-0 (orange 255 138 42) is SUN_COLOUR; --gamecult-nebula-2 (sky 89 183 255) is the ambient tint; --gamecult-nebula-1 (violet 109 96 255) is the cloud hue's second pole; --gamecult-nebula-base the sky. Outputs from the 8x8 readback as r2, with one change: --gamecult-light-0-at is written from the sun's projected screen position (quantised to 1%, at most 2 Hz), so the CSS wash's main light follows the sun while the shader runs; --gamecult-light-0 and -a come from the texel there; lights 1 and 2 keep their seed positions and sample their colours. When the canvas is absent the seeds paint a still of the same composition (the seed light-0-at is the sun's mean screen position).",
"Prove in the browser pane, in addition to r2's list: with ?ground=debug the pre-pass's mean probes per tile and the fraction of cells masked, and the mean snoise per pixel and the fraction of samples placed in the wall ramp against a dense-reference frame (?ground=dense: no pre-pass, S samples over the whole slab; the two converged frames' mean luminance agree within 1/255; the masked frame's per-frame variance at the walls is lower, printed); the camera reprojection test above; the orbit (after 36 s the sun's screen position has moved half an orbit and the TAA shows no smear at the walls); the contrast clamp with the lit cavity (max luminance <= 0.035 still holds; the sun disc is exempt only inside its own radius, which the clamp test masks and reports)."
],
"deletes": [
{
"path": "site/quartz/styles/custom.scss",
"lines": 6,
"note": "ritual body block 119-124: its own background goes; its seeds live under :root:has(body[data-slug]) (r2)."
},
{
"path": "site/quartz/styles/custom.scss",
"lines": 9,
"note": "body 10-27: the literal colours and stops become token references (r2)."
}
],
"keeps_moves": [
"r2's tokens, sampled wash, controller, vendoring (now two files: the site vendors CultMath.glsl only, MIT; the parity run concatenates CultMath.Phacelle.glsl for the phacelle family), parity flags, fallbacks and the contrast clamp: unchanged except where this revision names them.",
"--gamecult-wash and the index override from wash-home-only: untouched.",
".page::before, the tour/Pitch VN ground, the Thing deck's .hero-bg, the masthead, the engine: untouched.",
"The CSS wash stays the ground when the canvas is absent, not yet drawn, or removed by the controller."
],
"adds": [
"site/quartz/components/GameCultGround.tsx: as r2.",
"site/quartz/static/ground/ground.js (~500 lines): r2's passes plus worldAt(t) (sun, void, camera), the two view-projections, the pre-pass, the mask texture, the depth history, the sun-disc composite and the light-0-at writer.",
"site/quartz/static/ground/flow.frag.glsl (world-space window, swirl plus global), prepass.frag.glsl (the tile loop, mask and body-start out), cloud.frag.glsl (masked sampling, light, depth out), taa.frag.glsl (camera plus flow reprojection), downsample.frag.glsl.",
"site/quartz/static/ground/CultMath.glsl and glsl-parity.json, vendored from CultLib at interval-ops r3's head or later (cultmath_iv_frustum_ball with its float32 widening present).",
"scripts/ground/check-cultmath-glsl.ps1: the vendoring drift check.",
"custom.scss and docs/brand-design-language.md: as r2, with the void composition named in the brand doc's ground paragraph."
],
"file_changes": [
{
"location": {
"path": "site/quartz/styles/custom.scss",
"line": 4,
"end_line": 8
},
"change": ":root block: the fifteen ground seeds and --gamecult-ground-mean; light-0-at seeded at the sun's mean screen position."
},
{
"location": {
"path": "site/quartz/styles/custom.scss",
"line": 10,
"end_line": 31
},
"change": "body: four nebula input tokens; radials and the linear gradient reference the ground tokens (r2)."
},
{
"location": {
"path": "site/quartz/styles/custom.scss",
"line": 103,
"end_line": 124
},
"change": "ritual block: background declaration deleted; :root:has(body[data-slug]) carries its seeds and nebula inputs (r2)."
},
{
"location": {
"path": "site/quartz.layout.ts",
"line": 19
},
"change": "header: [GameCultThemeLock(), GameCultGround(), GameCultMasthead()]."
},
{
"location": {
"path": "docs/brand-design-language.md",
"line": 69,
"end_line": 84
},
"change": "wash section rewritten: the void scene, the sampled wash, the seeds, the nebula inputs and what each drives."
}
],
"authority_map": {
"owner": "The shader owns the ground. ground.js owns worldAt(t) (sun, void, camera), the flow texture, the pre-pass mask, the march, the TAA, the readback and the sampled tokens; custom.scss owns the four nebula inputs, --gamecult-wash and the seeds; CultLib owns every noise and interval function; the composed bound in prepass.frag.glsl is built from cultmath_iv_* only.",
"inputs": [
"the four nebula tokens and --gamecult-wash (computed on body)",
"prefers-reduced-motion, visibilityState, canvas size, devicePixelRatio, elapsed time",
"measured GPU frame time (timer query, fence, or rAF cadence)",
"the shader's own 8x8 readback and the sun's projected position",
"the vendored CultMath.glsl at its pinned CultLib commit"
],
"outputs": [
"one opaque moving canvas under the page, luminance <= 0.035 outside the sun disc",
"the fifteen ground tokens and --gamecult-ground-mean on :root inline, at most 2 Hz; light-0-at follows the sun",
"debug, dense-reference and parity readbacks (console, flag only)"
],
"derived_state": [
"sun, void and camera positions are derived from time by worldAt(t); nothing integrates motion frame to frame",
"the tile mask is derived each frame from that frame's camera and the field; it is never carried across frames",
"the flow texture is the one owner of motion: warp, hue, motion vectors and the stardust read it",
"the TAA history (colour, transmittance, depth) is a cache reprojected by camera and flow; reset on resize, first frame and budget step",
"the ground tokens are derived from the readback while the shader runs; the seeds are the fallback; the shader never reads them",
"S and dpr are derived from measurement within the session; the sessionStorage token set is a cache"
],
"forbidden_writers": [
"no colour, alpha, position or radius literal in ground.js or any GLSL file; the scene's dimensions (H0, Dw, Rw, F, S, Rl, orbit radius, period, camera offset) are named constants in one place in ground.js, uploaded as uniforms",
"no sample placed in a masked cell; no probe in the main pass; no early-out of the pre-pass on transmittance",
"no light march in this cut; the light is the analytic pulse",
"no read of the ground output tokens by the shader or ground.js except the seed read before the first readback",
"no userAgent, platform or maxTouchPoints; no class or attribute set on body or html; no second wash token; no second flow evaluation; no noise or interval function defined in the site's GLSL",
"no synchronous readPixels to client memory; one requestAnimationFrame scheduler; localStorage never"
],
"shared_paths": [
"every page paints one canvas and one token set; the ritual essays differ only in seeds and inputs",
"slow device, hidden tab, reduced motion, missing WebGL2 and missing half-float all pass through the one controller",
"the 8x8 readback and the sun projection are the only paths from the shader to CSS",
"the pre-pass and the main pass read the same uniforms from the same worldAt(t); the dense-reference mode differs only by skipping the mask"
],
"deletion_line": "As r2: the ritual block's own background and the body literals go in the same commit the tokens are declared. Nothing else in the engine or content changes. Against pass 2's design this revision removes the static camera and the everywhere-clouds field; it adds one pre-pass, one mask texture, one depth history channel and the scene function."
},
"verification": {
"builds": [
"Deploy Quartz green on the landing commit: gh run list -R GameCult/gamecult-site -w \"Deploy Quartz\" -L 1",
"scripts/ground/check-cultmath-glsl.ps1 exits 0 against a CultLib checkout at the pinned commit",
"the built stylesheet contains rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))) verbatim and the ritual :root:has rule"
],
"tests": [
{
"name": "browser pane, /?ground=debug: max relative luminance over the canvas outside the sun disc after 32 frames <= 0.035",
"pins": "text contrast with the lit cavity."
},
{
"name": "browser pane, /?ground=debug vs /?ground=dense: converged mean luminance within 1/255; steps and snoise per pixel printed for both, debug at most half of dense's steps; mask fraction printed",
"pins": "the mask culls only provably empty cells; the shipped march takes at most half the fixed-step steps; the saving on the shipped scene is a number."
},
{
"name": "browser pane, /?ground=debug-mv: |mv| non-zero with the camera moving and flow frozen, and with flow moving and camera frozen; zero under reduced motion",
"pins": "both reprojection terms are live and separable."
},
{
"name": "browser pane, / for 40 s: the sun's screen position moves about half an orbit; no history smear at the walls (3x3 clamp); light-0-at on :root follows within 2 s",
"pins": "the moving scene converges and the CSS wash follows the sun."
},
{
"name": "browser pane, /?ground=parity: every fixture family within its tolerance class on WebGL2, cultmath_iv_frustum_ball exact; numbers and GPU string in the report",
"pins": "GLSL = C# on a device, including the tile ball."
},
{
"name": "browser pane, /?ground=parity-wash and a ritual essay: canvas vs CSS seed wash within 2/255 at 16 points with density 0 and the sun disc off",
"pins": "the seeds and the shader's base agree."
},
{
"name": "reduced motion, hidden tab, WebGL2 disabled, sessionStorage cleared: as r2",
"pins": "every fallback goes through the controller."
},
{
"name": "git diff base..head --stat: custom.scss, quartz.layout.ts, docs/brand-design-language.md, the new component, static/ground/*, scripts/ground/* only",
"pins": "no engine, content or masthead change."
}
],
"negative": [
{
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(|\\b(255|138|109|183)\\b",
"scope": "site/quartz/static/ground/*.js and *.glsl: 0 matches."
},
{
"pattern": "gamecult-ground-|gamecult-light-",
"scope": "site/quartz/static/ground/*.glsl: 0 matches; in ground.js only inside the token writer and the seed read."
},
{
"pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|setInterval|classList|setAttribute\\(",
"scope": "site/quartz/static/ground/ground.js: 0 matches."
},
{
"pattern": "cultmath_iv_",
"scope": "cloud.frag.glsl, taa.frag.glsl, flow.frag.glsl: 0 matches; the bound lives only in prepass.frag.glsl."
},
{
"pattern": "float cultmath_|vec4 cultmath_|float snoise|hash\\(",
"scope": "every site .frag.glsl: 0 matches; cultmath_* is defined only in the vendored CultMath.glsl."
},
{
"pattern": "Mozilla|MPL|cultmath_phacelle",
"scope": "site/quartz/static/ground/CultMath.glsl: 0 matches; the site vendors the MIT file."
},
{
"pattern": "radial-gradient\\([^)]*rgba?\\([0-9]",
"scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches."
},
{
"pattern": "gamecult-titlebar|gamecult-nav-link|sidebar|composite|GameCult-Quartz",
"scope": "git diff base..head changed lines: 0 matches."
}
],
"operator": [
"open / on the desktop and on the phone; say whether the hollow, the sun's orbit and the camera's height read as the old main menu's mood (uniforms: A, SWIRL, Rl, CARVE, the camera offset)",
"on the phone with ?ground=debug, read the logged GPU ms per pass and the controller's final S/dpr; say whether it got warm after five minutes",
"say whether the analytic light is moody enough or the self-shadowing sample (desktop only) should be tried",
"navigate / -> /Blog -> back; say whether the ground is continuous (Q5)"
]
},
"estimate": {
"lines_added": 1250,
"lines_removed": 15,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [
"GLSL ES 3.00 shader files in static/ground",
"vendored glsl-parity.json",
"one sessionStorage key holding the last sampled token set",
"an RGBA32UI tile-mask texture and an R16F depth history (GPU-side only)"
],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-ground-void-brush",
"cultmath-tapes:ruling:operator-void-true-cavity",
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-analytic-wall-body",
"cultmath-tapes:ruling:operator-split-phacelle",
"site-masthead:ruling:operator-chromeless-column"
],
"questions": [
"Q4 in the map: palette input tokens (a, recommended)",
"Q5 in the map: carry the last sampled wash across page loads (a, recommended)",
"Q9 in the map: the one shipped void (a, recommended: Rh 198, S 50, Rc 248, e 0.40); ruled in interval-ops r3's fork, this spec follows it"
]
}

View file

@ -0,0 +1,206 @@
{
"campaign": "cultmath-tapes",
"cut": "site-ground",
"revision": 2,
"title": "Site ground: a per-pixel WebGL2 nebula under every page, flow-warped, interval-culled, TAA-reprojected, budget-controlled by measurement; the CSS wash is sampled from the result, seeds fall back",
"repo": "GameCult/gamecult-site",
"branch": "main",
"base": "535d170",
"depends_on": [
"site-masthead:cut_spec:wash-home-only.r1",
"cultmath-tapes:cut_spec:glsl-lowering.r1",
"cultmath-tapes:cut_spec:interval-ops.r1"
],
"first": [
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'The shader design' (field, culling with the warp, TAA motion-aware, sampled wash, palette input), 'Budget', D4-D8, Q4 and Q5. Rulings in force: site-masthead:ruling:operator-chromeless-column; wash-home-only's authority map (no second wash token, no class toggled by script, no page rule setting a radial alpha). Re-pin base to main after wash-home-only lands; line numbers below are from 535d170; re-read by selector. Stardust and DoF are the next cut: leave the resolve pass with a named, empty composite slot for them (a uniform-gated branch is not needed; a comment and a function stub are).",
"Aetheria reference, read once, copy nothing: Volumetric.cginc:165-221 (two-phase flow-map warp, tri2 cross-fade), :130-135 (globalFlow); Raymarching/CloudShader.shader:83-104 (integration) and :261-314 (what not to do: gamma 0.5 clip, fixed 0.05 blend, packed depth, the density2 bug at :311).",
"Engine facts to confirm before writing: how GameCult-Quartz bundles a component's afterDOMLoaded and whether a dynamic import() of /static/ground/ground.js is allowed, else emit a module script tag; site/quartz/static/** served at /static/**; enableSPA false (quartz.config.ts:15). Local build: scripts/quartz/quartz.ps1 dev.",
"Tokens. Inputs (Q4 a), on body in custom.scss beside --gamecult-wash: --gamecult-nebula-0/1/2 (\"255 138 42\", \"109 96 255\", \"89 183 255\") and --gamecult-nebula-base (#07111a). Outputs with seeds, on :root: --gamecult-ground-0/1/2 (#03070d #07111a #09141f), and for N in 0..2 --gamecult-light-N (rgb triple), -a (0.18/0.16/0.14), -at (\"78% 14%\"/\"18% 10%\"/\"52% 0%\"), -r (18%/24%/28%), plus --gamecult-ground-mean (#07111a). The body background becomes radial-gradient(circle at var(--gamecult-light-0-at), rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))), transparent var(--gamecult-light-0-r)) x3 over linear-gradient(180deg, var(--gamecult-ground-0) 0%, var(--gamecult-ground-1) 44%, var(--gamecult-ground-2) 100%).",
"Ritual essays: their seeds go under :root:has(body[data-slug=\"Blog/...\"]) for the three slugs (yellow 255 178 30 .15 at 78% 8% 24rem; cobalt 49 88 255 .22 at 18% 4% 30rem; violet 91 54 184 .19 at 44% 20% 34rem; stops #030414 0%, #070b24 42%, #040514 100%; nebula inputs likewise overridden to yellow/cobalt/violet and base #050614). The ritual block's own background declaration is deleted; it inherits body's.",
"ground.js owns: WebGL2 context on #gamecult-ground (alpha:false, antialias:false, powerPreference 'low-power'); capability check (WebGL2, EXT_color_buffer_half_float or float); input token read via getComputedStyle(document.body) -> uniforms; the flow texture pass (quarter res, RG16F, two snoise per texel, once per frame); the march pass; the TAA resolve with reprojection; the 8x8 downsample and the async PBO readback; the token writer on document.documentElement.style; the scheduler (30 fps cadence, visibilityState pause, reduced-motion freeze and stop at 32 frames); the budget controller; the debug flag (?ground=debug) and parity flags.",
"Sampled wash: after the resolve, draw the final frame into an 8x8 RGBA8 target; gl.readPixels into a PIXEL_PACK_BUFFER; gl.fenceSync; on later frames poll getSyncParameter(SYNC_STATUS) and only when SIGNALED call getBufferSubData. Never readPixels to client memory directly (that stalls). At most 2 Hz. Compute the token values as the map states (row means for the stops; light colour and excess at the seed light's centre texel; the mean for --gamecult-ground-mean) and write them with setProperty on :root only when a value moved by more than 1/255. Q5 (a): keep the last written set in sessionStorage under one key and apply it on load before the first frame; it is a cache, and a missing or malformed entry means the seeds.",
"Budget controller, one function, one state machine: GPU time per frame by EXT_disjoint_timer_query_webgl2 when present, else a fenceSync after the resolve polled to completion, else rAF cadence. Over a 60-frame window: if the median exceeds the budget (4.0 ms), step down: S 16 -> 8 -> 4, then dpr -> 1 (Q2), then remove the canvas (the seeds paint). Never step up within a session. A median rising 50% over 5 minutes also steps down. No storage of the verdict, no userAgent, no maxTouchPoints.",
"cloud.frag.glsl: #version 300 es; precision highp float; precision highp int; the vendored CultMath.glsl is concatenated by ground.js before this file's body. The field, warp, interval march (ball radius F0 * (Δ/2 + D) with D = FLOW_MAX * PERIOD / 2) and integration exactly as the map's pseudocode; hue from the flow texture's channels mixed over the three nebula inputs; uniforms: resolution, time, dt, frame, jitter, S, intensity, nebula colours, base, Z0/Z1, F0, CUTOFF, K, PERIOD, FLOW_MAX. flow.frag.glsl: the two low-frequency snoise. taa.frag.glsl: motion vector from the flow texture (mv = project(-flow * dt)), history fetched at uv - mv, 3x3 min/max clamp, alpha = max(1/frames, 1/32), out-of-bounds alpha 1; writes new history and the screen (luminance clamp 0.035) via drawBuffers or two draws.",
"Vendoring (D3): site/quartz/static/ground/CultMath.glsl byte-equal to CultLib shaders/CultMath.glsl at the commit named in its first line. scripts/ground/check-cultmath-glsl.ps1 diffs it against git show <sha>:packages/cultmath/shaders/CultMath.glsl from a CultLib checkout and exits non-zero on drift.",
"Parity on the device: with ?ground=parity, ground.js evaluates the vendored fixture (CultLib tests/CultMath.Tests/fixtures/glsl-parity.json) one pixel per point, reads back float bit patterns, and reports per function the max ulp distance and the count outside its tolerance class. With ?ground=parity-wash, density and stardust are forced to 0 and the flow frozen; the canvas must equal the CSS seed wash within 2/255 at 16 points on / and on a ritual essay (this pins that the seed values and the shader's base agree at zero density, which is what the fallback promises).",
"Prove in the browser pane: max relative luminance after 32 frames on / at intensity 1 (<= 0.035); the parity-wash probe; the debug readback (mean evals per pixel, unfinished-ray fraction < 1%); the token writer (after 2 s on /, :root's inline style carries all fifteen tokens and --gamecult-ground-mean, each within the expected range, and getComputedStyle(body).backgroundImage resolves to the sampled values); the motion test (with ?ground=debug-mv the resolve writes |mv| into red: non-zero where flow is non-zero, zero under reduced motion); GPU ms per pass with the timer query at the pane's desktop size and at the mobile preset's pixel count. Phone GPU time is operator verification."
],
"deletes": [
{
"path": "site/quartz/styles/custom.scss",
"lines": 6,
"note": "ritual body block 119-124: its own background (three radials and the linear gradient) goes; its seeds live under :root:has(body[data-slug]) and it inherits body's ground."
},
{
"path": "site/quartz/styles/custom.scss",
"lines": 9,
"note": "body 10-27: the six literal colours and three literal stops leave as literals (rewritten as token references in the same lines; counted 9 removed, 9 added)."
}
],
"keeps_moves": [
"--gamecult-wash and the index override from wash-home-only: untouched; the shader reads it as intensity and the CSS wash still multiplies by it.",
".page::before (the grid overlay), the tour/Pitch VN ground, the Thing deck's .hero-bg, the masthead, the engine: untouched.",
"The CSS wash stays the ground when the canvas is absent, not yet drawn, or removed by the controller; it renders the sampled tokens when there are any and the seeds otherwise.",
"docs/brand-design-language.md 'The ground is a wash, not a flat fill' (69-84): rewritten: the ground is the shader; the CSS wash is sampled from it; the seeds are today's literal wash; the four nebula inputs are where the palette is tuned."
],
"adds": [
"site/quartz/components/GameCultGround.tsx: renders <canvas id=\"gamecult-ground\" aria-hidden=\"true\"></canvas>, mounts /static/ground/ground.js after DOM load; in sharedPageComponents.header after GameCultThemeLock (quartz.layout.ts:19). Its beforeDOMLoaded script applies the sessionStorage token cache to :root if present (Q5 a), so the first paint matches the last sample.",
"site/quartz/static/ground/ground.js (~400 lines, no dependencies, no build step): context, tokens in and out, flow/march/resolve/downsample passes, PBO readback, scheduler, controller, debug and parity modes, a named composite slot for the stardust cut.",
"site/quartz/static/ground/flow.frag.glsl, cloud.frag.glsl, taa.frag.glsl, downsample.frag.glsl; a shared 3-vertex full-screen vertex shader inline in ground.js.",
"site/quartz/static/ground/CultMath.glsl and glsl-parity.json, vendored from CultLib with the pin in the header.",
"scripts/ground/check-cultmath-glsl.ps1: the vendoring drift check.",
"custom.scss: the four nebula input tokens on body; the fifteen ground seeds plus --gamecult-ground-mean on :root with the ritual overrides under :root:has(body[data-slug]); #gamecult-ground { position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }.",
"docs/brand-design-language.md: the wash section rewritten as above; one paragraph on the ground shader: what it reads (inputs), what it writes (outputs), what it may not hold, and the fallback."
],
"file_changes": [
{
"location": { "path": "site/quartz/styles/custom.scss", "line": 4, "end_line": 8 },
"change": ":root block: the fifteen ground seeds and --gamecult-ground-mean declared beside color-scheme."
},
{
"location": { "path": "site/quartz/styles/custom.scss", "line": 10, "end_line": 31 },
"change": "body: four nebula input tokens declared; radials and the linear gradient reference the ground tokens; alphas keep the wash-home-only calc."
},
{
"location": { "path": "site/quartz/styles/custom.scss", "line": 103, "end_line": 124 },
"change": "ritual block: background declaration deleted; a :root:has(body[data-slug]) rule carries its seeds and nebula inputs."
},
{
"location": { "path": "site/quartz.layout.ts", "line": 19 },
"change": "header: [GameCultThemeLock(), GameCultGround(), GameCultMasthead()]."
},
{
"location": { "path": "docs/brand-design-language.md", "line": 69, "end_line": 84 },
"change": "wash section rewritten: shader-owned ground, sampled wash, seeds, nebula inputs."
}
],
"authority_map": {
"owner": "The shader owns the ground. custom.scss owns the four nebula inputs, --gamecult-wash and the seed values; ground.js owns the flow texture, the march, the TAA, the readback and the sampled ground tokens on :root; CultLib owns every noise and interval function the field calls.",
"inputs": [
"the four nebula tokens and --gamecult-wash (computed on body)",
"prefers-reduced-motion, visibilityState, canvas size, devicePixelRatio, elapsed time",
"measured GPU frame time (timer query, fence, or rAF cadence)",
"the shader's own 8x8 readback",
"the vendored CultMath.glsl at its pinned CultLib commit"
],
"outputs": [
"one opaque moving canvas under the page, luminance <= 0.035",
"the fifteen ground tokens and --gamecult-ground-mean on :root inline, at most 2 Hz",
"debug and parity readbacks (console, flag only)"
],
"derived_state": [
"the ground tokens are derived from the shader's result while it runs; the seeds are the fallback; the CSS wash renders whichever is in force; the shader never reads them",
"the flow texture is the one owner of motion: warp, hue, motion vectors (and the stardust, next cut) read it",
"the TAA history is a cache, reprojected by the flow; reset on resize, first frame and budget step",
"S and dpr are derived from measurement within the session; never stored",
"the sessionStorage token set is a cache of the last sample; absent or malformed means seeds",
"CultMath.glsl in the site is derived from CultLib at the pinned commit"
],
"forbidden_writers": [
"no colour, alpha, position or radius literal in ground.js or any GLSL file",
"no read of --gamecult-ground-* or --gamecult-light-* by the shader or by ground.js, except the seed read before the first readback",
"no navigator.userAgent, platform, maxTouchPoints, or any device-name branch",
"no class or attribute set on body or html by ground.js; token writes are setProperty on :root only",
"no second wash token; no page rule setting a radial alpha; no second flow evaluation outside flow.frag.glsl",
"no noise or interval function defined in the site's GLSL; only cultmath_* calls",
"no synchronous readPixels to client memory; no setInterval; one requestAnimationFrame scheduler; localStorage never"
],
"shared_paths": [
"home, every content page and the ritual essays paint one canvas and one token set, in CSS and in the shader",
"slow device, hidden tab, reduced motion, missing WebGL2 and missing half-float all pass through the one controller",
"the 8x8 readback is the only path from the shader to CSS; the beforeDOMLoaded cache apply and the live writer set the same tokens the same way",
"the same Deploy Quartz workflow; static files ship with the overlay"
],
"deletion_line": "The ritual block's own background and the nine body literals go in the same commit the tokens are declared. The canvas, four shaders, ground.js and the vendored library are the whole addition; nothing else in the engine or the content changes."
},
"verification": {
"builds": [
"Deploy Quartz green on the landing commit: gh run list -R GameCult/gamecult-site -w \"Deploy Quartz\" -L 1",
"scripts/ground/check-cultmath-glsl.ps1 exits 0 against a CultLib checkout at the pinned commit",
"the built stylesheet contains rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))) verbatim and a :root:has(body[data-slug=\"Blog/small-ritual-of-reach\"]) rule"
],
"tests": [
{
"name": "browser pane, /?ground=debug: max relative luminance over the canvas after 32 frames <= 0.035",
"pins": "text contrast: body text keeps >= 7:1 on every ground pixel."
},
{
"name": "browser pane, /?ground=parity-wash and a ritual essay: canvas vs CSS seed wash within 2/255 at 16 points with density 0 and flow frozen",
"pins": "the seeds and the shader's base agree; the fallback promise holds."
},
{
"name": "browser pane, / after 2 s: :root inline style carries the sixteen tokens; body's computed backgroundImage resolves to the sampled values; a read 1 s later differs only where the frame changed",
"pins": "the CSS wash is sampled from the shader, at most 2 Hz, without a stall (no readPixels-to-client in the profiler)."
},
{
"name": "browser pane, /?ground=parity: every fixture function within its tolerance class on WebGL2; numbers and GPU string in the report",
"pins": "GLSL = C# on a device; the proving ground's third runtime."
},
{
"name": "browser pane, /?ground=debug at desktop and the mobile preset: mean evals per pixel, unfinished rays < 1%, GPU ms per pass; ?ground=debug-mv shows motion where flow is non-zero",
"pins": "the step budget, the interval saving with the warp, and the reprojection on the shipped field."
},
{
"name": "reduced motion emulated: frames stop at 32, mv is 0, time frozen; hidden tab: no frames; WebGL2 disabled: no canvas in the DOM and the seed wash paints; sessionStorage cleared: seeds paint first",
"pins": "every fallback goes through the controller and lands on the same CSS ground."
},
{
"name": "git diff base..head --stat: custom.scss, quartz.layout.ts, docs/brand-design-language.md, the new component, static/ground/*, scripts/ground/* only",
"pins": "no engine, content or masthead change."
}
],
"negative": [
{
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(|\\b(255|138|109|183)\\b",
"scope": "site/quartz/static/ground/*.js and *.glsl: 0 matches."
},
{
"pattern": "gamecult-ground-|gamecult-light-",
"scope": "site/quartz/static/ground/*.glsl: 0 matches; in ground.js only inside the token writer and the seed read, never in a uniform upload."
},
{
"pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|setInterval|classList|setAttribute\\(",
"scope": "site/quartz/static/ground/ground.js: 0 matches."
},
{
"pattern": "readPixels\\([^)]*(Uint8Array|Float32Array|Uint16Array)",
"scope": "ground.js: 0 matches outside the parity and debug readbacks, which are flag-gated and named."
},
{
"pattern": "float cultmath_|vec4 cultmath_|float snoise|hash\\(",
"scope": "flow/cloud/taa/downsample.frag.glsl: 0 matches; cultmath_* is defined only in the vendored CultMath.glsl."
},
{
"pattern": "radial-gradient\\([^)]*rgba?\\([0-9]",
"scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches."
},
{
"pattern": "gamecult-titlebar|gamecult-nav-link|sidebar|composite|GameCult-Quartz",
"scope": "git diff base..head changed lines: 0 matches."
}
],
"operator": [
"open / on the desktop and on the phone; say whether the nebula's motion and intensity are right (uniforms: PERIOD, FLOW_MAX, intensity)",
"on the phone with ?ground=debug, read the logged GPU ms and the controller's final S/dpr; say whether it got warm after five minutes",
"navigate / -> /Blog -> back; say whether the ground is continuous or shows a seed flash (Q5)",
"open a ritual essay and confirm its ground still reads as its own paper"
]
},
"estimate": {
"lines_added": 980,
"lines_removed": 15,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": ["GLSL ES 3.00 shader files in static/ground", "vendored glsl-parity.json", "one sessionStorage key holding the last sampled token set"],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"site-masthead:ruling:operator-chromeless-column"
],
"questions": [
"Q4 in the map: palette input tokens (a, recommended)",
"Q5 in the map: carry the last sampled wash across page loads (a, recommended)"
]
}

View file

@ -0,0 +1,141 @@
{
"campaign": "cultmath-tapes",
"cut": "site-stardust",
"revision": 1,
"title": "Site stardust: stateless particles advected by the ground's flow texture, rendered to a half-res layer, blurred by the clouds' accumulated opacity (raymarch-driven DoF), composited behind the nebula",
"repo": "GameCult/gamecult-site",
"branch": "main",
"base": "535d170",
"depends_on": [
"cultmath-tapes:cut_spec:site-ground.r2"
],
"first": [
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'Stardust', 'Depth of field driven by the raymarch', 'Budget', D4, D9, D10. Re-pin base to main after site-ground lands. The composite slot site-ground left in the resolve is where this cut plugs in; do not restructure the resolve.",
"Aetheria reference, read once, copy nothing: Compute/Stardust/Stardust.compute:118-147 (grid cell -> hashed spawn, lifetime = frac(time/period + hash), position -= flow * lifetime * period, size = parabola(lifetime, 2), colour from a hue ramp x nebula tint); Stardust.shader:49-86 (billboard from quadPoints by SV_VertexID/SV_InstanceID, powerPulse falloff, distance exponents); CustomDoF/DepthOfField.hlsl:29-32 (CoC read from a raymarch output), :150-201 (disk gather weighted by CoC), :217-242 (combine). The web version is far smaller: no distance exponents (the slab is thin), no shadow pass, no near/far split.",
"Particles are stateless: no buffers are updated. One static 6-vertex quad VBO; drawArraysInstanced with count N; the vertex shader derives everything from gl_InstanceID, time and the flow texture (vertex texture fetch is standard in WebGL2): cell = (id mod G, id / G) over the slab mid-plane; hashes via cultmath_pcg3d (the vendored CultMath.glsl is concatenated before the vertex source too) for spawn jitter, lifetime offset, size and brightness; lifetime = fract(t / PERIOD_P + offset); position = spawn - flow(spawn uv) * lifetime * PERIOD_P; size = base * parabola(lifetime, 2); colour = hue at the screen position (from the flow texture's channels, the same mix as the clouds) * brightness. Reduced motion freezes t as site-ground does.",
"Stardust layer: half-resolution RGBA16F target, cleared each frame, additive blend, powerPulse falloff in the fragment shader; N = 16384 desktop, 4096 phone; the controller scales N with S (N halves when S halves) and the layer follows dpr.",
"DoF: in the resolve's composite slot, read opacity = 1 - T from the reprojected, clamped history (not the current frame) and gather the stardust layer with an 8-tap Poisson disk of radius RMAX * opacity in layer texels (RMAX ~ 12 px at dpr 1, scaled by dpr); weight taps by a soft disk; composite out = ground + clouds + stardust_blurred * T, then the luminance clamp. All stardust is behind the whole cloud layer (D9).",
"Debug flags: ?ground=debug-stardust writes the stardust layer alone; ?ground=debug-coc writes opacity into red. Prove in the browser pane: the stardust and the clouds move together (with ?ground=debug-mv from site-ground and the particle velocity written in green under ?ground=debug-stardust-v, the two directions agree within 5 degrees at 64 sampled pixels where |flow| is above a threshold); luminance clamp still holds after the composite; GPU ms per pass (stardust layer, gather) with the timer query at desktop and at the mobile preset's pixel count; the full-stack total against the budget table; the unfinished-ray fraction unchanged by this cut.",
"Budget contract (map, 'Budget'): the stardust layer plus the gather cost no more than a quarter of the march at the same S; if the measurement says otherwise, the gather drops to 4 taps before N drops, and the report says which. Stardust is never cut before the clouds' S on the ladder."
],
"deletes": [],
"keeps_moves": [
"site-ground's flow texture, march, TAA, readback and token writer: untouched; the stardust reads the flow texture and the hue mix the same way the clouds do.",
"The sampled wash now includes the stardust in its 8x8 readback by construction (it samples the final frame); nothing changes in the writer.",
"The budget controller gains one scaled quantity (N) and no new state; the ladder order is unchanged."
],
"adds": [
"site/quartz/static/ground/stardust.vert.glsl and stardust.frag.glsl: the instanced billboard and the powerPulse sprite.",
"ground.js: the stardust pass (one VBO, one program, drawArraysInstanced), the half-res layer, N in the controller, the gather in the resolve's composite slot, the two debug flags.",
"taa.frag.glsl: the composite slot filled: opacity from history, the 8-tap gather, out = ground + clouds + stardust_blurred * T.",
"docs/brand-design-language.md: two sentences under the ground shader paragraph: the stardust and the DoF, and that they read the same flow and hue as the nebula."
],
"file_changes": [
{
"location": { "path": "site/quartz/static/ground/ground.js", "line": 1 },
"change": "stardust pass, layer, N scaling, gather hookup, debug flags; line numbers are site-ground's and are re-read by function name."
},
{
"location": { "path": "site/quartz/static/ground/taa.frag.glsl", "line": 1 },
"change": "the named composite slot becomes the opacity-weighted gather and the final composite."
},
{
"location": { "path": "docs/brand-design-language.md", "line": 69 },
"change": "two sentences on stardust and DoF under the ground shader paragraph (post site-ground numbering)."
}
],
"authority_map": {
"owner": "ground.js owns the stardust pass and the DoF gather; the flow texture (site-ground) owns the particles' motion; the TAA history owns the opacity the DoF reads; the nebula input tokens own the particles' hue through the same mix as the clouds.",
"inputs": [
"the flow texture and the hue mix (site-ground)",
"the reprojected, clamped history's transmittance",
"time, N and dpr from the controller; gl_InstanceID"
],
"outputs": [
"a half-res stardust layer per frame",
"the composite with stardust blurred by cloud opacity behind the nebula, still under the luminance clamp"
],
"derived_state": [
"every particle's position, lifetime, size and colour are derived from gl_InstanceID, time and the flow texture each frame; nothing is stored or updated",
"N is derived from S by the controller; the layer size from dpr",
"the blur radius is derived from the accumulated opacity; there is no second CoC source"
],
"forbidden_writers": [
"no particle buffer update, transform feedback or CPU-side particle state",
"no second flow function or hue mix; the vertex shader samples the flow texture",
"no colour literal; no per-depth transmittance; no second march",
"no new controller state beyond N; the ladder order is not changed",
"no change to the readback or the token writer"
],
"shared_paths": [
"clouds and stardust read one flow texture and one hue mix, so they move and colour as one",
"the one resolve pass composites ground, clouds and stardust and applies the one luminance clamp",
"the one controller scales S, N and dpr together"
],
"deletion_line": "Nothing is deleted. The addition is two small shaders, one pass and one gather; it buys the stardust and the DoF the operator named, and it must prove it fits the same budget."
},
"verification": {
"builds": [
"Deploy Quartz green on the landing commit",
"scripts/ground/check-cultmath-glsl.ps1 still exits 0"
],
"tests": [
{
"name": "browser pane, /?ground=debug-stardust-v with ?ground=debug-mv: particle velocity and cloud motion vectors agree within 5 degrees at 64 sampled pixels where |flow| exceeds the threshold",
"pins": "one flow: the stardust and the nebula move as one."
},
{
"name": "browser pane, /?ground=debug-coc: opacity is 0 where the clouds are empty and rises inside them; the stardust gather radius follows it (a visible blur under a dense cloud, sharp in a gap)",
"pins": "the raymarch drives the DoF, as in Aetheria's hack."
},
{
"name": "browser pane, /?ground=debug: max relative luminance after 32 frames <= 0.035 with stardust on",
"pins": "the clamp holds after the composite."
},
{
"name": "browser pane, timer query at desktop and at the mobile preset: stardust layer + gather <= 25% of the march at the same S; full-stack totals quoted against the budget table",
"pins": "the stardust fits the same budget or the report says what gave."
},
{
"name": "reduced motion: particles frozen, frames stop at 32; hidden tab: no frames",
"pins": "the stardust obeys the one controller."
}
],
"negative": [
{
"pattern": "bufferSubData|transformFeedback|TRANSFORM_FEEDBACK|new Float32Array\\((N|count|particles)",
"scope": "ground.js stardust section: 0 matches; particles are stateless."
},
{
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(",
"scope": "stardust.vert.glsl, stardust.frag.glsl, ground.js: 0 matches."
},
{
"pattern": "snoise\\(|float cultmath_",
"scope": "stardust.vert.glsl and stardust.frag.glsl: 0 matches; the flow and hue come from the texture, not from a second evaluation."
},
{
"pattern": "readPixels|setProperty",
"scope": "git diff base..head changed lines in ground.js: 0 matches; the readback and the token writer are not touched."
}
],
"operator": [
"open / on the desktop and on the phone; say whether the stardust sells the motion and whether the blur under the clouds reads like the video (RMAX, N, PERIOD_P are the dials)",
"on the phone with ?ground=debug, read the full-stack GPU ms and the final S/N/dpr; say whether it got warm"
]
},
"estimate": {
"lines_added": 320,
"lines_removed": 4,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"site-masthead:ruling:operator-chromeless-column"
],
"questions": []
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,125 @@
# CultMath tapes: interval-pruned field evaluation on CPU and GPU
Status: steps 2 and 4 (graph, C# evaluator, HLSL interpreter) unparked on
2026-10-14 (ruling tape-unparked-for-trophic-graphs, which supersedes
tape-target-unparks-when-asura-stable); their model page and cut sequence are
`docs/cultmath-interval-ground-cut.md`, "Pass 7". Steps 3 and 5 (pruning,
meshing) stay parked. The affine-forms
piece was unparked on 2026-10-03 (ruling affine-forms-unparked) and is on main
(156782da), alongside step 1's intervals; its map and as-built record are in
`docs/cultmath-interval-ground-cut.md`. This note records the direction for the
parked steps so they can be picked up without re-deriving it.
## Intent
Evaluate large implicit fields, meaning deeply nested CSG trees of brushes with
thousands of nodes, cheaply enough to mesh at many levels of detail and to
raymarch. The operator's words (2026-10-02): "I would love to design a space
station out of a grammar of nested brushes, and have the ability to
automatically mesh it at multiple levels of detail so I can get arbitrarily
close to its tiny details without melting my GPU."
GameCult has no deep CSG trees today because nothing could evaluate them
cheaply. The capability comes first, and the content follows.
## Prior art
[Fidget](https://www.mattkeeter.com/projects/fidget/) by Matt Keeter
(MPL-2.0, Rust, v0.5.1 on 2026-09-27; repo `mkeeter/fidget`).
- It compiles an expression to a register-allocated bytecode tape.
- It evaluates that tape as points, SIMD batches, intervals, and forward-mode
gradients.
- The central trick is interval evaluation over a region. When it proves that
one operand of a `min` or `max` always wins, the tape for that region is
rewritten without the other branch. In the project's example, the tape
shrinks from 254 clauses to about 20 at pixel level.
- `fidget-jit` targets x86-64 and AArch64.
- `fidget-wgpu` has a WGSL tape interpreter, interval ops and tape
simplification on the GPU (`interval_ops.wgsl`, `tape_interpreter.wgsl`,
`tape_simplify.wgsl`).
- `fidget-mesh` implements Manifold Dual Contouring. It is known to be weak on
thin features.
- Halfspace is the IDE codesigned with `fidget-wgpu` (Rust, egui and wgpu,
MPL-2.0).
## Chosen shape (option c)
The ideas are ported into CultLib and owned here. Fidget is not a dependency.
Unity renders HLSL on its own graphics backend, so `fidget-wgpu` cannot run
inside it.
Three options were weighed on 2026-10-02:
| Option | What it is | Trade |
|---|---|---|
| a | Fidget as a Rust native plugin in Unity | Fastest demo; costs per-platform native builds, an FFI boundary, and an upstream that warns of breaking changes |
| b | Offline bake with a Rust tool; ship meshes | Simplest; the baked depth caps how close a viewer can get |
| c | CultLib owns tape, interval and pruning in C# and HLSL | Most coherent end state; the largest code liability we would own |
The operator parked (c). An (a) spike to validate authoring and LOD behaviour
before owning code was suggested but not ruled.
Sketch, to be replaced by the target pass:
1. **CultMath `interval`** in C# and HLSL with parity tests:
- arithmetic, `min`/`max`, `smin`, `sqrt`, `clamp` and `smoothstep`;
- interval or Lipschitz bounds for every CultMath noise primitive.
This step needs no tape. It stands alone, and is the first cut to take even
if the rest stays parked.
2. **Tape**: an expression graph lowered to bytecode, with point, interval and
gradient evaluators in C#. Gradients reuse CultMath's analytic-gradient
primitives, which Asura cut 2a built.
3. **Pruning**: per-region tape simplification driven by interval results.
4. **HLSL interpreter** for the same bytecode, with interval ops and pruning.
Fidget's MPL-2.0 WGSL is the reference. MPL code keeps its own MPL-headed
files, as Phacelle does.
5. **Meshing**: an octree of chunks with seams stitched between LOD levels.
- The default is surface nets (GameCult.Geometry, Asura Cut 2) plus
Asura's tile pass: Newton refinement onto `f = 0`, then crease snapping
from field gradients (Asura Cut 6c).
- The operator's lean (2026-10-02) is that gradient work beats dual
contouring here. Thin authored bevels can be shaded by their analytic
normals, and the refined positions can be iterated further.
- Dual contouring is not planned. The tile pass on station brushes is its
test.
## Truth and lowerings
The field is data: a typed CultCache document holding the expanded tree. A
generator, such as a brush grammar, expands into that document, and everything
downstream caches it. The CPU evaluator, the HLSL interpreter, the mesher and
collision are all lowerings of that one document. This is how "one
implementation of the field" survives having both CPU and GPU evaluators.
Asura's invariant 2 had to forbid CPU evaluation because its field is HLSL
source.
## Known consumers
- **Aetheria volumetric raymarching** (clouds, nebulae). Step 1 alone gives it
empty-space skipping. Coverage and shape terms carry the culling, because
detail octaves only bound to ±amplitude.
- Prerequisite: Aetheria's shaders must use CultMath noise, not their own
copies. That is already owed at Aetheria's CultMath pin bump (see the Asura
cut map, snoise release follow-ups).
- **Space stations from a brush grammar**, meshed in LOD chunks around the
camera. This is a separate project with its own target. This campaign
supplies its evaluator.
- Asura does not need this campaign. Its field is noise-dominated, and its
`A_max` shell and band-limiting already give the cheap culling.
## Decided since the unpark, and what stays open
- The spike-or-(c) choice was settled by the operator's unpark: the tape
unparked for Aetheria's trophic species graphs (ruling
`cultmath-tapes:ruling:tape-unparked-for-trophic-graphs`).
- Interval representation: affine forms for noise and warp, unparked on
2026-10-03 (ruling `cultmath-tapes:ruling:affine-forms-unparked`): one
shared symbol, mapped in `docs/cultmath-interval-ground-cut.md`, "Pass 5".
- Register model: the HLSL interpreter keeps its registers in groupshared
memory, a slice per thread, linear-scan allocated at lowering (ruling
`cultmath-tapes:ruling:tape-register-model-ruled`; research in
`docs/research/tape-register-model.md`).
- Open: whether the brush-grammar station project lives in Aetheria or in
its own repo (follow-up `cultmath-tapes:follow_up:brush-grammar-station-repo`,
raised as an operator question when a station cut reaches the frontier).

View file

@ -0,0 +1,88 @@
# CultMesh garden path: map
Campaign `cultmesh-garden-path`, stage 1 (server-authoritative garden path for the
orbs .io game). Target: `docs/cultmesh-garden-path-target.md` (typed as
`cultmesh-garden-path:target:r2`). Rulings, questions and follow-ups are documents
in the mind; this page holds the model page, body facts and the stage 1 cut lines.
Written by Imagination (`imagination-gp-0b`, session `self-2026-10-27-eureka`)
against CultLib `d1bf21b3`. Refreshed by `imagination-gp-stage1` (session
`self-2026-10-27c-eureka`) against `51ca81a1`: every fork the page waited on is
ruled, and the cells below carry the rulings.
## Model page
One row per kind. "Store" says where it lives: a CultCache document in the room's
store, or transient per-tick data that never enters a store. Proposed values are
marked as such; they are the design this page offers for the cuts, not landed
behaviour. A cell that waits on an operator question names it.
The governing split, from `state-is-cultcache` and the CultCache scope restraint:
every value that crosses a boundary is CultCache-encoded (one serialisation model,
no JSON), but only state that outlives a tick is a record in a store. Inputs, ticks
and snapshots are typed CultNet message payloads, not store records. The store
receives exactly one write set per tick (ruled `tick-state-commit`: tick-batch).
| Kind | Store | Identity (what names it) | Lifecycle (what happens to it) | Authority (who decides; forbidden writers) |
|---|---|---|---|---|
| **Game definition** (documents, systems, authority declaration, tick rate) | Proposed: one CultCache document per server process (`gamecult.mesh.game_definition`), read-only to everyone but the server's startup. | Game id string from code (`"orbs"`), namespaced by Verse id. The id alone is not injective (two builds share it), so proposed identity is `(game id, definition hash)`, the hash derived from the lowered definition: document schema ids and versions, system ids, order and marks (`[Predicted]`, later `[Adjudicable]`), tick rate, authority declaration. Schema ids (`orbs.player`) are global strings; the game prefix is convention, not enforced (body fact F9). | Created at server startup from code; never revised at runtime; a new build supersedes it with a new hash. Clients carry their own compiled hash (C# in wasm, ruling `browser-sim-runtime`) and are refused at join on mismatch. A declaration a mode cannot honour is refused at startup (`modes-do-not-leak`). Never replayed. | The game's code, through the builder, at startup; the runtime validates. Forbidden: clients, any runtime mutation, Odin (Odin may read and index it, never write it). |
| **Game document** (Player, Pellet, Shot) | CultCache documents in the room store. | `(schema id, record key)`. Today the key is a free string the caller picks; `[CultIndex] Id` in the illustration is an index, not the key. Proposed: the runtime mints keys. Player key derives from the session principal (one Player per principal per room). Spawned documents (Shot, Pellet) key on `(spawner key, tick, spawn ordinal within that tick)`, so a client's predicted spawn and the server's authoritative spawn derive the same key and reconcile instead of duplicating. That is injective as long as spawn order within a system step is deterministic for the predicting client, which the predicted system's own code gives under one runtime (C# both sides). The developer's `Id` field becomes redundant; whether to keep it as a mirror of the key is an API call for the cut, not an operator fork. | Created by a system during a tick, or by the join system (Player). Revised at most once per tick, in that tick's commit. Deleted by a system (pellet eaten) or by the game's leave system after a session ends. Across reconnect within the grace window the Player record is unchanged; only the session's connection binding moves. Across a server restart: gone (ruled `room-durability`: ephemeral; a restart is a new room epoch at tick 0). Replayed only on the client, during reconciliation; the server never replays in stage 1. | The server tick runner on the room's primary is the one writer of committed state, and systems write only through the tick's working set. Forbidden writers: client document puts through `CultNetDatabaseServer.HandlePutAsync` (today accepted from any peer, F1); `PutPredictedAsync` and `CultMeshGameSession.PredictAsync` on any networked store; game code calling database APIs; the simulation-fact committer (F7). |
| **Player input** | Transient. A CultCache-encoded payload on a CultNet message; never a store record in stage 1. | `(session principal, input sequence)`, the sequence monotonic per session; each input also carries the tick it targets. Namespace: the room. Injective given the principal. The input type is a schema'd CultCache type, so the schema catalog publishes it like any document. | Sampled each client tick (devices read by the TS shell, handed to wasm); sent with the last few unacknowledged inputs repeated, so one loss is absorbed; buffered by the server per session against its target tick; consumed exactly once by the matching tick (proposed policy for late inputs: applied to the next tick and counted); acknowledged in the snapshot as "last processed sequence"; kept by the client until acknowledged, for replay; then discarded. Stage 4 changes this: inputs become signed, hash-chained and retained for a dispute window. | The client's principal authors the content; the tick runner decides which tick consumes it. Forbidden: server code altering input content; other clients; any input that arrives without a bound session. |
| **Tick** | Transient counter; proposed: the last committed tick is written into the room record in each tick's batch, so a snapshot or checkpoint knows it. | `(room id, room epoch, tick number)`, the number starting at 0 when the room epoch starts. Injective with the epoch. | Advanced only by the server tick runner at the one declared rate (ruled `tick-model`: one-fixed-rate); never revised. The client estimates the server tick from snapshots and runs its predicted tick ahead by its round trip plus a buffer; on reconnect it re-syncs from the next full snapshot. | Server tick runner. Forbidden: client clocks, transport arrival time, wall-clock reads inside systems (systems receive the tick, not the time). Today no tick scheduler exists anywhere in the C# body (F6). |
| **Predicted state and reconciliation** | Client-side only. In the browser they live in the C# wasm CultCache store; the TS renderer reads a per-frame view (ruled `browser-client-store`: wasm-owns-store). | Same `(schema id, record key)` as the authoritative document, plus the client tick it was predicted for. | Each client tick: predicted systems run over owned documents and the client's own spawns. On a snapshot for tick T acknowledging input sequence S: predicted documents are replaced by the authoritative ones at T, then inputs after S are replayed through the predicted systems up to the current client tick; predicted spawns are matched by derived key, and an unmatched one is removed once the acknowledgement passes its tick. Today `Reconciled` means "the authoritative put replaced the predicted copy" with no replay (F3); the replay loop does not exist. | Predicted state owns nothing outside the client. It is derived from the last authoritative snapshot plus unacknowledged inputs plus the predicted systems. The snapshot owns truth. Forbidden: sending predicted documents to the server; `CultNetClientAuthorityScope` in server mode (F4); the renderer writing. |
| **Session and the player's identity** | Proposed: session membership as a small CultCache document in the room store (`gamecult.mesh.game_session`, low churn, readable by systems and present in snapshots). The connection handle is transient process state. | Principal: the fingerprint of a browser-held P-256 key, proved by signing a join challenge (ruled `player-identity`: browser-keypair). Connection: a server-minted id per transport connection (ephemeral). Logical session: `(principal, room)`, bound across reconnects. Today the client's identity is a self-asserted `runtimeId` string (browser client options; `SourceRuntimeId` on operation envelopes); CultMesh's session identity proves the server to the client and not the reverse (F5). The body still has no authenticated client principal (F5, F13); the session cut builds it. | Connect, then join (definition hash check, principal proof), then the session record is created and the join system spawns the Player. Disconnect suspends the session for a grace window; resume binds a new connection to the same session; expiry ends the session, systems see the leave, and the game's leave system removes the Player. On server restart sessions end (ruled `room-durability`). | The server's join and leave handler on the room primary decides membership; the principal proves itself by signing the join challenge. Forbidden: a client-asserted `runtimeId` as identity; transport peer address; peer catalogs. |
| **Authority declaration and the lease it lowers to** | Declaration: inside the game definition. Lease: proposed as a CultCache document (`gamecult.mesh.authority_lease`); today a plain class in an in-memory dictionary (F2). | Declaration: per document type, `(writer, checker, arbiter)`. Lease: `LeaseId`, carrying Verse, peer, roles, shard ids, issuer, validity window, epoch and signature (`CultMeshAuthorityLease`). Namespace: the Verse. | Declaration fixed per build. At startup `Authority.Server` lowers to: the server holds a `ShardPrimary` lease on the room shard, and clients hold no write scope at all. Renewed before expiry; revocation or expiry stops the tick runner from committing; a new primary takes a new epoch. Proposed issuer in stage 1: the server's own deployment key, which clients pin through the browser trust policy, so an indie deployment needs no Odin. Odin-issued leases stay possible. | The game definition decides the declaration; the issuer issues leases; `CultMeshAuthorityResolver` decides whether a peer holds a role on a shard. Today the database's write gate (`CultNetShardDescriptor.IsPrimary`, `CultNetClientAuthorityScope`, both set from options) never consults the resolver: two authority structures (F2, F4). Proposed: the write gate is derived from lease resolution. First step (cut `remote-write-authority`): a peer's put or delete reaches the database only when `CultNetDatabaseServerOptions.RemoteWriteAuthority` grants it; none configured refuses every remote write (F1, GP1). The lease-backed grant needs an authenticated principal bound to the connection, so it follows the session cut. `CultNetClientAuthorityScope` is no longer an owner; it is derived from the declaration, and in stage 1 it is empty. |
| **Shard / room** (stage 1: one) | `CultNetShardDescriptor` (configuration); the room's state is its CultCache store. | Room id `(Verse, game id, room name)`, for example `orbs/main`, carried as the shard id plus the shard epoch. | Created at server start (epoch 1; on restart a new epoch, ruled `room-durability`); never moved in stage 1; ended at shutdown. Join means "join the one room"; matchmaking is out of scope. Interest management is stage 5 per the target, so in stage 1 every client receives the whole room. | The runtime holding the `ShardPrimary` lease. Forbidden: a second primary; clients. |
| **Snapshot sent to clients** | Transient. A CultNet message whose payload is CultCache-encoded documents; never stored. The server keeps per-client baselines in memory for deltas. | `(room, epoch, tick, recipient session, baseline tick)`. | Derived after a tick commit at the send rate; a delta against the recipient's last acknowledged snapshot, or a full snapshot on join and resume; carries the last processed input sequence for that session. Discarded once sent; a baseline is dropped when a newer one is acknowledged. Today the nearest things are per-record change pushes from `CultNetDatabaseSubscriptionServer` (no tick, no acknowledgement, no input sequence) and shard catch-up snapshots in `CultMeshSnapshots.cs` (F8). | The snapshot builder decides nothing; it is derived from the committed tick. Forbidden: the per-record subscription stream acting as a second game replication path. That stream stays for non-game observers. |
| **Simulation fact / witness observation** (found) | `CultMeshSimulationFact` is a CultCache document; observations are hub memory. | `(shard, epoch, frame, subject, kind)`. | Stage 1: not used. The target replaces it at stage 4. | Today `CultMeshGameSession` serves observations by default (`ServeSimulationObservations = true`) and commits quorum facts through the primary (F7). In server mode there is no checker, so the observation server and the committer must not be wired. Forbidden writer in stage 1. |
| **Shard mutation log entry** (found) | `CultNetDatabase` in-memory `SortedList` per shard, plus an optional durable store. | `(shard id, sequence)`. | Today every primary write appends one entry, and the in-memory list is never trimmed (F10). Per-record writes at tick rate would grow it without bound. In stage 1 the tick's write set either bypasses the per-record log or lands as one bounded entry per tick. Ruled: the tick's write set lands as one batch per tick (`tick-state-commit`), in memory (`room-durability`); the bounded per-tick entry is the tick-runner cut's to specify. | `CultNetDatabase` on the primary. Forbidden: per-system-write appends from the tick runner. |
### Cells filled by rulings (2026-10-27)
- Session principal: `player-identity` (browser-keypair). Not yet in the body (F5, F13).
- Browser documents and the renderer's read: `browser-client-store` (wasm-owns-store).
- Room state across restarts: `room-durability` (ephemeral).
- Tick rate model: `tick-model` (one-fixed-rate).
- How a tick reaches the store: `tick-state-commit` (tick-batch; the tick-runner cut times `CultCacheBatch` at tick rate first).
- Stage 1 transport: `browser-transport` (WebSocket behind a port carrying only inputs and snapshots; a WebTransport server in parallel, `webtransport-server-layer`: managed C# over the MsQuic bridge).
- Still unprobed: `CultCacheBatch` commit cost at tick rate for about 50 players plus pellets (wasm measuring probe and tick-runner cut).
## Body facts
All are source reads at CultLib `d1bf21b3` on 2026-10-27 by `imagination-gp-0b`. No
runtime probe was run in this pass. Each fact names the read that would falsify it.
- **F1. The primary applies any peer's document put.** `CultNetDatabaseServer.HandlePutAsync(message, peer)` (`src/GameCult.Networking/CultNetDatabaseServer.cs:307`) calls `ApplyPutAsync(message)` (`CultNetDatabase.cs:1022`), which checks only that this runtime is primary for the shard and the epoch matches (`EnsurePrimary`, `:2003`). `peer` is used only to send errors. Any connected client can overwrite any game document on the server.
- **F2. Leases are not typed state and do not gate writes.** `CultMeshAuthorityLease` is a plain class (no `[CultDocument]`); `CultMeshAuthorityLeaseCatalog` is a `Dictionary` (`CultMeshAuthority.cs:242`). The only `[CultDocument]` types in `GameCult.Mesh` are body publication, CDN chunk and manifest, content transfer state, discovery state, entity prefab package and simulation fact. `GameCult.Networking` (the database) references no lease type; the one Networking file naming `Lease` is `CultNetSchemaMessages.cs`.
- **F3. Reconciliation is replacement, not replay.** `CultNetDatabase.cs:1247-1252`: a predicted put records a prediction key; the next non-predicting write to that key turns the change into `Reconciled`. No code re-applies inputs after an authoritative state.
- **F4. Client write scope is a runtime-id string match from options.** `CultNetClientAuthorityScope.Matches` compares `OwnerRuntimeId` with the database's own `_runtimeId` (`CultNetDatabase.cs:111-153`, `EnsureClientAuthority` `:1934`). It gates local prediction only and has no relation to leases.
- **F5. No authenticated client principal.** `CultMeshSessionIdentity.cs` verifies transports for `(verse, authorityRuntimeId, protocol, routeGeneration)`, which proves the server to the client. The browser client's `runtimeId` (`packages/cultmesh-browser/src/index.ts:182`) and the operation envelope's `SourceRuntimeId` (`CultNetOperationServer.cs:39`) are self-asserted.
- **F6. No tick scheduler.** A grep of `GameCult.Networking` and `GameCult.Mesh` for tick or timestep names finds none. `Frame` exists only as a witness-asserted number in `CultNetSimulationConsensus` and `CultMeshSimulationFact`.
- **F7. The game session serves the witness path by default.** `CultMeshGameSession` constructs `CultNetSimulationObservationServer` when `ServeSimulationObservations` (default `true`) and always constructs a `CultMeshSimulationFactCommitter` (`CultMeshGameSession.cs:70-104`). `PredictAsync` is `PutPredictedAsync` (`:161`).
- **F8. The browser can watch and invoke, not put.** The `cultmesh-browser` README says it "does not expose direct document writes". The client offers `leaseRawDocument` (`index.ts:349`) and `invoke` of typed operations (`:370`), over one WebSocket. On C#: `CultNetDatabaseSubscriptionServer` and `CultNetOperationServer` take `ICultNetSchemaServer`, so they run over `CultNetWebSocketSchemaServer`; `CultNetDatabaseServer` (puts) takes the concrete UDP `Server` (`CultNetDatabaseServer.cs:36`). Operations are request and response with receipts and idempotency keys, which is a poor fit for an input stream at tick rate.
- **F9. Schema-id namespacing is convention.** `[CultDocument("orbs.player", ...)]` ids are global strings; nothing ties a schema id to a game id.
- **F10. The in-memory mutation log is never trimmed.** `RecordMutationLogEntry` (`CultNetDatabase.cs:1610`) inserts into a per-shard `SortedList`; no `Remove` or trim on that list exists in the file. Compaction applies to the durable store and to burned sequences only.
- **F11. CultCache has a write-set batch; CultNetDatabase does not expose one.** `CultCacheBatch` with `Commit` and `TryCommit` (`GameCult.Caching/CultCache.cs:1796`, `:2212`) is the 2.0.0 write set (`variants:ruling:write-set-content-version-declared-ownership`). `CultNetDatabase` has no batch method: every write is a per-record door.
- **F12. A reflection site on the mesh put path.** `CultMesh.cs:3502` `PutDatabaseDocumentAsync` picks `PutAsync` or `PutPredictedAsync` by `GetMethods` and `MakeGenericMethod`, which matters to the AOT prerequisite (`cultlib-gaps:follow_up:cs-aot-safe-registration`; probe in `research/cultmesh-garden-path/cultcache-aot-probe-2026-10-27.md`).
- **GP1 (runtime probe, 2026-10-28, `imagination-gp-stage1`).** Scratch NUnit test at `51ca81a1`, run on Yggdrasil (`ygg-verify.sh`, `mcr.microsoft.com/dotnet/sdk:10.0`, filter `ImagGpProbeTests`, never pushed to origin). The server writes `gp:player` = "server-owned" through `CultNetDatabase.PutAsync` on a primary shard. `CultNetDatabaseServer._putHandler`, invoked with an uninitialized `CultNetServerPeer` (no field set: no endpoint, no transport, no identity) and a put of "client-overwrote", leaves the record "client-overwrote", and `Server.CanProcessBeforeVerification(put)` returns `true`. Output: `GP1 after=client-overwrote canProcessBeforeVerification=True`, Passed 1. This confirms F1 at runtime: nothing about the peer is consulted, and puts and deletes pass before the legacy login verification (`Server.cs:562-575`).
- **F13. A server peer carries no identity.** `CultNetServerPeer` (`Server.cs:829`) holds the LiteNetLib `NetPeer`, its endpoint and transport stats. The only principal on `Server` is the legacy username and password login (`PlayerData.PlayerId`, `IsVerified` `:697`), which the database door never reads. `ServerSecurityOptions` is one shared `ConnectionKey`: transport admission, not authority.
- **F14. A new refusal code is a coordinated change.** The `cultnet.error.v0` `code` is a closed enum (`contracts/cultnet/cultnet.error.schema.json:28-31`, with a byte-identical copy under `packages/cultnet-rs/contracts/`). Rust `CultNetErrorCode` (`packages/cultnet-rs/src/contracts.rs:242-273`) refuses unknown codes, `packages/cultnet-rs/tests/error_message.rs` decodes captured C# bytes per code, and TypeScript imports the root schema (`packages/cultnet-ts/src/contracts.ts:10`). `unowned_schema` was added this way and is the pattern.
- **F15. Forwarding re-sends as a new client.** `TryForwardPutAsync` (`CultNetDatabaseServer.cs:654`) forwards a `not_primary` put through `ICultNetShardWriteForwarder`, and `CultNetSchemaWriteForwarder` opens its own client connection (`CultNetShardWriteForwarding.cs:122-128`). The primary therefore sees the forwarding runtime, never the original peer.
- **F16. Consumers of the remote write door.** No C# consumer outside CultLib constructs `CultNetDatabaseServer` (grep of the Aetheria, Epiphany, Ghostlight, huginn and gamecult-ops `*.cs` files finds only vendored CultLib copies). In CultLib, tests reach the put and delete handlers directly in `CultNetJournalTests.cs:738`, `:935-936` and `SoulP4cProbeTests.cs:57`. `CultNetDatabaseServer.ApplyPutAsync` and `ApplyDeleteAsync` (`:73`, `:81`) have no caller in `src` or `tests` besides the handlers. TypeScript puts (the `cultnet-ts` Idunn presence and signed health publishers) target Rust servers.
- **F17. The Rust document server admits a put by size only.** In `packages/cultmesh-rs/src/rudp_document_server.rs:682-720`, a `DocumentPutRaw` is refused only when its served response would not fit; no authority is asked. This is F1's gap in the Rust runtime (a follow-up, not stage 1).
## Cut sequence (stage 1)
One line per cut not yet mapped. The model page has no empty cell as of 2026-10-28. Remote write authority (`cultmesh-garden-path:cut_spec:cut-remote-write-authority.r1`, mapped 2026-10-28, so no longer a line here) goes first: it waits on nothing, every later server cut stands on a closed write door, and GP1 shows the door open today. The wasm measuring probe is the sibling that may run beside it, since it touches no file this cut does; ruling `browser-sim-runtime` names it the first cut of the browser runtime track, not of stage 1 as a whole.
- **Wasm runtime measuring probe.** Download size, startup and 60 Hz tick cost of a stub orbs tick under Mono browser-wasm and NetWasm, including `CultCacheBatch` commit cost at tick rate. This is the first cut named by ruling `browser-sim-runtime`, which it can overturn. Its NetWasm and AOT variant waits on `cultlib-gaps:follow_up:cs-aot-safe-registration`.
- **Witness services off in server mode.** `CultMeshGameSession` stops constructing the observation server and fact committer when the declaration has no checker (F7). Waits on nothing; can run beside remote write authority.
- **Leases as typed state and the lease-derived grant.** `CultMeshAuthorityLease` becomes a CultCache document, the lease catalog a store view, and the Mesh host's `RemoteWriteAuthority` resolves the connection's principal through `CultMeshAuthorityResolver`. Waits on remote write authority and on session and principal.
- **Demote `CultNetClientAuthorityScope`.** It becomes derived from the authority declaration (empty in server mode). Waits on game definition.
- **Changelog entry for the remote write refusal.** `unity/org.gamecult.cultlib/CHANGELOG.md` is also written by `variants:cut_spec:cut-write-ownership-cs.r1`, so Self adds this cut's bullet at merge, after that cut lands, and the two specs share no file.
- **Game definition and server tick runner.** Builder, document and system registration, definition document and hash, fixed tick, tick-boundary commit. Rests on rulings `tick-model`, `tick-state-commit` and `room-durability`; waits on nothing mapped.
- **Session and principal.** Join, resume and leave; principal proof; session document. Rests on ruling `player-identity`; waits on game definition (the join checks the definition hash).
- **Input and snapshot path.** Input messages with redundancy, a server input buffer, acknowledgements, delta snapshots. Rests on ruling `browser-transport`; waits on session and principal.
- **Browser client runtime.** Wasm simulation host, TypeScript shell for transport and rendering, prediction with replay reconciliation. Waits on the measuring probe and `browser-client-store`.
- **Orbs reference game and garden-path proof.** The game written with no networking code (`garden-path-needs-no-networking`), and a browser client that writes (`browser-is-first-class`). Waits on all of the above.

View file

@ -0,0 +1,153 @@
# CultMesh garden path: target
Status: draft target for campaign `cultmesh-garden-path`, 2026-10-27. It holds the
ends, not the means. The code below is illustrative: it shows what a developer
should have to write, and no name in it is decided.
## The problem
Operator, 2026-10-27: "the value is in solving the multiplayer scaling problem
for indie developers in an ergonomic fashion. Having one authoritative server is
fine, up to a point, but there's no real open source playbook on how to ship an
MMO, and existing solutions like SpacetimeDB have a huge ergonomics gap. I want
CultMesh to feel like magic: as a garden path, you annotate your domain POCOs and
simulation code and declare your authority structure ... and it'll all just work
without the developer needing to understand networking deeply."
Prior art: `research/cultmesh-garden-path/prior-art-2026-10-27.md` (frameworks,
ergonomics, scale-out) and `research/witness-authoritative-networking/` (consensus
and P2P anti-cheat). What the field shows, in short:
- One API over several authority modes ships today (Photon Fusion: Server, Host,
Shared; Unity NGO: client-server and distributed authority), and in both the
modes leak into the developer's code (`IsServer` against `HasAuthority`,
features missing in one mode).
- SpacetimeDB gives one machine per database. Scaling past it, client
prediction, and the reducer-shaped rewrite of game code are left to the
developer.
- Determinism, where it is required at all, is required of the whole simulation
(Croquet/Multisynq, Photon Quantum), and that tax is the main ergonomic cost.
- Shipped world-scale designs treat a spatial unit as indivisible and move
players between units (EVE, Pax Dei, Folia); nobody publishes an open,
end-to-end playbook (Astron, the nearest, is discontinued).
## What a developer writes (illustrative)
The reference game is a small browser .io game (operator, 2026-10-27: "the .io
game, Aetheria later"): players steer orbs, eat pellets, and shoot.
```csharp
[CultDocument("orbs.player", "orbs.player.v1")]
public sealed class Player
{
[CultIndex] public string Id { get; set; } = "";
public Fix2 Position { get; set; }
public Fix2 Velocity { get; set; }
public int Mass { get; set; }
}
[CultDocument("orbs.shot", "orbs.shot.v1")]
public sealed class Shot
{
[CultIndex] public string Id { get; set; } = "";
public string Shooter { get; set; } = "";
public Fix2 Origin { get; set; }
public Fix2 Direction { get; set; }
}
// Runs every tick. Predicted on the owning client, corrected by authority.
public sealed class Movement : CultSystem
{
[Predicted]
public void Step(Player player, PlayerInput input, Tick tick) { /* ... */ }
}
// A non-owner may re-run this to settle a dispute, so it must be
// deterministic. The analyzer enforces that on this system only.
[Adjudicable]
public sealed class Hits : CultSystem
{
public HitResult Resolve(Shot shot, Rewind<Player> target) { /* ... */ }
}
var game = CultMesh.Game("orbs")
.Documents<Player, Pellet, Shot>()
.Systems<Movement, Eating, Hits>()
.Authority(Authority.Server);
// .Authority(Authority.TrustedCoop);
// .Authority(Authority.Witnessed(w => w.Challenger(Party.Victim).Quorum(3).Arbiter(Party.Server)));
// .Authority(Authority.Regions(r => r.Grid(512).Handoff())); // later: past one server
```
Changing the authority line is the whole migration between modes, unless the new
mode re-executes a system on a non-owner and that system is not yet
deterministic. Then the analyzer names exactly those systems.
## Invariants
- `one-model`: server-authoritative, trusted co-op, witnessed and regioned
authority are configurations of one authority declaration, answering per
document: who writes, who checks, who arbitrates. No mode has its own API for
state, systems or queries.
- `modes-do-not-leak`: game code does not branch on the authority mode or ask
"am I the server"; a system states what it does and the runtime decides where
it runs. Where a mode cannot offer a capability, the declaration is refused at
startup with a reason, never degraded silently at runtime.
- `determinism-is-per-system`: only code a non-owner may re-execute must be
deterministic. That requirement is marked on the system and checked by an
analyzer at build time, not by documentation or by desync in production.
- `state-is-cultcache`: game state is ordinary CultCache documents; the
framework adds no second state model and no JSON on the load-bearing path.
- `authority-is-typed-state`: who holds authority over what is CultMesh state
(leases, shard primaries), inspectable and revocable, never inferred from
transport success or peer catalogs.
- `garden-path-needs-no-networking`: the reference game is written without the
developer touching sockets, serialisation, tick scheduling, prediction or
reconciliation code.
- `honest-guarantees`: each mode states its fault model (trusted server, trusted
peers, honest-majority sampled witnesses with server arbitration) and its
documentation and tests claim nothing past it.
- `browser-is-first-class`: the reference game's client runs in a browser and can
write (submit inputs, own predicted state), not only watch.
- `built-for-load`: what the garden path ships that is database-shaped (the
tick commit, the batch door, logs, subscriptions) is architected for
unreasonable load from the start (operator 2026-10-27,
`cultlib-gaps:ruling:database-shaped-built-for-load`): "if what we're shipping
is database shaped, people will use it as such".
## Sequence (each a campaign or stage of its own; one foundation at a time)
1. Server-authoritative garden path for the .io game: documents, systems,
ticked scheduling, client prediction and reconciliation, browser client that
writes. This is the proof of ergonomics.
2. Trusted co-op: authority moves to the owning peer; same game code.
3. Deterministic dispute kernel (CultMath fixed-point hit tests, cross-runtime
vectors) and the `[Adjudicable]` analyzer.
4. Witnessed authority: signed, hash-chained inputs; victim challenge; sampled
network-near witnesses; server arbitration; browser-to-browser channels.
Demonstration: a cheating client caught and corrected.
5. Regions: spatial authority handoff and interest management past one server.
6. Aetheria on the same declaration.
## Not in scope
- Aetheria until stage 6. Eve and Gjallar (deprecated).
- A new CultCache storage engine (the paged single-file store stays postponed,
`variants:ruling:page-tree-store-postponed`). The garden path fills gaps in
their owners, built for load.
- Asynchronous Byzantine total ordering, zero-knowledge proofs, and chains of
any kind.
- The existing witness aggregator (`CultNetSimulationConsensus`, unsigned and
weighted by observed votes) as a base for stage 4: it is the trusted co-op
first stage by design and will be replaced, not extended.
## Open forks (to the Fork Desk, each with prior art first)
1. Where simulation code runs in the browser: C# compiled to WebAssembly (one
implementation everywhere, which also shrinks the determinism problem to
fixed-point maths, at the cost of runtime download size), C# on the server
with TypeScript systems in the browser (two implementations of each predicted
system), or another arrangement.
2. Server transport for the browser: WebSocket as today, or WebTransport
(QUIC datagrams) on the provider side.
3. Tick model: fixed tick with a declared rate, or per-system rates.

View file

@ -0,0 +1,286 @@
# TypeScript CultMesh Content Plane: Map
Campaign `cultmesh-ts-content`. Imagination wrote this on 2026-10-19 at CultLib
`8990bd46` (origin/main `a88c629e` plus the target doc), and refreshed it the
same day at `2de07065` for C2 and C3, reading C1 from `b8b825e1`. The target
is `docs/cultmesh-ts-content-target.md`. The HTTP prior art is
`docs/research/2026-10-19-content-addressed-http-answers.md` (cited below as
**R**, with its section number). This page holds the body facts, the model
page, the rationale and the cut sequence. Specs and questions live in the
typed mind, not here.
## 1. Body facts
F1-F18 were probed by running code or read from source at `8990bd46`. F19-F27
were probed or read at `b8b825e1` (C1's head) and `2de07065`. F28-F33 were
probed on 2026-10-19 over the C2+C3 merge tree (`7adbf3cb` merged with C3's
head, extracted by `git archive`), on Windows and in a Linux container. Scratch probes
ran under the session scratchpad (`imagination/`), not in the repo.
| # | Fact | Evidence |
|---|---|---|
| F1 | A C#-packed manifest is a MessagePack **fixarray of 10** (`0x9a`): `[artifactId, kind, version, contentHash, sizeBytes, mimeType, createdAtUtc, chunks[], tags[], metadata{}]`. Each chunk ref is a 4-array `[chunkHash, offset, sizeBytes, recordKey]`. The fixture manifest is 1,111 bytes: 6 chunks (5 x 262,144 + 4,831), content hash `f4ae2a7c…2af1`. The empty body packs to 315 bytes with one zero-size chunk whose hash is `e3b0c442…b855` (the SHA-256 of nothing). | probe: `@msgpack/msgpack` 3.1.3 decode of `contracts/cultmesh/content-vectors.cs-written.json`, kind `manifest` |
| F2 | `@msgpack/msgpack` 3.1.3 re-encodes all four C# manifest vectors **byte-identically**, metadata included (`zeta, alpha, mid` keep insertion order). Integers come out minimal (`4831` is `cd12df`, `262144` is `ce00040000`), as MessagePack-CSharp writes them. | same probe, `encode(decode(bytes))` equals the input for all four |
| F3 | Three decoder hazards for a TS manifest reader. (a) JS objects put integer-like keys first (`{zeta,"2",alpha,"1"}` iterates `1,2,zeta,alpha`), so metadata must be ordered entries, not an object, or a `{"10":…,"2":…}` manifest re-encodes in another order. (b) Plain `decode` takes the last of duplicate map keys silently (`{"a":"2"}`); C# `Dictionary` and Rust `ordered_map` refuse duplicates. `new Decoder({ mapKeyConverter })` can refuse them: probed. (c) Plain `decode` rounds int64 above 2^53 (`…993` reads as `…992`); `useBigInt64: true` keeps it exact. | probe (node) |
| F4 | `@msgpack/msgpack`'s decoder is iterative: a 1,000,000-deep `0x91` input decodes in 160 ms but costs about 158 MB of heap. So nesting does not crash it but amplifies memory about 150 times. `checkPayloadDepth` (the owner of ruling `payload-depth-limit-256`) is not exported from any `@gamecult/cultcache-ts` entry: `document.ts:108` exports only the error and the constant. | probe (node); source `packages/cultcache-ts/src/document.ts:2,108,123` |
| F5 | The C# HTTPS connector sends `GET <endpoint>?chunkHash=<hex>&recordKey=<escaped>`, **without** a `sha256:` prefix, because `BuildChunkUri` runs `NormalizeHash` first. Probed URIs: `https://ygg.example/content?chunkHash=5993ac…&recordKey=mesh%3Acdn%3Achunk%3A5993ac…`. An existing endpoint query is kept and joined with `&` (`?tenant=sp&chunkHash=…`). A null record key sends `recordKey=` (empty). Other characters use `Uri.EscapeDataString`, so on the wire `k with space/ü&=?#` becomes `k%20with%20space%2F%C3%BC%26%3D%3F%23`. | probe: `BuildChunkUri` lines copied into a net10.0 console (`CultMeshHttpsContentTransport.cs:114-122`) |
| F6 | The connector calls `EnsureSuccessStatusCode` and fails on any status outside 2xx. When `Content-Length` is present and is not the chunk's `SizeBytes`, it throws before reading. With no `Content-Length` it copies the stream without checking the length. Verification belongs to the caller's transfer owner. `CanConnect` admits only `https`. The constructor takes a `Func<HttpMessageHandler>`, so a test can pin a self-signed certificate without modifying the connector. | source `CultMeshHttpsContentTransport.cs:18-29,92-104` |
| F7 | `NormalizeHash` (C#) strips a case-insensitive `sha256:` prefix *before* trimming, then `Trim().ToLowerInvariant()`. It never checks for hex or a length of 64. So the record-key grammar accepts any non-blank string, and a provider must add its own hex check before a hash can become a file name. | source `CultMeshCdn.cs:549-560` |
| F8 | `StableToken` (the token in a manifest record key) is a C# rule with no Rust mirror: `cultnet-rs/src/content.rs` builds no artifact record key. Its Unicode semantics differ from the naive JS spelling in four probed ways. .NET `ToLowerInvariant` leaves `U+0130` unchanged, while JS gives `i` plus `U+0307`. .NET `Trim` keeps `U+FEFF` (and trims `U+0085`, per `char.IsWhiteSpace`), while JS `trim` drops `U+FEFF` and keeps `U+0085`. JS whole-string lowercasing applies final-sigma (`ΑΣ` becomes `ας`), while per-unit lowercasing gives `ασ`. `char.IsLetterOrDigit` is false on a lone surrogate and on `U+00B2`, which JS `\p{L}\p{Nd}` agrees with only when it is tested per UTF-16 unit. | probe (net10.0 console and node); source `CultMeshCdn.cs:562-580` |
| F9 | `CreatedAtUtc` defaults to `DateTimeOffset.UtcNow.ToString("O")`, which looks like `2026-10-19T12:07:25.5235579+00:00`: 7 fractional digits and `+00:00`, never `Z`. | probe (net10.0) |
| F10 | `contracts/cultmesh/content-vectors.cs-written.json` and `.rs-written.json` are written by `tests/GameCult.Mesh.Tests/CultMeshContentVectorTests.cs` and `packages/cultnet-rs/tests/content.rs`, under `CULTNET_WRITE_VECTORS=1`. Each side judges both files. Kinds are `request`, `response` and `manifest`. Rust panics on an unknown kind (`tests/content.rs:302`), and C# requires the Rust labels to equal its own (`CultMeshContentVectorTests.cs:54`). **So a new kind or label in the cs-written file breaks both siblings.** A TS file must be a separate file. | source |
| F11 | **Neither content-vector suite runs in CI.** `cultmesh-portability.yml` filters `dotnet test` to `AuthorityProof`, `RealtimeWireProtocol` and a lease churn test. `cultnet-interop.yml` runs `cargo test … rudp` (a name filter) and the Networking `Rudp` filter. The C#/Rust content parity has only ever been run by hand. | grep of `.github/workflows/*.yml` |
| F12 | TS→C# vector precedent: `scripts/write-cultmesh-realtime-frame-vectors.mjs` writes `realtime-frame-vectors.ts-written.json` from built `dist`, deliberately from **different inputs** than the C# file. `CultMeshRealtimeWireProtocolTests` judges it, and `cultmesh-portability.yml:145-149` runs it. | source |
| F13 | Cross-process precedent for C# against a Node provider exists. `packages/cultnet-ts/test/interop/cultnet-interop.test.ts:536-560` spawns the TS QUIC provider, then runs `dotnet test --filter NativeConnectorReceivesFromExternalProvider` with the endpoint in `CULTMESH_NATIVE_EXTERNAL_ENDPOINT`. The C# test skips without the variable (`tests/GameCult.Mesh.Quic.Native.Tests`, line 21). | source |
| F14 | `cultnet-ts` is CommonJS. Its `./wire`, `./contracts` and `./authority` subpaths import only `@msgpack/msgpack`, `ajv` and `cultcache-ts/document`, so they are browser-safe. Its root `index` pulls `node:` modules (`peer`, `rudp`, `transport`, `security` and others). `cultmesh-browser` (ESM) already imports `cultnet-ts/wire`, `/contracts` and `/authority`. `cultmesh-ts` loads `koffi` lazily, inside a function (`realtime-quic-native.ts:139`), so a new subpath of it pays no native load. The root has `esbuild` ^0.28.1 as a devDependency. | source, grep |
| F15 | TS packages build with `tsc` and test with `node --test dist-test/test/*.test.js` per workspace. `scripts/run-typescript-workspaces.mjs` runs them in order `cultcache-ts, cultnet-ts, cultmesh-ts, cultmesh-browser`, after `check-typescript-tests-wired.mjs`. No StrykerJS config exists in CultLib (`docs/mutation-testing.md:156-159`). | source |
| F16 | **No CultLib TS package except `cultcache-ts` has a publish job.** `publish-packages.yml` triggers on `cultcache-ts-v*`, `cultcache-py-v*`, `cultnet-py-v*` and `cultmesh-py-v*` only. Tags `cultnet-ts-v0.1.1` and `cultmesh-ts-v0.1.1` exist, but no workflow consumes them. `npm view` returns 404 anonymously for `cultnet-ts`, `cultmesh-ts`, `cultmesh-browser` and even `@gamecult/cultcache-ts` (`@gamecult/cultcache-ts` may be private; unverified). The doctrine's rule of at most three tags per push stands (SKILL.md:674). | source; `npm view` |
| F17 | StreamPixels consumes CultLib as the git submodule `vendor/CultLib` (gitlink `30ee8b97`) with `file:` deps and `pnpm.overrides`. Its CI and Idunn recipes build the vendored workspaces (`npm ci --ignore-scripts`, then build `cultcache-ts`, `cultnet-ts` and `cultmesh-ts`). `apps/web` depends only on `cultnet-ts` today, not on `cultmesh-browser`. | StreamPixels `.gitmodules`, `apps/*/package.json`, CI workflow lines 62-95 |
| F18 | The Rust reader sets two guards that the TS reader should match. A caller cap `max_bytes` is checked before any request. And a zero-size chunk is refused unless it is the only one, because a manifest of zero-size chunks would be a request flood. | source `cultnet-rs/src/content.rs:364-385` |
| F19 | `BuildChunkUri` on more endpoint shapes, as `AbsoluteUri` (what `HttpClient` sends). `https://h` and `https://h/` both give `https://h/?chunkHash=…&recordKey=k`. `https://h:443/x` drops the default port. `https://H.Example/X/` lowercases the host and keeps the path's case. `https://h/x#frag` keeps the fragment after the query. `https://h/x?a=%20b+c` keeps the existing query verbatim and joins with `&`. **`https://h/x?` (an empty query) gives `https://h/x?&chunkHash=…`**, because `UriBuilder.Query` is `?`, which is not blank. `https://[::1]:8443/c` keeps the bracketed host. `Uri.EscapeDataString("!'()*~-._ +é𐐀")` is `%21%27%28%29%2A~-._%20%2B%C3%A9%F0%90%90%80`: unlike JS `encodeURIComponent`, it escapes `!'()*`. | probe: `dotnet fsi` on .NET 10, the three lines of `BuildChunkUri` (`CultMeshHttpsContentTransport.cs:114-122`) copied verbatim |
| F20 | Node 24.15 global `fetch` (undici) against a raw-socket server. A gzip body (`Content-Encoding: gzip`, `Content-Length: 29`) arrives as 1,000 decoded bytes, while `headers.get("content-length")` still says `29`. A body shorter than its `Content-Length` (400 of 1,000) rejects while reading: `TypeError: terminated`, cause code `UND_ERR_RES_CONTENT_LENGTH_MISMATCH`. A body longer than its `Content-Length` (1,000 against 400) is cut silently to 400 by HTTP framing. With no `Content-Length`, all 1,000 bytes arrive. So a reader that trusts the header refuses valid gzipped content, and it learns nothing the received bytes do not tell it (R §6: Fetch decodes content codings, and its own note calls `Content-Length` unreliable). | probe: `imagination/fetchprobe.mjs` |
| F21 | The C# TCP content server is the reference for what a provider does per request (`SendChunkAsync`, `CultMeshTcpContentTransport.cs:384-430`). It runs `NormalizeHash` on the requested hash, refuses a non-blank `RecordKey` that is not `CreateRecordKey(hash)` (ordinal compare, message "record key disagrees with its content hash"), resolves the chunk **by hash** (`_resolveChunk(hash)`), and runs `CultMeshCdn.ValidateChunkPayload` before it sends a byte. So **verify-before-serve on every request is the reference behaviour**. C# chunk storage is one record per chunk, keyed by the chunk record key (`CultMeshDatabaseContentProvider`, `CultMeshContentTransfer.cs:41-71`). Per-chunk storage is the reference layout. | source |
| F22 | C1's exports at `b8b825e1` (`packages/cultnet-ts/src/content.ts`): `normalizeHash`, `parseSha256Hex`, `sha256Hex`, `chunkRecordKey`, `artifactRecordKey` (async), `validateManifest`, `encodeManifest`, `decodeManifest`, `packContent`, `CULTMESH_CDN_MANIFEST_SCHEMA`, `DEFAULT_CHUNK_SIZE_BYTES` (4,194,304), `EMPTY_SHA256`, and the types `CultMeshCdnChunkRef`, `CultMeshCdnArtifactManifest`, `CultMeshCdnPackOptions` and `CultMeshCdnPackedChunk`. `sha256Hex` throws a plain `Error` ("WebCrypto SHA-256 is unavailable.") when `crypto.subtle` is missing. `validateManifest` takes the whole manifest (it reads `metadata`). `cultnet-ts/authority` already exports `isLoopbackEndpoint` (`cultmesh-authority.ts:377`: `localhost`, `loopback`, `::1`, `::ffff:7f00:1`, `127.x.x.x`), and `cultmesh-browser` already imports it (`index.ts:23`). | `git show b8b825e1:…`; cut report `cut-c1-shapes.h1` landed names |
| F23 | The consumer (StreamPixels `dfbe2a6`, `docs/web-character-customization-map.md`) stores one whole-artifact file per content hash at `/srv/streampixels/content/<hex>`, builds manifests at service boot, and hashes every referenced blob at boot (lines 149-159, 175, 317-325). Its artifacts are 10-125 KB (W10), well under the 4 MiB default chunk. It also builds manifests for `catalog.cc` and `character-artifacts.cc`, whose sizes the map does not give. It mounts the handler at `GET /api/content` on its existing server (line 320). Its sketch, `createHttpsContentHandler(store)` (line 293), takes `(chunkHash, recordKey)` and returns bytes or not-found. | StreamPixels `git show dfbe2a6:docs/web-character-customization-map.md` |
| F24 | Build shapes for the two packages. `cultmesh-ts` is CommonJS (`module` and `moduleResolution` `Node16`), and its `package.json` exports only `.` (lines 7-13). `cultmesh-browser` is ESM (`"type": "module"`, `moduleResolution` `Bundler`) and is one file, `src/index.ts`, with no relative imports. A new relative import must therefore be spelled `./content.js`, or Node's ESM loader cannot find it at test time. Both packages' `test` scripts run the glob `dist-test/test/*.test.js`, which the wiring check accepts (`scripts/check-typescript-tests-wired.mjs`). CI runs both in `cultmesh-portability.yml`: the browser at lines 119-120, and `cultmesh-ts` at lines 121-129 under the step name "Verify the realtime frame codec with the TypeScript codec". `esbuild` resolves from the root `devDependencies`, as C1's browser-bundle test shows. | source at `b8b825e1` |
| F25 | The existing TLS fixture `packages/cultmesh-ts/test/fixtures/quic-test.p12` is a self-signed P-256 certificate with subject `CN=cultmesh-quic-native-tests` and no subjectAltName (fixtures README). Node's TLS hostname check therefore refuses it for `127.0.0.1` even when it is trusted through `NODE_EXTRA_CA_CERTS`. A C# handler that pins by certificate hash does not care about the name. | source: fixtures README |
| F26 | `CultMeshCdn.ValidateChunkPayload`, `HashBytes`, `ValidateManifest` and `NormalizeHash` are `internal`, and `GameCult.Mesh` declares no `InternalsVisibleTo`. A test in `tests/GameCult.Mesh.Tests` can therefore verify chunk bytes only with `SHA256.HashData`, or through a public consumer of the connector. The public surface is `CreateRecordKey`, `PackArtifact`, `PublishAsync` and `ReadArtifact(Async)`. | grep of `CultMeshCdn.cs`, `src/GameCult.Mesh` |
| F27 | No CultLib workflow runs `cultmesh-ts` interop tests. `cultnet-interop.yml` (windows-latest, main only) runs `cultnet-ts`'s `test/interop`, and its QUIC native case spawns `dotnet test --filter NativeConnectorReceivesFromExternalProvider` through `runDotnetTestFilter` (`cultnet-interop.test.ts:550,2826`). `cultmesh-ts` has a `test/interop/` folder (a QUIC peer script) but no `test:interop` script. | grep of `.github/workflows`, source |
| F28 | **The real provider meets the real reader across processes, on Windows and Linux.** C2's handler, store and `toNodeListener` served on `node:https` with a self-signed P-256 certificate (subjectAltName `IP:127.0.0.1, DNS:localhost`, `CA:FALSE`, `serverAuth`, 100-year validity). They served a 4-chunk artifact (3.5 x 64 KiB) and an empty one. C3's `fetchVerifiedContent` ran in a child `node` with `NODE_EXTRA_CA_CERTS` set to that certificate, through the real global `fetch`, and returned both bodies with the right hashes. Without the variable it rejected `network` (cause `DEPTH_ZERO_SELF_SIGNED_CERT`). Behind a test wrapper that flips byte 0 of every non-empty 200 body, it rejected `chunk-hash-mismatch`. Platforms: Windows (Node 24.15) and a `node:24-trixie` container (Node 24.21). | probe: `imagination/probe/{serve,read,drive}.mjs` over the C2+C3 merge tree `8d1482c8` |
| F29 | **The C# consumer that verifies is `CultMeshContentTransferService`, and it runs against an HTTPS route without Odin.** The probed path: `CultMeshDiscoveryService` over a test `RouteSource(<https endpoint>)`, then `CultMeshSessionManager` with `LocalDevelopment` trust and the unmodified `CultMeshHttpsContentTransportConnector`, then `CultMeshSessionContentProvider`, then `FetchAsync(manifest)`. This is the shape of `CultMeshContentSessionTests.TcpContentSessionStreamsLargeBodyAndReusesCommittedBody` with the TCP connector swapped for the HTTPS one. It fetched both artifacts from the TS provider, decoding the **TS-encoded** manifest bytes with `CultDocumentMessagePackSerialization.DeserializeUntyped`. Trust was a `SocketsHttpHandler` whose `SslOptions.CertificateChainPolicy` is `CustomRootTrust` with only the fixture certificate in `CustomTrustStore` (`RevocationMode.NoCheck`). That is real chain and name validation, with no callback and no system roots. The default handler failed with `UntrustedRoot`. Against the tampering wrapper, `FetchAsync` threw `AggregateException` over `InvalidDataException` ("CDN artifact chunk hash does not match its manifest."), leaving only `.<hash>.partial`, never `<hash>.body`. The connector itself does not verify (F6). The C# verifier is the transfer service's `BoundedHashingWriteStream` per chunk and its whole-file hash. The results on the Linux container (self-contained linux-x64 publish) were identical. | probe: `imagination/csprobe/Program.cs` and `probe/drive-cs.mjs`; source `CultMeshContentTransfer.cs:115-330`, `CultMeshContentTransports.cs:138-160` |
| F30 | The two consumers request the same URLs. C# sent five requests: four chunks, plus the empty chunk `e3b0…`, which the provider answers intrinsically. Each was `/api/content?chunkHash=<hex>&recordKey=mesh%3Acdn%3Achunk%3A<hex>`. The reader sent the same four chunk URLs, and nothing for the empty chunk, which it verifies locally. | same probes, the provider's request log |
| F31 | **Build order decides where the cross-process test lives.** `scripts/run-typescript-workspaces.mjs` builds `cultcache-ts, cultnet-ts, cultmesh-ts, cultmesh-browser`, and each package's `test` compiles `test/**/*.ts`, `test/interop` included. A `cultmesh-ts` test that imports `cultmesh-browser` therefore fails to compile on a clean checkout, while a `cultmesh-browser` test that imports `cultmesh-ts/content` follows the order. Named ESM imports of the CommonJS `cultmesh-ts/content` work from an ESM file: all four provider names import as functions. At `7adbf3cb`, `cultmesh-portability.yml` runs the browser step (lines 119-120) **before** the `cultmesh-ts` step (129-130), which is the opposite of the script's order. The wiring check requires `test/interop/*.test.ts` to be run by a `test:interop` script. | probe (node, ESM import); source |
| F32 | Node's `execFile("dotnet")` resolves `dotnet.exe` on Windows without an extension. The C3 merge into `7adbf3cb` is clean at C3 head `67ff3f64` (`git merge-tree` gives `666162a3`). It touches the workflow only at comment lines 105-107, and the reader's exports keep their names. The probe's body pattern `(i*31+7)&255` repeats every 256 bytes, so all of its 64 KiB chunks shared one hash. A parity fixture needs non-periodic bytes, or chunk order is invisible. | probe (node); `git merge-tree` |
| F33 | `cultmesh-portability.yml` triggers on `push` only for `main` and `codex/**`. A `hands/**` branch runs it through `pull_request` or `workflow_dispatch`, and both triggers exist. The job `reactive-and-resource-lifetime` has .NET 10 and Node 24 on `windows-latest` and `ubuntu-latest`, and it already builds `tests/GameCult.Mesh.Tests` in earlier steps. | source at `7adbf3cb` |
## 2. Model page
There is one row per persistent or wire-visible kind. Identity is what names the
kind, lifecycle is what happens to it over time, and authority is who decides.
The HTTP answers are new design: no runtime has an HTTPS content server (target,
"Why now"). They rest on R, which fetched the specs and sources on 2026-10-19
and corrected this page's first draft (R §8). In short:
- **404 for absent** is universal (R §9). Nix also reads 410 and S3's 403 as
absent; we send 404 only.
- **400 for a malformed id** comes from the IPFS path gateway (generic client
error, with a malformed CID as its example) and bazel-remote (a hash that is
not 64 lowercase hex). The OCI blob-pull text defines no 400; its
`DIGEST_INVALID` is an upload code, so the first draft was wrong to cite it.
- **No system specifies a server answer for a stored object that fails its
digest** (R §9). Every one puts verification on the client. bazel-remote
streams without checking, so a failure after the headers have gone "cannot
change the status" (R §4). That is the scar `verify-before-serve` avoids by
buffering and hashing before the status line.
- **Range** may be ignored (RFC 9110 §14.2). OCI says registries SHOULD
support it, which the first draft called "optional". We ignore it.
- **`Cache-Control: public, max-age=29030400, immutable`** is the IPFS path
gateway's MUST for immutable content (R §1). The trustless gateway itself
does not state it.
- **The Fetch facts** decide the reader (R §6). Fetch decodes content codings
before the page sees the body, so `Content-Length` describes the encoded
body, which F20 reproduces in Node. `Content-Length` is CORS-safelisted, and
`ETag` is not. `Access-Control-Allow-Origin: *` passes the CORS check for any
request whose credentials mode is not `include`.
| Kind | Identity (what names it) | Lifecycle | Authority (who decides; forbidden writers) |
|---|---|---|---|
| **Manifest** `gamecult.mesh.cdn_artifact_manifest.v1` | The record key (row below), derived from `artifactId`, `version` and `contentHash`. On the wire it is the 10-array of F1. Its namespace is the schema id. Two manifests for one body under different ids or versions are distinct and legitimate. | Immutable once packed. A new body or version is a new manifest and a new key. Nothing revises one in place, and deletion is not in scope (no GC). `createdAtUtc` is set at pack, and a caller may pin it so that repacking is reproducible. | `packContent` in `cultnet-ts/content` (C1, `b8b825e1`) is the only TS writer. The encoder and decoder in the same module are the only TS byte owners. The reader takes a manifest value and never builds or edits one. **Forbidden:** a second TS manifest type or encoder in `cultmesh-ts`, `cultmesh-browser` or a consumer, and any URL or endpoint field. |
| **Chunk ref** `CultMeshCdnChunkRef` | Its `chunkHash` (SHA-256 of the chunk bytes) within one manifest, plus `offset`. Two refs may share a hash: this is legal, and the reader fetches each ref. | Immutable inside its manifest. `recordKey` is derived and may be blank on read. A blank key goes on the wire as `recordKey=` (F5), and the provider accepts it. | The packer writes. `validateManifest` (C1) judges contiguity, non-negative sizes and the size sum. The reader and provider never edit one. |
| **Chunk record key** `mesh:cdn:chunk:<hex>` | Fully derived from `normalizeHash(chunkHash)`. It is injective only over valid hex, because C# accepts any non-blank string (F7). | Derived on demand, never stored independently of a manifest. | `chunkRecordKey` in `cultnet-ts/content`. On the HTTP wire the provider **checks** a non-blank `recordKey` against it, as the C# TCP server does (F21), and never derives a file from it. |
| **Artifact record key** `mesh:cdn:artifact:<token>:<versionToken\|unversioned>:<hex>` | Derived by `StableToken` (F8). It is not injective: `A/b` and `a b` both give `a-b`, and the content hash at the end disambiguates. | Derived on demand. | `artifactRecordKey` in `cultnet-ts/content` (async; C1 deviation). Truth comes from the C#-written `contracts/cultmesh/cdn-record-key-vectors.json`. Neither C2 nor C3 uses it. |
| **Content hash / chunk hash** | Lowercase 64-hex SHA-256. On input, `normalizeHash` accepts `sha256:`, any case and surrounding whitespace, as C# does. The HTTP boundary and the reader also demand 64 hex through `parseSha256Hex`. | Immutable. It *is* the identity. | `sha256Hex` and `parseSha256Hex` (C1) are the only TS hasher and the only hex rule. The provider and reader call them and keep no copy. The negative greps pin this. |
| **Stored chunk** (provider disk) | File `<root>/<hex>`, flat, one file per distinct chunk hash. This is the reference layout (F21: C# stores one record per chunk). For a single-chunk artifact the chunk hash equals the content hash, so StreamPixels' whole-artifact file `/srv/streampixels/content/<hex>` (F23) is already a stored chunk. A multi-chunk artifact's publisher writes `packContent(...).chunks`, one file each. | Written by the publisher outside the provider, and never written by the provider. Immutable. Deletion has no owner (GC is out of scope). A corrupt or oversized file stays on disk and is never served. | The publisher writes; **the provider is read-only**. The provider verifies every read against its name before serving (`verify-before-serve`, the C# per-request behaviour of F21). A symlink or a replaced file cannot leak anything, because only bytes that hash to the requested name are ever served. **Forbidden:** any path derived from `recordKey`, the URL path or an unvalidated hash. |
| **Provider store index** | **None.** The file name is the index. The directory store maps hex to `<root>/<hex>` on each request, after its own `parseSha256Hex` equality check. No in-memory table exists, so nothing can go stale. | Not applicable: it is computed per request. | `ContentChunkStore` is the port: `readChunk(chunkHash, maxBytes) → Promise<Uint8Array \| undefined>`. `createDirectoryChunkStore(root)` stats before reading and rejects a file over `maxBytes` with `ContentChunkTooLargeError`, so an oversized file is never read into memory. A host may supply another store, for example one indexed from manifests, and the handler verifies whatever any store returns. |
| **The empty chunk** `e3b0c442…b855` | The SHA-256 of zero bytes. | Intrinsic: it needs no file. | The provider answers it as 200 with `Content-Length: 0` without consulting the store, so empty artifacts work against any store (bazel-remote does the same, R §4). The reader verifies it locally and sends no request. |
| **Wire request** | `GET` or `HEAD <mount>?chunkHash=<hash>&recordKey=<key>`. Extra query parameters are ignored, because an endpoint may carry its own (F5). The mount path belongs to the host, and the handler never reads it. | Stateless. | Built by the C# connector or the TS reader. The TS reader builds the URL **byte-identical** to `BuildChunkUri`, `Uri.EscapeDataString` escaping (F19) and the empty-query `?&` join included, so C# and browser clients share HTTP cache entries. |
| **Content coding** | Identity, always. | Not applicable. | Provider: no `Content-Encoding`, and `Cache-Control` carries `no-transform`, which forbids an intermediary to transform the content (RFC 9111 §5.2.2.6). That keeps the C# connector's `Content-Length` check meaningful (F6). The reader does not rely on it: it judges the bytes it received (F20). |
| **Answer 200** (GET) | The body is exactly the verified chunk bytes. | Immutable per URL. | Provider. Headers: `Content-Length: <size>`, `Content-Type: application/octet-stream`, `X-Content-Type-Options: nosniff`, `Cache-Control: public, max-age=31536000, immutable, no-transform` and `Access-Control-Allow-Origin: *`. Octet-stream with `nosniff` keeps a public chunk from rendering as HTML on the host's origin. The IPFS `Content-Disposition: attachment` adds nothing over that for a fetch client, so it is not sent. |
| **Answer 200** (HEAD) | The same headers as GET, with no body. | Same as GET. | Provider. HEAD verifies exactly as GET does, so it can never advertise a corrupt chunk. RFC 9110 §8.6 requires HEAD's `Content-Length` to equal GET's. |
| **No ETag, no 304** | Not applicable. | Not applicable. | Provider. `immutable` with a one-year max-age means browsers do not revalidate. `ETag` is not CORS-exposed (R §6), and the URL is already the content identity. An `ETag` without a 304 path is noise, and a 304 path is a second answer to test. The first draft had both, and both are cut. |
| **Answer 400** | `chunkHash` missing, repeated, or not 64 hex after `normalizeHash`. Or `recordKey` repeated, or non-blank and not exactly `mesh:cdn:chunk:<hex>` (the C# server's "record key disagrees" rule, F21). | Stateless. | Provider. Body is a fixed `text/plain; charset=utf-8` reason with no echo of input (bazel-remote echoes the URL; we do not). Headers: `nosniff`, `Cache-Control: no-store`, `Access-Control-Allow-Origin: *`. |
| **Answer 404** | A valid hash whose chunk the store does not hold. | Stateless; a later publish can turn it into 200. | Provider. Same error headers as 400. `no-store` keeps an absence from being cached as immutable. |
| **Answer 405** | Any method but GET and HEAD, OPTIONS included. | Stateless. | Provider, with `Allow: GET, HEAD`. The reader's GET is a CORS simple request (no custom headers, credentials omitted) and never preflights. A client that adds headers would preflight, and its preflight is refused, which is correct because the provider honours no request header. The first draft's 204 preflight answer is cut. |
| **Answer 500** | The store returned bytes whose SHA-256 is not the requested hash; the store holds a file over `maxChunkBytes`; or the store failed. | Persists until the operator replaces the file. | Provider. It sends a fixed reason and none of the chunk's bytes, then calls the host's `onServerFault({ chunkHash, kind, error })` with `kind` ∈ {`hash-mismatch`, `too-large`, `store-error`}, so the host can fail health. A throwing hook does not change the answer. It is 500 and not 404 because the IPFS trustless gateway reserves 500 for "genuine server errors" (R §1), and corruption is one the operator must see, while a 404 would hide it as absence. |
| **Range** | Not supported. A `Range` header is ignored, and the answer is 200 with the full chunk (RFC 9110 §14.2). No `Accept-Ranges` header is sent. | Not applicable. | Provider. The chunk is the resumption unit, and resumable TS state is out of scope. |
| **Provider memory** | At most `maxChunkBytes` buffered per in-flight request. | Per request. | Provider. Buffering is forced, because the status line cannot be sent before the hash is known (the bazel-remote scar above). `maxChunkBytes` defaults to `DEFAULT_CHUNK_SIZE_BYTES` (4 MiB) and must lie in 1..2147483647. The host bounds concurrency (server `maxConnections` or a reverse proxy), and the README says so. |
| **Reader result / error** | A `Uint8Array` of the whole body, or one `CultMeshContentError` with `code` ∈ {`manifest-invalid`, `too-large`, `insecure-endpoint`, `webcrypto-unavailable`, `network`, `http-status`, `length-mismatch`, `chunk-hash-mismatch`, `content-hash-mismatch`}, plus `chunkHash` and `status` where they apply. A caller abort rejects with the signal's own `reason`, unwrapped. Bad options reject with `RangeError`. | Transient, held in memory only. No byte of a failed chunk or body is returned, in whole or in part (`verify-before-use`). | `fetchVerifiedContent` (and `fetchVerifiedChunk`) in `cultmesh-browser`. Concurrency defaults to 4 and is bounded 1..32 (the C# `CultMeshContentTransferOptions` bounds). `maxBytes` defaults to 256 MiB and is checked before any request. The scheme must be `https:`, or `http:` where `isLoopbackEndpoint` (F22) holds. Only status 200 is accepted. **`Content-Length` is not consulted** (F20). The body is read through a stream into the chunk's slot, a body that runs past the chunk's size is cancelled at the first excess byte, and one that ends short is refused. |
| **Endpoint** | A URL the caller resolved through CultMesh. | Per call; never persisted in content. | The caller. **Forbidden:** any manifest, chunk-ref or record field carrying it (`no-urls-in-content`). Error messages never echo it, because an endpoint query may carry a tenant or a token. |
| **Vector files** | `content-vectors.cs-written.json` (C#), `.rs-written.json` (Rust), `content-vectors.ts-written.json` (TS, manifests only, own inputs) and `cdn-record-key-vectors.json` (C#, record-key traps), all landed by C1. | Each is rewritten only by its writer under `CULTNET_WRITE_VECTORS=1`. CI never sets the variable: the committed files are the contract. | Each writer owns its file. C2 and C3 add no vector file. HTTP behaviour is pinned by tests and, in C4, by the C# transfer service driving the unmodified connector (F29). |
Two cells rest on reasoning rather than a probe: that the host, not the
provider, bounds concurrency, and the cost of hashing on HEAD. Both are
defaults a later cut can revise without changing any shape.
## 3. Rationale
**Where the CDN shapes live: `cultnet-ts/src/content.ts`, exported as
`cultnet-ts/content`.** C1 landed this at `b8b825e1`. Both consumers already
depend on `cultnet-ts`, and its subpaths are the repo's established
browser-safe entry points (F14). The placement matches Rust, where
`cultnet-rs/src/content.rs` owns the same rules.
**Hashing is WebCrypto in one place.** `crypto.subtle` exists in browsers
(secure contexts) and in Node 20+, so one hasher serves the packer, the
provider and the reader. WebCrypto has no incremental digest, so every hash
needs its bytes in memory. That fits all three users: the reader holds the
body (the target keeps artifacts in memory), the provider holds one chunk at a
time, and the packer holds the body.
**Decoding follows the siblings.** C1's report records two places where its
decoder is stricter than MessagePack-CSharp: an exact field count, and refusal
of nil strings. Ruling `leaf-acceptance-follows-reference` may make that a
finding. That belongs to C1's Soul pass, not to C2 or C3, which take manifest
values and never decode bytes.
**No TS CultCache document definition for manifests yet.** This is still
follow-up `ts-manifest-document-definition`.
**Provider: `cultmesh-ts/content`, a request-to-answer core, one store and one
Node adapter, in one file.** `createContentChunkHandler(store, options)`
returns `(request: { method, url }) => Promise<{ status, headers, body? }>`.
The request carries no headers, because the provider honours none: Range is
ignored, and conditional requests have no answer now that ETag is cut. The
core reads only the query, never the path, which is what makes
`chunk-only-route` structural rather than checked. `toNodeListener(handler)`
is about 15 lines for `node:http(s)`, and Express, Fastify or Next mount the
core the same way. The name says "chunk" because the handler answers chunks
only. StreamPixels' sketch name, `createHttpsContentHandler` (F23), implied it
does TLS, and it does not: the host terminates TLS. One file, not three,
because the store and adapter are about 50 lines between them and have no
consumer apart from the core. `cultmesh-ts` is already a Node package, so
`node:fs` in the file costs nothing.
**Store layout: per-chunk flat files.** This is the reference layout (F21),
and it needs no index that can go stale. It coincides with StreamPixels'
whole-artifact files, because every StreamPixels artifact is a single chunk
(F23). The alternative is a store indexed from manifests, mapping a chunk hash
to (artifact file, offset, size), so that multi-chunk artifacts could be
stored whole. It is not shipped, because it adds a derived index that must be
rebuilt when manifests change, and no consumer needs it. A host that wants it
writes about 30 lines against the port, and the handler still verifies every
byte. **Consumer note for StreamPixels B3:** its `catalog.cc` and
`character-artifacts.cc` manifests must stay single-chunk, so their files are
the stored chunk. Either they are packed with `chunkSizeBytes` at least the
file size and the provider's `maxChunkBytes` is raised to match, or B3
publishes the chunk files.
**Verify on every request, not once at boot.** The invariant says "serves
only bytes whose SHA-256 equals the requested chunk hash", and a boot check
cannot see a file replaced after boot. C# verifies per request (F21). A
"verified at boot" cache would be a cache pretending to be truth. StreamPixels'
boot check is its own deploy gate (missing blob means failed health), and it
stays useful beside per-request verification. The cost is one SHA-256 of at
most 4 MiB per request, and the immutable one-year caching keeps repeats away
from the origin.
**Reader: `fetchVerifiedContent(endpoint, manifest, options)` and
`fetchVerifiedChunk(endpoint, chunkHash, sizeBytes, options)` in
`cultmesh-browser`, over one internal per-chunk primitive.** The second is the
"fetch by hash" a general consumer expects for a single-chunk artifact. It
builds no manifest: it validates the hash and size, sends
`recordKey=chunkRecordKey(hash)` so its URL equals a C#-packed manifest's, and
runs the same primitive. The whole-body check is the chunk check, because for
a single chunk the two hashes are the same. Before any request,
`fetchVerifiedContent` runs `validateManifest`, `parseSha256Hex` on every
hash, the zero-size rule (F18) and the `maxBytes` cap. It then fills a private
buffer of `sizeBytes` through a sliding window of at most
`maxConcurrentChunkRequests` requests. It uses `credentials: "omit"` and an
internal `AbortController` chained to the caller's signal, so the first
failure cancels every request still in flight. It hashes each chunk in place
and returns the buffer only after the whole-body hash matches.
**Why the reader ignores `Content-Length`.** The target says the reader
"refuses a response whose length differs from the advertised size before
reading it all". The header cannot carry that duty. Fetch decodes content
codings, and a proxy that gzips in spite of `no-transform` turns a correct
1,000-byte chunk into a 29-byte `Content-Length` (F20, R §6). In a browser the
reader cannot even see `Content-Encoding` cross-origin, because it is not
safelisted, so it cannot tell that case apart. What the invariant protects is
bounded reading and no acceptance of a wrong length. The streaming cap
delivers both from the bytes actually received: the reader stops at the first
byte past the chunk's size, and refuses a short body at its end. The target
sentence should be reworded (reported to Self as a contradiction).
**C3 does not wait for C2.** C3's live test serves from a stub `node:http`
loopback server written in the test (about 30 lines). The stub sends what the
model page says, plus the misbehaviours: gzip, a short body, an endless body,
and wrong bytes. The real provider meets the real reader in C4. This keeps
the two cuts parallel, and it lets C3 test misbehaviours the real provider can
never produce.
**C4: the proof lives in `cultmesh-browser/test/interop`, with the C# half
in `GameCult.Mesh.Tests`.** The orchestrator is Node, following the QUIC
precedent (F13): it starts the provider, runs both consumers and judges what
the provider saw. It lives in `cultmesh-browser` and not `cultmesh-ts`,
because the workspace build order compiles `cultmesh-ts` first (F31). The
browser package can import `cultmesh-ts/content` with types. The reverse
would need either an untyped script or a build-order exception. The provider
runs in its own process rather than the orchestrator's, so each of the three
parties is a separate OS process, and the orchestrator never shares an event
loop with the server it judges. The C# half drives the connector through
`CultMeshContentTransferService` (F29), because the connector alone does not
verify (F6). A parity test that stopped at the connector would show that C#
receives bytes, not that C# refuses bad ones. TLS trust is a committed
fixture certificate, as the QUIC fixture is (F25). The C# side anchors it
through `CustomRootTrust`, which keeps chain and name validation and adds no
code path that can accept a different certificate. A hash-pinning
`ServerCertificateCustomValidationCallback` would skip name validation and
add a callback that has to be read to be trusted. Corruption comes in two
kinds. On disk, the real provider refuses it with 500 (C2's
verify-before-serve). In flight, a test wrapper flips bytes after the
provider has verified them. The in-flight leg is what proves that both
consumers verify.
**"Released"** is ruled: `release-shape-gitlink` (Defaulted by Self).
## 4. Cut sequence
C1 (`cut-c1-shapes.r1`, report `h1` under a Soul pass), C2
(`cut-c2-provider.r1`) and C3 (`cut-c3-reader.r1`) are specified in the mind.
C2 and C3 run in parallel once C1 merges, each in its own worktree from C1's
merge. Both are based on `b8b825e1`, and they touch disjoint files except
`cultmesh-portability.yml`, where their hunks lie 15 lines apart. What
remains:
4. **C4 `c4-parity`** (`cut-c4-parity.r1`, branch
`hands/cultmesh-ts-content-c4-parity` from `7adbf3cb` plus the C3 merge),
waiting on C2 and C3. The spec carries the plan. In one paragraph: a
node:test orchestrator in `cultmesh-browser/test/interop` packs three
artifacts (multi-chunk, single and empty) and spawns the C2 provider as its
own `node:https` process with three mounts: honest, tampered and corrupt
store. It runs the C3 reader in a child with `NODE_EXTRA_CA_CERTS`, once
more without it as a control, and then `dotnet test` over a new
`CultMeshHttpsContentInteropTests`. That suite drives the unmodified
connector through `CultMeshContentTransferService` and trusts the committed
fixture through `CustomRootTrust`. The orchestrator then judges URL parity
and the provider's fault log. CI gains the step on both OSes, and the
browser and `cultmesh-ts` steps swap to follow the workspace order (F31).
5. **C5 `c5-release`**, waiting on C4 and the Soul passes of C2 to C4. Ruling
`release-shape-gitlink`: merged to CultLib `main` with version bumps,
CHANGELOGs and git tags, and no npm publish. Line-level plan:
- Bumps (minor; every change is additive): `cultnet-ts` 0.2.0 to 0.3.0
(`./content`), `cultmesh-ts` 0.1.1 to 0.2.0 (`./content`), and `cultmesh-browser` 0.3.0 to 0.4.0 (the reader).
`@gamecult/cultcache-ts` gains only the `checkPayloadDepth` re-export:
a patch bump, 0.14.0 to 0.14.1 (its version at `b8b825e1`).
- Dependency ranges: `cultmesh-ts` and `cultmesh-browser` move
`cultnet-ts` to `^0.3.0`. `cultnet-ts`'s `^0.14.0` on
`@gamecult/cultcache-ts` already admits 0.14.1 and stays. C4 gives
`cultmesh-browser` a devDependency `cultmesh-ts: ^0.1.1`, which does not
admit 0.2.0, so it moves to `^0.2.0` in the same commit. Otherwise npm
looks for 0.2.0 on the registry and the install fails. `npm install
--no-package-lock`, the four workspace tests and the browser's
`test:interop` run after the bumps.
- A `CHANGELOG.md` per bumped package. None exists in any package at
`b8b825e1`, so C5 creates four, each naming the live surface and not the
history (Keep a Changelog headings).
- A Soul pass over the release diff, then four tags: `cultcache-ts-v…`,
`cultnet-ts-v0.3.0`, `cultmesh-ts-v0.2.0` and `cultmesh-browser-v0.4.0`.
These go in **two pushes** (at most three tags per push, SKILL.md):
`cultcache-ts` first, because its tag triggers `publish-packages.yml`
(F16), and the three plane tags second. Whether the `cultcache-ts` tag's
npm publish job still runs, and to where, is checked before that push;
follow-up `npm-publish-ts-packages` owns the rest.
- The consumer's pickup (StreamPixels bumping `vendor/CultLib` to the
tagged commit) is StreamPixels' B-cuts, not this campaign.
- Size: about 60 lines of version and CHANGELOG text; a short Hands pass.

View file

@ -0,0 +1,101 @@
# TypeScript CultMesh Content Plane Target
Campaign `cultmesh-ts-content`. Self wrote this on 2026-10-19 for the
StreamPixels M1 live character preview. That preview is a three.js page that
loads glTF bodies and accessories by content hash from Yggdrasil.
## Why now
CultMesh moves large immutable artifacts as content-addressed chunks described
by a `gamecult.mesh.cdn_artifact_manifest`. C# packs manifests, serves chunks
over TCP (`CultMeshContentServer`), fetches them over HTTPS
(`CultMeshHttpsContentTransportConnector`), and verifies each chunk and the
whole body (`CultMeshContentTransferService`). Rust speaks the chunk messages
through `cultnet-rs` `fetch_content` and `answer_content_chunk_request`.
TypeScript has none of this: no manifest type, no packer, no provider and no
reader. And no runtime has an HTTPS content *server*; the C# HTTPS connector
has never had a peer.
Standing ruling (StreamPixels `state/map.yaml`, "Content plane", operator
2026-09-15): HTTPS content transport serves both browser and overlay.
Yggdrasil plus SHA-256 verification is the trust boundary for now.
Distributed zero-trust distribution is future work. The catalog never stores
URLs; clients resolve content through CultMesh.
## Target state
- **One TypeScript owner of the CDN document shapes.** It holds the manifest
and chunk-ref types, their MessagePack wire form, `normalizeHash`, the
record-key grammar (`mesh:cdn:chunk:<hex>` and
`mesh:cdn:artifact:<token>:<version>:<hex>`), and the packer. A manifest
that TypeScript packs is byte-identical to the one C# packs from the same
bytes and options. The owner can be imported in a browser, because the
reader needs it there.
- **An HTTPS content provider in `cultmesh-ts`.** It answers
`GET <endpoint>?chunkHash=<hex>&recordKey=<key>` with exactly the chunk's
bytes and a `Content-Length` equal to the chunk's size: the request shape
the C# connector sends (`CultMeshHttpsContentTransport.cs` `BuildChunkUri`).
Its core is framework-neutral: a request maps to an answer, and a host can
mount it on `node:https` or on any HTTP framework. It reads from a content
store port. The store shipped with it serves content-addressed files from a
directory outside git. The provider never serves a byte it has not
verified against its hash, and it never serves anything but a chunk:
there is no generic static-file route.
- **A verifying reader in `cultmesh-browser`.** Given a resolved endpoint and
a manifest, it fetches the manifest's chunks over HTTPS with the C# query
shape. It checks each response's length, each chunk's SHA-256 and the whole
body's SHA-256 with WebCrypto, and returns the bytes or throws a typed
error. No byte that failed its hash reaches the caller, in whole or in part.
It runs in browsers and in Node 20+.
- **Endpoints come from CultMesh, not from content.** The reader takes the
endpoint as an argument that the caller resolved through CultMesh. Manifests
carry no URL.
## Invariants
- `wire-parity`: manifests, chunk refs and record keys are byte-identical
across C#, Rust and TypeScript for the same input. The C# runtime is the
reference. TypeScript writes and reads the shared vectors in
`contracts/cultmesh/`.
- `https-shape-parity`: the C# `CultMeshHttpsContentTransportConnector`
fetches content from the TypeScript provider unmodified, in a cross-process
test. The TypeScript reader fetches the same chunks from the same provider.
- `verify-before-use`: the reader releases no byte that failed its chunk hash
or the whole-body hash. It refuses a body longer than the advertised size at
its first excess byte, and a shorter one at its end. It does not trust
response length headers, because Fetch decodes content codings and a proxy
can rewrite them (research 2026-10-19, section 6).
- `verify-before-serve`: the provider serves only bytes whose SHA-256 equals
the requested chunk hash.
- `chunk-only-route`: the provider answers only chunk requests. A path, a
filename or a hash outside the store can never select a file.
- `no-urls-in-content`: no manifest, chunk ref or record carries an endpoint.
- `one-owner-per-shape`: one TypeScript module owns the CDN shapes, and both
packages consume it. Neither package keeps a private copy.
- `general-purpose`: the plane is judged by what a reasonable consumer
expects, not only by StreamPixels' slice (operator, 2026-09-30). Multi-chunk
artifacts, empty artifacts, cancellation, bounded concurrency and browser
cross-origin use all work.
- `released`: the npm packages StreamPixels vendors are published at versions
that contain this plane. The release is its own cut with a Soul pass before
the tag.
## Not in this campaign
- Odin route verification for content. Single-host trust is ruled for now.
- Resumable or durable transfer state in TypeScript. The browser holds
artifacts in memory.
- Distributed or zero-trust distribution, and peer serving.
- A TypeScript TCP or RUDP content server, and an HTTPS content server in Rust
or C#.
- Garbage collection of stored blobs.
- StreamPixels' own export, publish and service wiring (its B2 and B3 cuts).
StreamPixels is the consumer, not part of this change.
## Consumer
StreamPixels `map/web-customize` (dfbe2a6),
`docs/web-character-customization-map.md` section B1. Each option's artifact is
10-125 KB, and a full avatar is 0.5-0.8 MB. The browser fetches them. The
service serves them from `/srv/streampixels/content/` on Yggdrasil, outside
git, and hashes every referenced blob at boot.

View file

@ -0,0 +1,663 @@
# CultNet error contract: cut map
Status: cut map, not started. Imagination pass 1, 2026-09-30, against CultLib `main` at `8fc74c70`.
This document owns the means. There is no separate target doc: section 1 holds the ends until the
operator rules on section 3. After that, Self moves the rulings and invariants into
`contracts/cultnet/cultnet-distributed-database.md` ("Error Codes") and the schema's own description.
Open: operator questions Q1-Q8 (section 3). No cut is dispatched until Q1, Q2, Q3 and Q8 are ruled. Q4-Q7
change only single emit sites and can be ruled during Cut 1.
Depends on: `hands/cultmesh-put-serve-bound` merging first. It introduces `document_unservable` in Python
(`fd394d54`), a codeless unservable refusal in cultmesh-ts (`0eb7434a`) and `CultMeshRudpRejectionReason` in
cultmesh-rs (`c9cdad12`, `85288a5a`). The cuts below convert those sites. Their anchors are by commit, not by
line, because the branch is not on main.
Found by the put-serve Hands (batch 3, `38000efa`, 2026-09-30); they belong to this map's cuts:
- **Python codes outside the closed code list.** Python refuses with `document_unservable`,
`malformed_document_put` and `snapshot_*_exceeded`. None of these are in the closed `cultnet.error.v0` code
list, and Rust and TS peers refuse to decode an unknown code.
- **Two error-map shapes.** TS sends a two-key error map that its own peers cannot parse. Rust sends the five-key
contract shape. The two agree only at the message level.
- **The session after a refusal.** TS and Python keep the session open after a refusal; Rust sends a goodbye and
ends it.
- **New Rust refusal frame.** The put-serve branch adds a Rust refusal frame, `cultnet.error.v0` carrying the
reason text with no code and no details, sent before the goodbye and acknowledging nothing. Cut 1 gives it a
code.
- Found by the put-serve Soul (merge verdict, 2026-09-30):
- A store `OSError(ENOSPC)` in Python is refused as `malformed_document_put`, with the text "Raw put document could
not be stored." That is a server fault reported as the peer's fault.
- Over RUDP, Python's `document_unservable` details add `fragmentCount` and `maxFragmentCount`, and send
`maxSnapshotBytes: null` when it is unbounded. The map specifies `{responseBytes, maxSnapshotBytes}`.
- `response_unqueueable` (Python, P3) is a new code outside the closed list.
- A TS peer cannot tell "stored, receipt lost" from "refused" (put-serve Soul F4, pass 3).
Map branch: `main`.
---
## 0. What the Body does today (probed)
Every claim in this table was established by running code. The C#, Rust and TypeScript probes ran on
Yggdrasil through `ygg-verify.sh` at `8fc74c70`. The Python bytes came from `msgpack.packb` on the exact
dicts `cultmesh_py/server.py:573-589` and `cultmesh-ts/src/index.ts:5921-5926` build. The six probe inputs:
| Input | Bytes from |
|---|---|
| `cs_cursor_stale` | Rust `tests/error_message.rs` captured C# hex (`cursor_stale`, details `{asOf:1,current:2}`) |
| `cs_routing` | C# `new CultNetErrorMessage { Error, RoutingHint = { Reason="not_primary", Shard=... } }` serialized with `CultNetSchemaMessageSerialization` |
| `cs_codeless` | C# `new CultNetErrorMessage { Error = "boom" }` |
| `cs_unknown_code` | C# with `Code = "document_unservable"`, empty `CultNetErrorDetails` |
| `py_snapshot_byte_limit` | the Python `_error_response` shape: `messageId`, `code: "snapshot_byte_limit_exceeded"`, `details {responseBytes, maxSnapshotBytes}` |
| `ts_two_key` | the cultmesh-ts shape `{schemaVersion, error}` |
Decoded by:
| Input | C# `CultNetSchemaMessageSerialization.Deserialize` | Rust `decode_cultnet_message_from_slice` | TS `parseCultNetMessage` |
|---|---|---|---|
| `cs_cursor_stale` | ok | ok, typed | ok |
| `cs_routing` | ok | ok, **routingHint dropped** (`code: None, details: None`) | ok (untyped passthrough) |
| `cs_codeless` | ok | ok | ok |
| `cs_unknown_code` | ok (free string) | **Err: "not a recognized cultnet.error.v0 code"** | **throws: /code must be one of the allowed values** |
| `py_snapshot_byte_limit` | ok, but **details silently become an all-null object** (`responseBytes`, `maxSnapshotBytes` lost), `messageId` dropped | **Err: not a recognized code** | **throws** (missing routingHint, extra messageId, code enum, details keys) |
| `ts_two_key` | ok | ok | **throws: missing routingHint, code, details** — TypeScript cannot parse the errors TypeScript sends |
Captured C# bytes for the two shapes Rust's vector file does not hold (for Hands' reuse):
- `cs_codeless`: `85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72A4626F6F6DAB726F7574696E6748696E74C0A4636F6465C0A764657461696C73C0`
- `cs_routing`: see the probe; the shard descriptor writes ten keys, nil included.
Other findings, from reading source at `8fc74c70`:
1. **The interop CI lane has been red for 40 consecutive `main` runs.** It fails at "Verify CultNet schema
contracts stayed consolidated" (`.github/workflows/cultnet-interop.yml:67-90`). That step forbids every
`*.schema.json` outside `contracts/cultnet/`, and R-AB deliberately vendors 22 copies into
`packages/cultnet-rs/contracts/cultnet/`. Those copies are held byte-equal by
`packages/cultnet-rs/tests/schema_discovery_contracts.rs:74-99`. Nothing after that step (every
cross-runtime interop test) has run on main in that time. Run `36714768445` is the latest.
2. **The schema contradicts itself.** `contracts/cultnet/cultnet.error.schema.json:29` says the code is
"Additive: a v0 peer that does not know this field ignores it" and, in the same sentence, "a closed
enum: Rust and TypeScript refuse". The closed-enum text came from Soul pass 4d (`3f942a57`). It
described what the code did; no operator ruled it. `docs/` holds no ruling on unknown codes.
3. **Emitters that break the schema:**
- cultmesh-ts sends `{schemaVersion, error}` only, at `packages/cultmesh-ts/src/index.ts:5875-5878`,
`5885-5888`, `5913-5916` and `5922-5925`, and on the branch at `0eb7434a`.
- cultmesh-py `_error_response` (`packages/cultmesh-py/src/cultmesh_py/server.py:573-589`) adds a
top-level `messageId`, omits `routingHint`, omits `code` and `details` rather than sending nil, and
sends eight out-of-enum codes: `unsupported_schema_version` (`:161`), `malformed_document_put`
(`:358`), `unregistered_document_put` (`:365`), `malformed_document_delete` (`:386`),
`unregistered_document_delete` (`:393`), `snapshot_document_limit_exceeded` (`:558`),
`snapshot_byte_limit_exceeded` (`:567`) and `simulation_observations_disabled` (`:596`). The branch
adds a ninth, `document_unservable` (`fd394d54`).
- `packages/cultnet-py/src/cultnet_py/interop_peer.py:409-417` also sends `unsupported_schema_version`.
R-2 (`docs/cultnet-selection-cut.md:2046-2053`) required that code, but it never entered the schema
enum.
4. **A second schema authority.** `packages/cultnet-py/src/cultnet_py/schema_catalog.py:17`, `:272` and
`:357-362` publish a hand-built `cultnet.error.v0` descriptor under the canonical `$id`. It has a
different shape: `messageId`, `code` as any string, `details` open, and only `schemaVersion` and `error`
required. The same file does this for every wire message (about 350 lines). This is recorded as
follow-up F-1, not cut here.
5. **Silent refusals.** cultmesh-rs's RUDP document server returns every rejection to the local caller only
(`packages/cultmesh-rs/src/rudp_document_server.rs:470-590`, `CultMeshRudpApplicationRejection`). The
peer gets nothing and times out. On the branch this includes the new `DocumentUnservable`.
6. **Receivers drop the code.**
- Every C# receiver turns an error into `new IOException(error.Error)` or
`new InvalidOperationException(error.Error)`, so a caller cannot branch on the code. The sites:
`src/GameCult.Mesh/CultMeshBodySessions.cs:310`, `CultMeshContentSessions.cs:178/256-260`,
`CultMeshPeerExchange.cs:430/466`, `CultMeshSnapshots.cs:91/107/222-226/743` and
`CultMeshVerseDiscovery.cs:273`, plus `src/GameCult.Networking/CultNetShardReplication.cs:440/574`.
- Rust reads `Error { error, .. }` at `packages/cultmesh-rs/src/lib.rs:823`.
- TS types `CultNetErrorMessage` as `{schemaVersion, error}` only (`packages/cultnet-ts/src/contracts.ts:167-170`).
- Kotlin has no error type. A request that gets an error back fails a
`require(response.schemaVersion == ...)` (`packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt:1278`,
`:1334`), and the peer's text is lost.
7. **No correlation.** Only Python echoes `messageId`.
- `cultmesh-browser` rejects an error only when exactly one operation is pending
(`packages/cultmesh-browser/src/index.ts:724-731`); otherwise the error is dropped and the operation
times out.
- C# `CultMeshContentSessions.OnError` (`:256-260`) fails every pending request on any error.
8. **The reference is inconsistent with itself.** `src/GameCult.Networking/CultNetDatabaseServer.cs:560`
refuses a variant change with a codeless error. The subscription server refuses the same condition with
`ForVariantUnsupported` (`CultNetDatabaseSubscriptionServer.cs:211/333/366`).
9. **routingHint is never consumed.** No receiver in any runtime reads it. C# builds it at
`CultNetDatabaseServer.cs:638-650`, and Rust's decode discards it.
10. **Consumer audit** (read-only, all of `F:\Projects` except CultLib; no git in Aetheria):
- Outside CultLib, nothing sets `code`, `details` or `routingHint`, and nothing branches on a code.
- Emitters:
- Huginn: `crates/huginn-daemon/src/serve.rs:288/307/312`, Rust struct literals
`Error { error, code: None, details: None }`, pinned at cultnet-rs `8fc74c70`.
- AetheriaEve: `Aetheria.State.Daemon/Program.cs:1737/1778`, C# object initializers.
- Decoders read `.error` only: Gjallar `Program.cs:574`, Mimir
`MimirOdinMoveProofEvidenceRingProvider.cs:80`, Hermodr `hermodr-daemon.cjs:1380` (hand-rolled),
AetheriaEve's smoke test (text match against its own emit text), and Huginn's tests (text match).
- Stale vendored TS:
- `StreamPixels/vendor/CultLib` carries the current closed schema, so any change here breaks its
TS error decode until it is re-vendored.
- `Heimdall/vendor/CultLib` carries a pre-R-X schema with `additionalProperties:false` and no
`routingHint`, so it already refuses every error C# sends today.
- Odin, Idunn, Muninn, Ghostlight and voidbot: zero hits.
## 1. Ends
- **One schema.** `contracts/cultnet/cultnet.error.schema.json` is the only authority on the shape and on the
code registry. C# embeds it (`GameCult.Networking.csproj:19`), TS mirrors it at build time, and Rust
vendors a copy held byte-equal by test. Nothing else publishes a `cultnet.error.v0` shape (F-1 covers
Python).
- **Every runtime emits only schema-valid errors.** This is proven per emit path against the schema, and
byte-equal to the C# reference for every registry code.
- **Every runtime decodes every error it can receive into a typed value.** A known code becomes its typed
variant with its typed details. An unknown code, if Q1 says so, becomes a typed "other" that keeps the
code string and the raw details. No well-formed error is ever refused, and no field is silently dropped
(routingHint, messageId).
- **Code and details have one owner.** The details a message carries are decided by its code. The types in
every runtime make "cursor_stale with a field" unrepresentable. No free-standing code string plus a
grab-bag details object.
- **A refusal is an answer, not a silence** (R-2 precedent, and Q4). A server that refuses a peer's request,
and can still send, sends a coded error that correlates to the request.
- **The public surface is complete in every runtime**, per the consumer-expectation doctrine:
- a typed error value;
- a constructor per registry code;
- a typed exception or error that request APIs raise when the peer refuses, carrying the typed value.
## 2. Identity, lifecycle and authority
| Kind | What names it | Lifecycle | Who decides |
|---|---|---|---|
| Error message | `schemaVersion: cultnet.error.v0`; correlated to its request by `messageId` (Q3) | Sent once, in reply to one request or subscription. Never persisted. | The server that refused. |
| Error code | A snake_case string in the schema's registry (`$defs/knownCode`) | Added by one coordinated change: schema, C# constructor, a C#-written vector, and a typed variant in every runtime. Never renamed and never reused. A retired code stays in the registry marked retired, so an old reader still decodes it. | The operator rules meaning; Self keeps the registry. |
| Details for a code | The code | Frozen with the code. Adding a key to an existing code's details is a new code (or a v1), never an in-place edit (see Q2). | Same as the code. |
| Unknown code, at a reader | The raw string | Preserved as typed `Other`. The reader acts on the `error` text and the absence of a known code. | The reader (Q1). |
| Vector file | `contracts/cultnet/interop/error-vectors.cs-written.json` | Regenerated only by the C# writer test under `CULTNET_WRITE_VECTORS=1` and committed. Every other runtime judges it read-only. | C#, the reference. |
| routingHint | Shard authority refusal | Sent with a null code (unchanged). Typed in every decoder. | CultNetDatabaseServer's authority path. |
## 3. Operator questions
**Rulings (operator, 2026-09-30):** "Q1: B / Q2: B / Q3: A / Q8: A".
- **Q1:** closed for emitters, open for readers. An unknown code decodes to a typed `Other`.
- **Q2:** per-code details.
- **Q3:** a nullable top-level `messageId`.
- **Q8:** v0 is corrected in place, with the semver bumps the policy requires.
- Q4-Q7 are still open.
- Cut 1 waits for `hands/cultmesh-put-serve-bound` to merge.
### Q1. What does a reader do with an error code it does not know?
Today Rust and TypeScript refuse the whole message. The probes above show the result: the peer's error text
is lost and the caller sees "not a recognized code". In cultmesh-rs's snapshot client the `?` at `lib.rs:812-815`
even turns a peer refusal into a local decode failure. So under a closed enum every new code breaks every
reader that has not upgraded. By `docs/semver-policy.md:86-91` that makes each new code a breaking wire
change, and nine codes are already in flight.
- **A. Closed enum (today).** Readers refuse unknown codes. Every code addition is a coordinated,
semver-major wire change across all runtimes and consumers.
- **B. Closed for emitters, open for readers.** The schema keeps the registry, and every runtime's emit
constructors can produce only registry codes (tests prove each emit path valid against the schema).
Readers decode an unrecognized code (matching `^[a-z][a-z0-9_]*$`) into a typed `Other { code, details }`.
A new code is an additive, minor change: old readers still surface the text and know "an error I cannot
classify". This is how gRPC status codes and protobuf open enums behave.
- **C. Fully open.** No registry; any string, emit and decode.
**Recommended: B.** A refusal must never fail to arrive because it is more specific than the reader
expected. B keeps the registry as the single meaning authority. C loses the meaning authority, and A makes
the error channel the most brittle message in CultNet.
Depends on this: schema `code` shape, every decoder, the doc text at `cultnet-distributed-database.md:107-115`.
### Q2. How are details shaped on the wire?
Today C# writes all four detail keys (`field`, `value`, `asOf`, `current`), nil included, because
`CultNetErrorDetails` (`CultNetSchemaMessages.cs:592-603`) is a MessagePack map-mode object. The codes in
flight need new keys: `responseBytes`, `maxSnapshotBytes`, `documentCount`, `maxSnapshotDocuments` and
`schemaVersion`.
- **A. One shared details record; grow it.** Every detail key of every code sits on one record, written as
nil when unused. Each new key changes the bytes of every existing error that has details, so all of the
C# vectors must be recaptured on every addition. It also breaks every strict reader: TS Ajv, and
StreamPixels' vendored schema, have `additionalProperties:false`. And the type cannot tie a key to its
code.
- **B. Per-code details.** `details` carries exactly the keys its code defines. A defined-but-optional key
is present as nil; another code's keys are absent. Keys are named for their meaning, which retires the
`field: "schemaId"` / `field: "subscriptionId"` indirection that exists only because the record is
shared. Adding a code never changes an existing code's bytes. In every runtime, code and details are
fused into one tagged type:
- Rust: an enum with data.
- C#: sealed subclasses, with a custom formatter for `CultNetErrorMessage`. There is precedent in
`src/GameCult.Mesh/CultMeshRouteRecordFormatter.cs`.
- TS: a discriminated union.
- Kotlin: a sealed class.
- Python: constructors per code over a frozen dataclass.
The churn is one time. The five existing C# cases with details change bytes, and all eight Rust hex
cases move into the shared vector file anyway (Cut 1).
- **C. Opaque string map.** Untyped; no runtime can offer a typed accessor.
**Recommended: B.** A is the churn the brief warned of, and it recurs on every future code. B pays it once,
at the moment every vector is being recaptured regardless.
Depends on this: the schema's `details`, the C# model and formatter, `encode_error_details`
(`packages/cultnet-rs/src/contracts.rs:1339-1366`), and every decoder.
### Q3. Does an error carry the `messageId` of the request it refuses?
Python already sends `messageId` (`server.py:583`); the schema forbids it. Without it:
- `cultmesh-browser` drops an error whenever two operations are pending (`index.ts:724-731`);
- C# content sessions fail every pending request on any error (`CultMeshContentSessions.cs:256-260`).
Adding a top-level key changes the bytes of every C# error. That happens once, in the same recapture as Q2.
- **A.** Add a nullable top-level `messageId`. A reply to a request echoes the request's id, and an error
with no request (a subscription push, an unsolicited refusal) sends nil. Receivers correlate by it and
fall back to their current behavior when it is nil.
- **B.** No correlation. Python deletes the field.
**Recommended: A.** Correlation is a capability any multiplexing client needs, and one runtime already
depends on it.
### Q4. Do servers send `document_unservable` (and every other refusal they can) to the peer?
Self ruled that "unservable" must be distinguishable as a typed code in every runtime. On the branch:
Python sends it coded, TypeScript sends it codeless, and Rust tells only its local caller, so the Rust
peer times out. The same Rust silence covers every other document-server rejection: a sink refusal, a
failed snapshot source, too many snapshot documents, and a snapshot response that is too large.
- **A.** Every refusal of a peer's request that the server can still send is answered with a coded error
carrying the request's `messageId`, and is also reported locally (Rust's `ApplicationRejected` stays as
the local report). The refusals that cannot be answered are the ones where the answer itself cannot be
queued: Rust `PayloadBudgetFull` and `ResponseQueueFailed`. Those stay local-only, and the doc says so.
- **B.** Local-only in Rust; peer replies only where a runtime already sends them.
**Recommended: A.** R-2 already ruled that a silence is worse than a missing feature
(`docs/cultnet-selection-cut.md:2049-2053`), and the operator's ruling that CultMesh "refuses" puts only
means something to the party that sent the put.
### Q5. Does Python's `unregistered_document_put` / `unregistered_document_delete` become `unowned_schema`?
Python sends these when the node has no registered document type for the record's schemaId
(`cultmesh_py/node.py:653-666`). C# `unowned_schema` means "no shard owns this schema and key"
(`CultNetSchemaMessages.cs:537-544`). To a peer both mean "this server will not take that schema here; route
elsewhere". The put and delete variants duplicate what the peer already knows (it sent the message).
- **A.** Both collapse into `unowned_schema { schemaId }`. The registry text broadens to "this server
stores no document for that schema (and key)".
- **B.** Join as a separate `schema_unregistered { schemaId }`.
- **C.** Join both codes as they are.
**Recommended: A.** It is one meaning at the peer, and there is one action to take.
### Q6. Does Python's `malformed_document_put` / `malformed_document_delete` become one `message_invalid { field }`?
These are sent when the request lacks a required field (`server.py:355-359`, `383-387`). C# has the same
condition, codeless, at `CultNetDatabaseServer.cs:373/415` and `CultNetDatabaseSubscriptionServer.cs:148`
("requires a subscriptionId or messageId").
- **A.** One `message_invalid { field }`, naming the missing or ill-typed field. It is used by Python's four
sites and C#'s three.
- **B.** Join both per-message codes and leave C# codeless.
**Recommended: A.** The per-message suffix repeats the request's own schemaVersion, and C# gains a code for
free.
### Q7. Does `simulation_observations_disabled` become `unsupported_schema_version`?
Python sends it when the observation hub is off (`server.py:593-598`), with
`details { schemaVersion: "cultnet.simulation_observation.v0" }`, which is exactly the
`unsupported_schema_version` shape. cultmesh-ts's "no cache for snapshot requests" and "no operation
handler" refusals (`index.ts:5875`, `5913`) mean the same thing: this server does not serve that message.
- **A.** Collapse. `unsupported_schema_version` means "this server does not serve messages of this
schemaVersion: unknown, a newer version, or not enabled here". The three TS sites and Huginn's "answers
only X, Y, Z" adopt it.
- **B.** Keep a separate `feature_disabled`-style code.
**Recommended: A.** A peer cannot act differently on "not enabled" and "not known". Python's
`verify.py:335` check follows the rename.
### Q8. Is this a correction of `cultnet.error.v0` in place, or a new `cultnet.error.v1`?
Under Q2-B and Q3-A, strict v0 readers (TS Ajv and vendored copies) refuse the new bytes. Tolerant readers
(C#, Rust, Python) keep working, because the probes show they ignore unknown keys and treat a missing key
as null.
- **A. In place.** The v0 contract has never been decodable across all five runtimes (section 0). No
consumer outside CultLib reads code or details (finding 10). Every publishing package takes the semver
bump its policy requires:
- `org.gamecult.cultlib` major, because the public `CultNetErrorDetails` properties change;
- `cultnet-py` and `cultmesh-py` major, because emitted codes change;
- Rust pins move deliberately.
StreamPixels and Heimdall re-vendor.
- **B. v1.** Add `cultnet.error.v1`, keep v0 emit for peers that did not advertise v1 in hello, and run
two error encoders in every runtime until v0 retires.
**Recommended: A.** B builds a version-negotiation path for a message nobody outside CultLib parses beyond
`.error`. The one thing every old reader does need, the `error` text, survives A in every tolerant runtime.
## 4. The registry (as recommended; rewrite on each ruling)
| Code | Meaning | Details (per Q2-B; `?` = present-as-nil when absent) | Emitted by after this map |
|---|---|---|---|
| `selection_invalid` | A selection field failed the door. | `{ field, value? }` | C#, Rust (selection) |
| `cursor_stale` | The cursor predates the server's retained history. | `{ asOf, current }` | C#, Rust |
| `cursor_invalid` | The cursor could not be parsed. | none (details nil) | C#, Rust |
| `reference_outside_target` | A hop reference left the declared target. | none | C#, Rust |
| `variant_unsupported` | The selection selects a document variant CultNet does not carry. | `{ subscriptionId? }` | C# (incl. `DatabaseServer.cs:560`) |
| `unowned_schema` | This server stores no document for that schema (and key). | `{ schemaId }` | C#, Python (Q5) |
| `unsupported_schema_version` | This server does not serve messages of this schemaVersion. | `{ schemaVersion }` | Python, TS, Huginn (Q7) |
| `message_invalid` | The request lacks a required field or has an ill-typed one. | `{ field }` | C#, Python (Q6) |
| `snapshot_document_limit_exceeded` | The snapshot answer would exceed the document bound. | `{ documentCount, maxSnapshotDocuments }` | Python, Rust (Q4) |
| `snapshot_byte_limit_exceeded` | The snapshot answer would exceed the byte bound. | `{ responseBytes, maxSnapshotBytes }` | Python, Rust (Q4) |
| `document_unservable` | A put was refused because no snapshot could ever return the stored document under the byte bound. | `{ responseBytes, maxSnapshotBytes }` | Python, TS, Rust (Q4) |
A null code is legal and means "unclassified". Examples: an exception message from a failed snapshot
source, an authority refusal carrying `routingHint`, and "not authorized"
(`CultNetDatabaseSubscriptionServer.cs:165`). This map does not invent codes for those.
`snapshot_byte_limit_exceeded` and `document_unservable` have identical details. That is deliberate, and it
is the fixture trap named in the verification rules.
---
## 5. Cuts
All cuts are made on one campaign branch, `hands/error-contract`, from `main` after
`hands/cultmesh-put-serve-bound` merges. The branch merges to `main` once, after Cut 5's Soul pass. The
reason: Cut 1 changes C# bytes, and Rust's static hex cases would stay green against the stale bytes
(finding: `tests/error_message.rs` pins constants, not the reference). A partial merge would ship a
wire split while every suite passed. Each cut gets its own worktree, created by Self. Heavy verification
runs through `ygg-verify.sh`, never on Starfire.
### Cut 0. Unblock the interop lane
- **Repo/branch:** CultLib, `hands/interop-guard` from `main`, merged alone ahead of the campaign. It
does not depend on the rulings.
- **Deletes first:** nothing.
- **Per-file changes:** `.github/workflows/cultnet-interop.yml:72`. The `$stray` filter also accepts
`^packages/cultnet-rs/contracts/cultnet/`, with a comment naming R-AB and
`packages/cultnet-rs/tests/schema_discovery_contracts.rs:74-99` as the equality guard. The namespace
check is unchanged.
- **Authority map:** no ownership change. The canonical owner stays `contracts/cultnet/`, and the Rust
mirror is derived from it, with drift judged by the Rust test.
- **Verification:**
- Push the branch and run the workflow. Report every step that goes red after the guard. None of those
steps has run on main for 40 runs, so expect more red, and treat each failure as its own finding;
do not fix it in this cut.
- Negative: edit one byte of a vendored copy locally and check that the Rust equality test fails.
Reach it on Yggdrasil with `cd packages/cultnet-rs && cargo test --test schema_discovery_contracts`.
- **Ledger:** +3 lines.
### Cut 1. The contract and the C# reference
- **Repo/branch:** CultLib `hands/error-contract`, worktree named by Self.
- **First:** capture the current C# bytes for all eight factory cases, plus `cs_codeless` and `cs_routing`,
into the Hands report. This is the before-image a Soul pass compares against.
- **Deletes first:**
- `CultNetErrorDetails` (`src/GameCult.Networking/CultNetSchemaMessages.cs:588-603`, 16 lines) as a
shared record.
- The `field`/`value`/`asOf`/`current` property block and the `[MessagePackObject]`/`[Key]` attributes
on `CultNetErrorMessage` (`:497-521`). Its serialization moves to one formatter.
- `SoulP4dProbeTests.Wire_UnownedSchemaBytes` (`tests/GameCult.Networking.Tests/SoulP4dProbeTests.cs:191-199`).
It is an `[Explicit]` hex printer, superseded by the vector writer.
- The closed-enum paragraph at `contracts/cultnet/cultnet-distributed-database.md:107-115` and
the `code`/`details` descriptions in the schema (`:28-42`).
- **Adds:**
- **Schema** (`contracts/cultnet/cultnet.error.schema.json`, rewritten; the Rust mirror is re-copied
byte-equal):
- Top level: `schemaVersion`, `error` and `routingHint` as today, plus `messageId: string|null` (Q3)
and `code`. All five are required, present-as-nil (R-X is unchanged at the top level).
- `$defs/knownCode` is the registry enum from section 4.
- `code` is `null`, or a string matching `^[a-z][a-z0-9_]*$` (Q1-B).
- `details` is decided by a top-level `oneOf`, one branch per known code, each fixing `code` by
`const` and `details` by exact `properties`/`required`/`additionalProperties:false`. Two more
branches: a null code with null details, and an unknown code (`not: {$ref knownCode}`) with details
`object|null`, open.
- Use `oneOf`/`not`/`const` only. `tests/GameCult.Networking.Tests/MiniJsonSchemaValidator.cs:30-35`
implements those and **not** `if`/`then`/`allOf`, and it refuses unimplemented keywords. TS
compiles with Ajv2020 (`packages/cultnet-ts/src/contracts.ts:1,674`), which handles both.
- **C# model** (`GameCult.Networking`):
- `CultNetErrorMessage { SchemaVersion, Error, MessageId?, RoutingHint?, Refusal? }`, where
`Code => Refusal?.Code` is derived and not settable.
- `abstract CultNetRefusal { string Code }`, with one sealed subclass per registry code carrying its
typed details (`CultNetCursorStaleRefusal(ulong AsOf, ulong Current)` and so on), plus
`CultNetUnknownRefusal(string Code, IReadOnlyDictionary<string, object?>? Details)` for Q1-B.
- `CultNetErrorCodes`: string constants, one per registry code.
- `CultNetErrorMessageFormatter : IMessagePackFormatter<CultNetErrorMessage>`. It writes the five
top-level keys in order (schemaVersion, error, messageId, routingHint, code, details), and details
as exactly the refusal's keys. It reads by key name, tolerates unknown keys at both levels, and
maps an unknown code to `CultNetUnknownRefusal`. Register it through `[MessagePackFormatter]` on
the type, the way `CultMeshRouteRecordFormatter` is wired.
- Factories: keep `ForSelectionInvalid`, `ForCursor`, `ForReferenceOutsideTarget`,
`ForVariantUnsupported` and `ForUnownedSchema` as thin constructors over the refusal types, and add
one per new code. Each takes an optional `messageId`.
- `CultNetPeerErrorException : IOException`, carrying the `CultNetErrorMessage`.
- **Vector writer:** `ErrorVectorTests.WriteErrorVectors`, gated on `CULTNET_WRITE_VECTORS=1` like
`SelectionParityVectorTests.cs:460-552`. It writes `contracts/cultnet/interop/error-vectors.cs-written.json`:
one entry per registry code (both optional-key states where a key is optional), `cs_codeless`,
`cs_routing`, one unknown code with open details, and one error with `messageId` set. Each entry
holds `{ name, messagePackBase64, expected: { error, messageId, code, details, routingHint } }`,
where `expected` is a JSON projection every runtime compares its typed decode against.
- **Per-file changes** (`8fc74c70`):
- `CultNetDatabaseServer.cs:560`: `ForVariantUnsupported(refusal.Message, subscriptionId)`.
- `CultNetDatabaseServer.cs:373`, `:415` and `CultNetDatabaseSubscriptionServer.cs:148` become
`message_invalid { field: "subscriptionId" }` (Q6-A).
- Every `SendCultNet(CultNetErrorMessage...)` that answers a request passes the request's `MessageId`.
- Each receiver listed in finding 6 throws `new CultNetPeerErrorException(error)` instead of
wrapping `error.Error`. `CultMeshContentSessions.OnError` (`:256-260`) fails only the pending entry
whose id matches `error.MessageId`, and falls back to all entries only when the id is nil.
- `CultNetSchemaContractTests.cs:165-214`: the per-factory tests are replaced by one test that validates
every vector's decoded JSON against the schema with `MiniJsonSchemaValidator`, and one test that
serializes each registry factory and asserts byte equality with its committed vector.
- Rewrite `contracts/cultnet/cultnet-distributed-database.md` "Error Codes" to the registry, and state
the Q1 and Q2 rules.
- **Authority map:**
- Owner: the schema's registry owns code meaning and detail shape. `CultNetErrorMessageFormatter` owns
C# bytes. The refusal subclass owns the code-to-details binding.
- Inputs: the refusal instance, messageId, routingHint.
- Outputs: bytes, and `cs-written.json` for the other runtimes.
- Derived state: `CultNetErrorMessage.Code` becomes derived; the refusal decides it.
- Forbidden writers: any `Code = "..."` assignment (it cannot compile); `[Key]` attribute serialization
of the message; hand-typed hex anywhere.
- Shared paths: every server send uses the factories; every receiver uses `CultNetPeerErrorException`.
- Deletion line: `CultNetErrorDetails` and the settable `Code`.
- **Verification** (`dotnet` image):
- Builds: `tests/GameCult.Networking.Tests`, `tests/GameCult.Mesh.Tests`.
- `ErrorVectorTests` with `CULTNET_WRITE_VECTORS=1`, then without, and check that the committed file
reproduces byte-for-byte.
- Tests and the rules they pin:
- vector round trip: bytes, decode, bytes are equal for every entry;
- a registry factory is byte-equal to its vector;
- every vector validates against the schema;
- an unknown code decodes to `CultNetUnknownRefusal` with its raw details intact;
- a Python-shaped error (`py_snapshot_byte_limit` above, with the Q2/Q3 shape) decodes typed with
`responseBytes` and `maxSnapshotBytes` intact. Today's C# silently empties those.
- content-session correlation: two pending requests, an error for the second, and the first still
completes.
- Mutation: Stryker.NET on the formatter and factories. The survivors that must die:
- swapping two known codes whose details are identical (`snapshot_byte_limit_exceeded` and
`document_unservable`);
- padding a nil key into details;
- dropping `messageId` on encode;
- mapping an unknown code to null.
- Negative greps (checked at HEAD, where they hit the sites being cut):
- `git grep -nE 'new (IOException|InvalidOperationException)\(error\.Error\)' -- src` must return 0
(9 hits at HEAD).
- `git grep -nE 'Field = "(schemaId|subscriptionId)"' -- src` must return 0 (2 at HEAD).
- Operator: none.
- **Ledger:** about -60 C# (the details record, the settable code, the probe printer, and the per-factory
schema tests), +250 (the formatter, the refusal types, the vector writer). The schema grows by about 90
JSON lines. The net is positive, bought by Q1/Q2 typing in the reference.
### Cut 2. Rust: cultnet-rs and cultmesh-rs
- **Deletes first:**
- `CultNetErrorCode` and `CultNetErrorDetails` as separate types (`packages/cultnet-rs/src/contracts.rs:252-310`,
59 lines);
- `require_error_details` (`:1121-1146`);
- `encode_error_details` (`:1339-1366`);
- the eight hex `Case`s and the header comment in `packages/cultnet-rs/tests/error_message.rs:1-160`.
That file is rewritten to read the vector file.
- Keep `require_legacy_optional_u64` (`:1149`). Check with `git grep` for other callers first; at HEAD,
only the error details use it.
- **Adds:**
- `CultNetMessage::Error { error: String, message_id: Option<String>, routing_hint: Option<CultNetShardRoutingHint>, refusal: Option<CultNetRefusal> }`.
- `enum CultNetRefusal`, with one variant per registry code and its typed fields, plus
`Other { code: String, details: Option<rmpv::Value> }`.
- `CultNetRefusal::code(&self) -> &str`.
- A constructor `CultNetMessage::error(text)` and builder-style `with_refusal` / `with_message_id`, so
consumers stop writing struct literals. Huginn has three.
- Decode reads every top-level key by name and routes unknown codes to `Other` (Q1-B).
- Encode writes the Cut 1 key order.
- `CultNetShardRoutingHint` is typed, reusing the crate's existing shard descriptor type.
- **Per-file changes:**
- `packages/cultnet-rs/src/contracts.rs:361-368` (variant), `:1097-1115` (decode), `:1300-1336` (encode).
- `packages/cultnet-rs/src/selection.rs:1210-1255`: the four refusals build `CultNetRefusal` variants.
- `packages/cultnet-rs/src/content.rs:316`: uses `CultNetMessage::error`.
- `packages/cultmesh-rs/src/lib.rs:812-827`: a peer error becomes a typed error carrying the refusal,
instead of `anyhow!` text. Name it `CultMeshPeerError`, next to the crate's existing error types.
- `packages/cultmesh-rs/src/rudp_document_server.rs` (Q4-A): each `CultMeshRudpRejectionReason` that
can be answered sends the matching coded error, correlated by the request's message id, before
returning `ApplicationRejected`. `PayloadBudgetFull` and `ResponseQueueFailed` stay local.
- **Authority map:**
- Owner: `CultNetRefusal` owns code and details. The schema owns the registry.
- Forbidden writers: string codes (`from_wire_str` survives only inside decode); struct literals of
details.
- Shared paths: selection refusals, document-server refusals and content errors all go through the one
encoder.
- **Verification** (`rust` image, `cd packages/cultnet-rs` then `cd ../cultmesh-rs`):
- `cargo test --workspace`.
- `tests/error_message.rs` pins: every vector decodes to `expected`; encoding the decoded value
reproduces the vector's bytes; the unknown-code vector decodes to `Other`; the routing vector keeps
`routing_hint`.
- cultmesh-rs: a put over the bound makes the peer receive `document_unservable` with the request's
message id, and details equal to the computed size and the bound. Run it at two bounds so that a
constant or an offset in `maxSnapshotBytes` dies.
- A peer that sends a Python-shaped `snapshot_byte_limit_exceeded` produces a `CultMeshPeerError`, not
a decode failure.
- Mutation: `cargo mutants --in-diff` on the cut's diff. Triage every survivor by name.
- Negative: `git grep -n 'CultNetErrorDetails' -- packages` must return 0.
- **Consumer note:** Huginn's `serve.rs:288/307/312` and its tests stop compiling at the next pin bump.
The migration is mechanical (`CultNetMessage::error(...)`), and `:312` adopts `unsupported_schema_version`
(Q7). Self routes it as a Huginn change; it is not part of this branch.
### Cut 3. TypeScript: cultnet-ts, cultmesh-ts and cultmesh-browser
- **Deletes first:**
- the two-field `CultNetErrorMessage` interface (`packages/cultnet-ts/src/contracts.ts:167-170`);
- the four literal error constructions in `packages/cultmesh-ts/src/index.ts:5875-5878`, `5885-5888`,
`5913-5916` and `5922-5925`, and the branch's codeless unservable site (`0eb7434a`);
- `rejectCorrelated`'s single-pending heuristic (`packages/cultmesh-browser/src/index.ts:724-731`).
- **Adds:**
- `CultNetErrorMessage` with every field.
- `type CultNetRefusal` as a discriminated union on `code`, with an `{ code: string; details: Record<string, unknown> | null; known: false }` arm.
- `createCultNetError(text, { messageId, refusal })` as the only constructor.
- `class CultNetPeerError extends Error { message: CultNetErrorMessage }`.
- `parseCultNetMessage` keeps Ajv validation against the canonical schema; it now accepts every schema
branch, including unknown codes.
- **Per-file changes:**
- cultmesh-ts: the three "does not serve this" sites (`5875`, `5913`, `5922`) send
`unsupported_schema_version` (Q7-A); the snapshot catch (`5885`) sends a null code; the unservable site
sends `document_unservable` with details.
- cultmesh-ts `6527-6529` throws `CultNetPeerError`.
- cultmesh-browser `163`, `584` and `690`: correlate by `messageId`, falling back to today's behavior only
for a nil id.
- `packages/cultnet-ts/src/idunn-odin-presence-publisher.ts:118-120` throws `CultNetPeerError`.
- The legacy `gamecult.networking.v0` path (`contracts.ts:840-844`, `:899-900`) is unchanged. That is a
separate legacy contract.
- **Verification** (`kotlin` image, which carries Node 24; `npm install`, then build cultcache-ts before
cultnet-ts):
- A new `test/error-vectors.test.ts` pins that every vector parses and deep-equals `expected`, and that
`encode(createCultNetError(...))` is byte-equal to each registry vector.
- The cultmesh-ts document-server tests assert that each refusal parses with TypeScript's own
`parseCultNetMessage`. That is the probe's `ts_two_key` failure, pinned.
- Browser: two pending operations, an error for the second, and the first still resolves.
- Mutation: StrykerJS on the diff.
- Negative: `git grep -nE 'schemaVersion: "cultnet\.error\.v0",' -- packages/cultmesh-ts/src packages/cultmesh-browser/src`
must return 0. Exclude `cultnet-ts/src/schema-discovery.ts:302` (a descriptor) and
`contracts.ts:842` (legacy decode); both are legitimate hits for the unscoped pattern.
### Cut 4. Python: cultnet-py and cultmesh-py
- **Deletes first:**
- `_error_response` (`packages/cultmesh-py/src/cultmesh_py/server.py:573-589`);
- `unsupported_schema_version_error` (`packages/cultnet-py/src/cultnet_py/interop_peer.py:409-417`);
- every string literal code in `server.py` (lines in finding 3).
- **Adds:**
- `cultnet_py/errors.py`, holding:
- `CultNetErrorCode(StrEnum)`, the registry;
- a frozen `CultNetError` dataclass (`error`, `message_id`, `routing_hint`, `code: CultNetErrorCode | str | None`,
`details: Mapping | None`);
- one constructor function per code, taking exactly that code's keys;
- `to_wire()`, which writes the five top-level keys in the Cut 1 order, nil included;
- `parse_cultnet_error(dict)`.
- `CultNetPeerError` (`client.py:46-50`) keeps `.response` and gains `.error: CultNetError`.
- **Per-file changes:**
- Sites `server.py:158-163`, `355-394`, `555-569` and `593-598` and the branch's `document_unservable`
site use the constructors, with the Q5-A/Q6-A/Q7-A codes.
- `interop_peer.py` uses the constructor.
- `verify.py:335` reads `error.error.code`.
- Tests `packages/cultmesh-py/tests/test_cultmesh.py:2088`, `3791`, `4323` and `4363-4371`, and
`packages/cultnet-py/tests/test_cultnet.py:2342`, follow the renames.
- **Verification** (the image the campaign already uses for Python; state it in the report):
- A vector test pins that every vector parses to `expected` and that each constructor's `msgpack.packb`
is byte-equal to its vector.
- Mutation: mutmut on `errors.py` and the changed server sites.
- Negative: `git grep -nE '"schemaVersion": "cultnet\.error\.v0"' -- packages/cultnet-py/src packages/cultmesh-py/src`
returns only `errors.py`.
- **Not here:** F-1 (`schema_catalog.py` publishes its own shapes).
### Cut 5. Kotlin: cultmesh-kotlin
- **Adds:**
- A `CultNetErrorMessage` data class and a `sealed class CultNetRefusal` (known codes plus `Other`),
with a `CultNetMessage.asError(): CultNetErrorMessage?` view.
- `class CultNetPeerException(val error: CultNetErrorMessage) : IOException`.
- At `CultMesh.kt:1278` and `:1334`, and at every `receiveSchemaMessage` consumer that expects a
response, an error response throws `CultNetPeerException` before the `require`.
- **Verification** (`kotlin` image, `pwsh -File build.ps1`):
- A vector test pins that every vector decodes to `expected`.
- Kotlin emits no errors today, so encode parity applies only if a Kotlin server path is added. Say
so in the report; do not invent one.
- There is no mutation tool. Behavioural tests cover it: an error reply to a schema-catalog request
raises `CultNetPeerException` carrying the code.
### Cut 6. Merge, release, consumers
- Soul runs over the whole branch before the merge:
- rerun every vector suite;
- decode each runtime's live emissions with every other runtime. The interop lane from Cut 0 is the
harness; add an error round to `tests/GameCult.Networking.InteropPeer` and
`packages/cultnet-ts/test/interop` if Soul finds a runtime pair unreached.
- Release per `docs/semver-policy.md`, with a Soul pass before tags:
- `org.gamecult.cultlib` major, and the tracked Unity DLLs rebuilt, since they embed the schema;
- `cultnet-py` and `cultmesh-py` major.
- Consumers, routed by Self as separate repo changes:
- Huginn: pin bump plus Cut 2's note.
- StreamPixels and Heimdall: re-vendor. Heimdall is already broken by R-X.
- AetheriaEve, Gjallar and Mimir compile against the live CultLib path and read only `.Error`, so they
need no change. That is inferred from the audit; the next build of each confirms it.
- Hermodr: none.
## 6. Subtraction ledger
| Cut | Removed (estimate) | Added (estimate) | Formats / targets |
|---|---|---|---|
| 0 | 0 | 3 | none |
| 1 | ~60 C#, schema text | ~250 C#, ~90 schema | + `error-vectors.cs-written.json`; - `[Explicit]` hex printer |
| 2 | ~150 (types, two helpers, 160-line hex test) | ~200 | hex constants gone; vectors read from file |
| 3 | ~40 | ~120 | none |
| 4 | ~40 | ~120 (`errors.py`) | 8 ad-hoc codes → registry |
| 5 | 0 | ~80 | none |
The net is positive, by roughly 500 lines, across five runtimes. It buys typed decode where there was
none (Kotlin, TS and Python), a correlation capability, and one vector file in place of hand-copied hex.
The pressure point: if Cut 1's formatter passes 150 lines, report it rather than trimming typing.
## 7. Follow-ups outside this map
- **F-1.** `packages/cultnet-py/src/cultnet_py/schema_catalog.py` publishes about 350 lines of hand-built
schemas under canonical `$id`s. Python should package and serve `contracts/cultnet/*.schema.json`, the
way Rust vendors them. It is a separate cut, because it covers every message, not errors. It can wait
because the catalog is advisory: no runtime validates a peer's message against a published descriptor.
- **F-2.** Codes for the remaining codeless refusals (authorization, failed snapshot sources, routing).
Wait for a consumer that branches on them.
- **F-3.** Whatever the Cut 0 lane reveals once its later steps run again.

1637
docs/cultnet-rudp-ack-cut.md Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -271,6 +271,15 @@ Current progress:
flush where the runtime supports one, so callers that require confirmed
transport delivery do not substitute a fixed sleep. TCP-framed profiles do
not publish the field because they do not own the RUDP pending-reliable queue.
- The receive window is a contract, not a discovery. A receiver must hold 1,023
sequences and 4 MiB of payload beyond its contiguous received watermark, and
a conforming sender never exceeds either. The sender's flow window enforces
it without a wire field: a reliable packet is admitted only while its
sequence is at most 1,023 above the lowest unacked sequence and the payload
above that sequence stays within 4 MiB. The first packet is always
admissible, so a payload larger than 4 MiB goes out alone, and packets wait
in the FIFO in order, so a small packet never overtakes a large one. C#,
Rust, TypeScript, and Python enforce it; Kotlin does not yet.
- TypeScript, C#, Rust, Python, and Kotlin now share a portable
`cultnet.reconnect_policy.v0` document and deterministic exponential delay
helper, and RUDP transport profiles advertise that policy under

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,272 @@
# Element-id sweep list (C2a)
Data for the per-consumer sweep in docs/document-variants-cut.md, C2. Every object type below sits in a list (`List<T>`, `T[]`, `IEnumerable<T>`, and the like) reachable from a registered document type, and needs one string member with `[Key(n)]` and `[CultElementId]` before that consumer can take a CultLib that requires ids. A union base takes the member on the base; its subclasses inherit it, so put the slot above the subclasses' highest key.
Method: a static scan of `F:\Projects\<consumer>` C# files for types marked `[CultDocument(`, walked through keyed members, base classes, dictionary values and `[Union]` subtypes. Read-only; no consumer was edited. Worktree copies (`Aetheria-catalog`, `CultLib-*`, and so on) are excluded. Struct elements are values and need no id. Non-C# consumers carry the id as an ordinary member and are not listed. The scan is text-based: a document registered without a literal `[CultDocument(` attribute, or a type whose members lack an integer `[Key]`, is missed, so the sweep pass should confirm each repo by registering its types once.
Lines: `Type file:line (via Owner.Member)`, paths relative to the consumer root.
## Aetheria (13 element types)
- BehaviorData Assets/Scripts/ServerShared/Behaviors/Behaviors.cs:190 (via ConsumableItemData.Behaviors)
- EntityPack Assets/Scripts/ServerShared/EntitySerializer.cs:171 (via ZonePack.Entities)
- ProductRole Assets/Scripts/ServerShared/FactionProduct.cs:38 (via FactionProductData.Roles)
- InputLayoutRow Assets/Scripts/ServerShared/InputLayout.cs:32 (via InputLayout.Rows)
- InputLayoutColumn Assets/Scripts/ServerShared/InputLayout.cs:52 (via InputLayoutKeyRow.Columns)
- ItemRole Assets/Scripts/ServerShared/ItemData.cs:325 (via CraftedItemData.Roles)
- AudioStat Assets/Scripts/ServerShared/ItemData.cs:441 (via EquippableItemData.AudioStats)
- HardpointData Assets/Scripts/ServerShared/ItemData.cs:553 (via HullData.Hardpoints)
- StatTerm Assets/Scripts/ServerShared/ItemData.cs:630 (via PerformanceStat.Terms)
- LoadoutSlot Assets/Scripts/ServerShared/Loadout.cs:23 (via Loadout.Slots)
- RoleFill Assets/Scripts/ServerShared/Provenance.cs:84 (via Lot.Roles)
- SavedActionBarBinding Assets/Scripts/ServerShared/SavedGame.cs:168 (via SavedGame.ActionBarBindings)
- Asteroid Assets/Scripts/ServerShared/ZoneData.cs:133 (via AsteroidBeltData.Asteroids)
## AetheriaEve (115 element types)
- AetheriaCorporationAllegiance Aetheria.State/Documents/AetheriaCorporation.cs:67 (via AetheriaCorporation.Allegiances)
- AetheriaShapeCell Aetheria.State/Documents/AetheriaItemDefinition.cs:178 (via AetheriaItemDefinition.ShapeCells)
- AetheriaItemHardpoint Aetheria.State/Documents/AetheriaItemDefinition.cs:188 (via AetheriaItemDefinition.Hardpoints)
- AetheriaCurveKey Aetheria.State/Documents/AetheriaItemDefinition.cs:222 (via AetheriaItemDefinition.ThermalPerformanceCurveKeys)
- AetheriaItemAudioStat Aetheria.State/Documents/AetheriaItemDefinition.cs:238 (via AetheriaItemDefinition.AudioStats)
- AetheriaBehaviorPayload Aetheria.State/Documents/AetheriaItemDefinition.cs:267 (via AetheriaItemDefinition.BehaviorPayloads)
- AetheriaBehaviorField Aetheria.State/Documents/AetheriaItemDefinition.cs:286 (via AetheriaBehaviorPayload.Fields)
- AetheriaBehaviorMapEntry Aetheria.State/Documents/AetheriaItemDefinition.cs:296 (via AetheriaBehaviorValue.MapEntries)
- AetheriaBehaviorValue Aetheria.State/Documents/AetheriaItemDefinition.cs:306 (via AetheriaBehaviorValue.Children)
- AetheriaLegacyCatalogFile Aetheria.State/Documents/AetheriaLegacyCatalogQuarantine.cs:41 (via AetheriaLegacyCatalogQuarantine.NameFiles)
- AetheriaMigrationCount Aetheria.State/Documents/AetheriaMigrationLedger.cs:35 (via AetheriaMigrationLedger.Counts)
- AetheriaLegacyCatalogEntrySummary Aetheria.State/Documents/AetheriaMigrationLedger.cs:45 (via AetheriaMigrationLedger.LegacyCatalogEntries)
- AetheriaStoryFileHash Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:42 (via AetheriaPlayerSettings.StoryFileHashes)
- AetheriaInputBindingOverride Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:85 (via AetheriaPlayerInputSettings.BindingOverrides)
- AetheriaEntityLoadout Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:119 (via AetheriaEntityLoadout.Children)
- AetheriaLoadoutItemSlot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:186 (via AetheriaEntityLoadout.Equipment)
- AetheriaCargoBayLoadout Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:199 (via AetheriaEntityLoadout.CargoContents)
- AetheriaFactionZoneState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:268 (via AetheriaRunState.HomeZones)
- AetheriaAgentTaskState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:275 (via AetheriaRunState.AgentTasks)
- AetheriaDroppedPickupSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:346 (via AetheriaZoneState.DroppedPickups)
- AetheriaOrbitSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:362 (via AetheriaZoneState.Orbits)
- AetheriaBodySnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:384 (via AetheriaZoneState.Bodies)
- AetheriaBodyResource Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:449 (via AetheriaBodySnapshot.Resources)
- AetheriaAsteroidSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:459 (via AetheriaBodySnapshot.Asteroids)
- AetheriaAsteroidMiningAccumulatorSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:484 (via AetheriaAsteroidSnapshot.MiningAccumulators)
- AetheriaColor Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:545 (via AetheriaGasGiantVisualState.Colors)
- AetheriaFogFieldEmitterSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:691 (via AetheriaEntitySnapshot.FogFieldEmitters)
- AetheriaLoadoutGenerationSelection Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:723 (via AetheriaLoadoutGenerationReceipt.Selections)
- AetheriaEntityContactSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:734 (via AetheriaEntitySnapshot.Contacts)
- AetheriaEntityItemSlot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:751 (via AetheriaEntitySnapshot.Equipment)
- AetheriaWeaponGroupSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:782 (via AetheriaEntitySnapshot.WeaponGroups)
- AetheriaEntityStatGrid Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:789 (via AetheriaEntitySnapshot.StatGrids)
- AetheriaActiveConsumableSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:805 (via AetheriaEntitySnapshot.ActiveConsumables)
- AetheriaConsumableBehaviorStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:828 (via AetheriaActiveConsumableSnapshot.BehaviorStates)
- AetheriaBehaviorProgressSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:844 (via AetheriaEntitySnapshot.BehaviorProgress)
- AetheriaWeaponStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:863 (via AetheriaEntitySnapshot.WeaponStates)
- AetheriaBehaviorStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:924 (via AetheriaEntitySnapshot.BehaviorStates)
- AetheriaFactionRelationshipState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:1093 (via AetheriaRunState.FactionRelationships)
- AetheriaItemRarityTier Aetheria.State/Documents/AetheriaTradeValuePolicy.cs:29 (via AetheriaTradeValuePolicy.Tiers)
- AetheriaRuntimeAgentTaskCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs:35 (via AetheriaRuntimeRunCheckpointCommit.AgentTasks)
- AetheriaRuntimeAssetManifestEntry Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssetDocuments.cs:102 (via AetheriaRuntimeAssetManifestDocument.Assets)
- AetheriaRuntimeCurveKey Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:446 (via AetheriaRuntimeShotReceiptCommit.GuidanceCurve)
- AetheriaRuntimeCorporation Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:620 (via AetheriaRuntimeCatalogSnapshot.Corporations)
- AetheriaRuntimeNameFile Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:669 (via AetheriaRuntimeCatalogSnapshot.NameFiles)
- AetheriaRuntimeLoadoutTemplateSnapshot Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:796 (via AetheriaRuntimeLoadoutTemplatesDocument.Templates)
- AetheriaRuntimeDaemonCommandBoundaryEntry Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs:462 (via AetheriaRuntimeDaemonCommandBoundaryDocument.Commands)
- AetheriaRuntimeDaemonSoaIdentityDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:143 (via AetheriaRuntimeDaemonSoaViewDocument.Identities)
- AetheriaRuntimeDaemonRenderGroupDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:156 (via AetheriaRuntimeDaemonSoaViewDocument.RenderGroups)
- AetheriaRuntimeDaemonSoaBufferDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:222 (via AetheriaRuntimeDaemonSoaViewDocument.Buffers)
- AetheriaRuntimeDaemonSoaColumnDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:253 (via AetheriaRuntimeDaemonSoaViewDocument.Columns)
- AetheriaRuntimeDaemonSoaDirtyRangeDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:284 (via AetheriaRuntimeDaemonSoaViewDocument.DirtyRanges)
- AetheriaRuntimeEveSurfaceAdvertisement Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs:11 (via AetheriaRuntimeDaemonProviderAdvertisementDocument.EveSurfaces)
- AetheriaRuntimeEvePluginRequirement Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs:58 (via AetheriaRuntimeEveSurfaceAdvertisement.RequiresPlugins)
- AetheriaRuntimeZoneTargetRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:410 (via AetheriaRuntimeZoneContactsDocument.Targets)
- AetheriaRuntimeZoneContactRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:441 (via AetheriaRuntimeZoneContactsDocument.Contacts)
- AetheriaRuntimeStationStockItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:551 (via AetheriaRuntimeStationRefitEntityOption.CargoItems)
- AetheriaRuntimeStationRefitEntityOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:585 (via AetheriaRuntimeStationRefitDocument.AvailableEntities)
- AetheriaRuntimeStationDockingBayRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:620 (via AetheriaRuntimeStationRefitDocument.DockingBays)
- AetheriaRuntimeStationLoadoutRestoreOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:655 (via AetheriaRuntimeStationRefitDocument.LoadoutRestoreOptions)
- AetheriaRuntimeStationCargoTargetRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:674 (via AetheriaRuntimeStationRefitDocument.CargoTargets)
- AetheriaRuntimeSectorMapZone Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:765 (via AetheriaRuntimeSectorMapDocument.Zones)
- AetheriaRuntimeSectorMapLink Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:802 (via AetheriaRuntimeSectorMapDocument.Links)
- AetheriaRuntimeZoneRenderAdjacentZone Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1197 (via AetheriaRuntimeZoneRenderDocument.AdjacentZones)
- AetheriaRuntimeZoneRenderWormholeExit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1210 (via AetheriaRuntimeZoneRenderDocument.WormholeExits)
- AetheriaRuntimeZoneRenderBodyPose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1229 (via AetheriaRuntimeZoneRenderDocument.BodyPoses)
- AetheriaRuntimeZoneRenderAsteroidBeltPose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1260 (via AetheriaRuntimeZoneRenderDocument.AsteroidBeltPoses)
- AetheriaRuntimeZoneRenderAsteroidInstancePose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1286 (via AetheriaRuntimeZoneRenderAsteroidBeltPose.InstancePoses)
- AetheriaRuntimeViewportObject Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1382 (via AetheriaRuntimeGameViewportDocument.Objects)
- AetheriaRuntimeInventoryItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1458 (via AetheriaRuntimeViewportObject.Inventory)
- AetheriaRuntimeGravityInfluence Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1493 (via AetheriaRuntimeGameViewportDocument.GravityInfluences)
- AetheriaRuntimeBodyView Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1530 (via AetheriaRuntimeGameViewportDocument.Bodies)
- AetheriaHangarShip Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:119 (via AetheriaHangarState.Ships)
- AetheriaHangarItemStack Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:129 (via AetheriaHangarState.Inventory)
- AetheriaHangarCurrency Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:136 (via AetheriaHangarState.Currencies)
- AetheriaDeploymentReceipt Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:155 (via AetheriaHangarState.Deployments)
- AetheriaProgressionVerseOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeProgressionSourceDocuments.cs:45 (via AetheriaProgressionSourceDocument.AvailableVerses)
- AetheriaRuntimeStoryFileHashDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSettingsDocuments.cs:87 (via AetheriaRuntimePlayerSettingsDocument.StoryFileHashes)
- AetheriaRuntimeInputBindingOverrideDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSettingsDocuments.cs:108 (via AetheriaRuntimePlayerSettingsDocument.BindingOverrides)
- AetheriaRuntimeEntityLoadoutCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:78 (via AetheriaRuntimeEntityLoadoutCommit.Children)
- AetheriaRuntimeLoadoutItemSlotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:143 (via AetheriaRuntimeEntitySnapshotCommit.Equipment)
- AetheriaRuntimeCargoBayLoadoutCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:159 (via AetheriaRuntimeEntitySnapshotCommit.CargoContents)
- AetheriaRuntimeGameEventCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:490 (via AetheriaRuntimeRunCheckpointCommit.GameEvents)
- AetheriaRuntimeFactionZoneCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:509 (via AetheriaRuntimeRunCheckpointCommit.HomeZones)
- AetheriaRuntimeShotReceiptCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:539 (via AetheriaRuntimeRunCheckpointCommit.ShotReceipts)
- AetheriaRuntimePickupContactReceiptCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:591 (via AetheriaRuntimeRunCheckpointCommit.PickupContactReceipts)
- AetheriaRuntimeDamageCellCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:603 (via AetheriaRuntimeShotReceiptCommit.DamageCells)
- AetheriaRuntimeCorporationSurveyCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:614 (via AetheriaRuntimeRunCheckpointCommit.CorporationSurveys)
- AetheriaRuntimeFactionRelationshipCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:623 (via AetheriaRuntimeRunCheckpointCommit.FactionRelationships)
- AetheriaRuntimeZoneSnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:636 (via AetheriaRuntimeRunCheckpointCommit.Zones)
- AetheriaRuntimePhysicalPayloadCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:699 (via AetheriaRuntimeZoneSnapshotCommit.PhysicalPayloads)
- AetheriaRuntimeDroppedPickupCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:771 (via AetheriaRuntimeZoneSnapshotCommit.DroppedPickups)
- AetheriaRuntimeOrbitSnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:805 (via AetheriaRuntimeZoneSnapshotCommit.Orbits)
- AetheriaRuntimeBodySnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:830 (via AetheriaRuntimeZoneSnapshotCommit.Bodies)
- AetheriaRuntimeBodyResourceCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:901 (via AetheriaRuntimeBodySnapshotCommit.Resources)
- AetheriaRuntimeAsteroidCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:911 (via AetheriaRuntimeBodySnapshotCommit.Asteroids)
- AetheriaRuntimeAsteroidMiningAccumulatorCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:936 (via AetheriaRuntimeAsteroidCommit.MiningAccumulators)
- AetheriaRuntimeColorCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1008 (via AetheriaRuntimeGasGiantVisualCommit.Colors)
- AetheriaRuntimeEntitySnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1024 (via AetheriaRuntimeZoneSnapshotCommit.Entities)
- AetheriaRuntimeFogFieldEmitterCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1198 (via AetheriaRuntimeEntitySnapshotCommit.FogFieldEmitters)
- AetheriaRuntimeEquipmentStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1240 (via AetheriaRuntimeEntitySnapshotCommit.EquipmentStates)
- AetheriaRuntimeLoadoutGenerationSelectionCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1264 (via AetheriaRuntimeLoadoutGenerationReceiptCommit.Selections)
- AetheriaRuntimeEntityContactCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1275 (via AetheriaRuntimeEntitySnapshotCommit.Contacts)
- AetheriaRuntimeEntityStatGridCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1291 (via AetheriaRuntimeEntitySnapshotCommit.StatGrids)
- AetheriaRuntimeActiveConsumableCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1307 (via AetheriaRuntimeEntitySnapshotCommit.ActiveConsumables)
- AetheriaRuntimeConsumableBehaviorStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1330 (via AetheriaRuntimeActiveConsumableCommit.BehaviorStates)
- AetheriaRuntimeBehaviorProgressCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1355 (via AetheriaRuntimeEntitySnapshotCommit.BehaviorProgress)
- AetheriaRuntimeWeaponStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1374 (via AetheriaRuntimeEntitySnapshotCommit.WeaponStates)
- AetheriaRuntimeBehaviorStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1444 (via AetheriaRuntimeEntitySnapshotCommit.BehaviorStates)
- AetheriaRuntimeStarbridgeStationStockItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:71 (via AetheriaRuntimeStarbridgeScenarioDocument.StationStock)
- AetheriaRuntimeStarbridgeWaveDefinition Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:83 (via AetheriaRuntimeStarbridgeScenarioDocument.Waves)
- AetheriaRuntimeStarbridgeWaveForecast Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:94 (via AetheriaRuntimeStarbridgeSessionSummaryDocument.WaveForecast)
- AetheriaRuntimeStarbridgeRuntimeRole Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:104 (via AetheriaRuntimeStarbridgeScenarioDocument.RuntimeRoles)
- AetheriaRuntimeAuthorityRuntimeRole Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:194 (via AetheriaRuntimeVerseAuthorityPolicyDocument.RuntimeRoles)
- AetheriaRuntimeAuthorityRule Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:201 (via AetheriaRuntimeVerseAuthorityPolicyDocument.Rules)
- AetheriaRuntimeArenaSeat Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:255 (via AetheriaRuntimeArenaRosterDocument.Seats)
## AquaSynth (0 element types)
None.
## Aquarium (1 element types)
- AquariumFaceVoiceEndpointState src/Aquarium.Epiphany/State/AquariumLiveState.cs:61 (via AquariumLiveState.FaceVoiceEndpoints)
## Brokkr (4 element types)
- BrokkrGameObjectSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:42 (via BrokkrHostSnapshot.sceneObjects)
- BrokkrComponentSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:62 (via BrokkrGameObjectSnapshot.components)
- BrokkrSerializedPropertySnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:73 (via BrokkrComponentSnapshot.properties)
- BrokkrAssetSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:84 (via BrokkrHostSnapshot.assets)
## Delvehold (0 element types)
None.
## Eve (17 element types)
- EveAssetCatalogEntry packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:52 (via EveAssetCatalogDocument.Assets)
- EveAssetVariant packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:89 (via EveAssetCatalogEntry.Variants)
- EveEntitySoaBuffer packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:33 (via EveEntitySoaViewDocument.Buffers)
- EveEntitySoaColumn packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:41 (via EveEntitySoaViewDocument.Columns)
- EveEntitySoaDirtyRange packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:55 (via EveEntitySoaViewDocument.DirtyRanges)
- EveEntityIdentity packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:64 (via EveEntitySoaViewDocument.Identities)
- EveEntityRenderGroup packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:77 (via EveEntitySoaViewDocument.RenderGroups)
- EveInputActionDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:22 (via EveInputCapabilityDocument.Actions)
- EveInputProfileDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:51 (via EveInputCapabilityDocument.DefaultProfiles)
- EveInputBindingDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:59 (via EveInputProfileDocument.Bindings)
- EveProviderWitness packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:249 (via EveProviderAdvertisementDocument.Witnesses)
- EveAdvertisedSurface packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:265 (via EveProviderAdvertisementDocument.Surfaces)
- EveAdvertisedCommand packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:333 (via EveProviderAdvertisementDocument.Commands)
- EveSurfaceComponent packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:106 (via EveSurfaceComponent.Children)
- EveEmbeddedDocumentSlot packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:230 (via EveSurfaceComponent.EmbeddedDocuments)
- EveStyleToken packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:277 (via EveSurfaceTree.Styles)
- EveCommandTemplate packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:295 (via EveSurfaceDocument.Commands)
## EvePlugins (1 element types)
- EveFieldsSplatLayer plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields/Runtime/EveFieldsDocuments.cs:38 (via EveFieldsSplatsDocument.Layers)
## Ghostlight (17 element types)
These types are a vendored copy of Eve (`vendor/eve`); they change when Eve does and the pin bumps.
- EveAssetCatalogEntry vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:52 (via EveAssetCatalogDocument.Assets)
- EveAssetVariant vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:89 (via EveAssetCatalogEntry.Variants)
- EveEntitySoaBuffer vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:33 (via EveEntitySoaViewDocument.Buffers)
- EveEntitySoaColumn vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:41 (via EveEntitySoaViewDocument.Columns)
- EveEntitySoaDirtyRange vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:55 (via EveEntitySoaViewDocument.DirtyRanges)
- EveEntityIdentity vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:64 (via EveEntitySoaViewDocument.Identities)
- EveEntityRenderGroup vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:77 (via EveEntitySoaViewDocument.RenderGroups)
- EveInputActionDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:22 (via EveInputCapabilityDocument.Actions)
- EveInputProfileDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:51 (via EveInputCapabilityDocument.DefaultProfiles)
- EveInputBindingDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:59 (via EveInputProfileDocument.Bindings)
- EveProviderWitness vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:249 (via EveProviderAdvertisementDocument.Witnesses)
- EveAdvertisedSurface vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:265 (via EveProviderAdvertisementDocument.Surfaces)
- EveAdvertisedCommand vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:333 (via EveProviderAdvertisementDocument.Commands)
- EveSurfaceComponent vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:106 (via EveSurfaceComponent.Children)
- EveEmbeddedDocumentSlot vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:230 (via EveSurfaceComponent.EmbeddedDocuments)
- EveStyleToken vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:277 (via EveSurfaceTree.Styles)
- EveCommandTemplate vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:295 (via EveSurfaceDocument.Commands)
## Gjallar (3 element types)
- GjallarAdvertisedSchema src/Gjallar/VerseState.cs:462 (via GjallarProviderAdvertisementRecord.Schemas)
- GjallarAdvertisedWitness src/Gjallar/VerseState.cs:470 (via GjallarProviderAdvertisementRecord.Witnesses)
- GjallarAdvertisedSurface src/Gjallar/VerseState.cs:480 (via GjallarProviderAdvertisementRecord.Surfaces)
## Heimdall (12 element types)
These types are a vendored copy of CultLib (`vendor/CultLib`); C2a already adds the ids in CultLib, so they resolve when the pin bumps. The witness bundle types are the fork reported with C2a.
- CultGeometryDomainNode vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:61 (via CultGeometryDomainNode.Children)
- CultGeometryFeatureClaim vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:106 (via CultGeometryDomainNode.Claims)
- CultGeometryContributionRow vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:262 (via CultGeometrySelectedCutManifest.Diagnostics)
- CultMeshBodyDescriptor vendor/CultLib/src/GameCult.Mesh/CultMeshBodies.cs:35 (via CultMeshBodyPublicationDocument.Representations)
- CultMeshCdnChunkRef vendor/CultLib/src/GameCult.Mesh/CultMeshCdn.cs:73 (via CultMeshCdnArtifactManifest.Chunks)
- CultMeshDiscoveryCandidateDocument vendor/CultLib/src/GameCult.Mesh/CultMeshDiscoveryService.cs:418 (via CultMeshDiscoveryStateDocument.Candidates)
- CultMeshEntityPrefabComponent vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:81 (via CultMeshEntityPrefabNode.Components)
- CultMeshEntityPrefabAssetRef vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:96 (via CultMeshEntityPrefabPackage.Assets)
- CultMeshEntityPrefabNode vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:127 (via CultMeshEntityPrefabPackage.Nodes)
- CultWitnessContractPin vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:105 (via CultWitnessArtifactBundle.Contracts)
- CultWitnessArtifactEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:136 (via CultWitnessArtifactBundle.Artifacts)
- CultWitnessTimingEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:179 (via CultWitnessArtifactBundle.TimingWitnesses)
## Mimir (0 element types)
None.
## StreamPixels (20 element types)
`CultGeometryDomainNode` is the vendored CultLib copy; C2a already adds its id.
- ActivationCondition apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ActivationCondition.cs:18 (via CharacterConditions.Conditions)
- BattleEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/BattleEffect.cs:8 (via BattleAction.Effects)
- GearStatEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/GearStatEffect.cs:7 (via GearData.StatEffects)
- OverlaySceneIntent apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ServiceLink/OverlayFrameDocuments.cs:33 (via OverlaySceneIntentWindow.Intents)
- PlayedIntentRecord apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ServiceLink/PlayedIntentLedger.cs:25 (via OverlayPlayedIntents.Entries)
- StatModifier apps/overlay-unity/Assets/Scripts/Assembly-CSharp/StatModifier.cs:7 (via GearStatEffect.Modifiers)
- StatusEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/StatusEffect.cs:10 (via StatusData.Effects)
- WeaponDamageTypeMultiplier apps/overlay-unity/Assets/Scripts/Assembly-CSharp/WeaponDamageTypeMultiplier.cs:7 (via WeaponData.DamageMultipliers)
- CultGeometryDomainNode vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:61 (via CultGeometryDomainNode.Children)
- CultGeometryFeatureClaim vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:106 (via CultGeometryDomainNode.Claims)
- CultGeometryContributionRow vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:262 (via CultGeometrySelectedCutManifest.Diagnostics)
- CultMeshBodyDescriptor vendor/CultLib/src/GameCult.Mesh/CultMeshBodies.cs:35 (via CultMeshBodyPublicationDocument.Representations)
- CultMeshCdnChunkRef vendor/CultLib/src/GameCult.Mesh/CultMeshCdn.cs:73 (via CultMeshCdnArtifactManifest.Chunks)
- CultMeshDiscoveryCandidateDocument vendor/CultLib/src/GameCult.Mesh/CultMeshDiscoveryService.cs:418 (via CultMeshDiscoveryStateDocument.Candidates)
- CultMeshEntityPrefabComponent vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:81 (via CultMeshEntityPrefabNode.Components)
- CultMeshEntityPrefabAssetRef vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:96 (via CultMeshEntityPrefabPackage.Assets)
- CultMeshEntityPrefabNode vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:127 (via CultMeshEntityPrefabPackage.Nodes)
- CultWitnessContractPin vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:105 (via CultWitnessArtifactBundle.Contracts)
- CultWitnessArtifactEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:136 (via CultWitnessArtifactBundle.Artifacts)
- CultWitnessTimingEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:179 (via CultWitnessArtifactBundle.TimingWitnesses)
## Ymir (1 element types)
- YmirTransportLoweringDocument src/Ymir.Core/YmirServicePublicationDocument.cs:63 (via YmirProviderAdvertisementDocument.CommandLowerings)
## CultLib itself
C2a adds ids to: `CultGeometryDomainNode`, `CultGeometryFeatureClaim`, `CultGeometryContributionRow` (`src/GameCult.Geometry/CultGeometryDocuments.cs`); `CultMeshBodyDescriptor` (`src/GameCult.Mesh/CultMeshBodies.cs:35`); `CultMeshDiscoveryCandidateDocument` (`CultMeshDiscoveryService.cs:418`); `CultMeshEntityPrefabAssetRef`, `CultMeshEntityPrefabComponent`, `CultMeshEntityPrefabNode` (`CultMeshEntityPrefabs.cs`); `CultWitnessContractPin`, `CultWitnessArtifactEntry`, `CultWitnessTimingEntry` (`src/GameCult.Networking/CultWitnessArtifactBundle.cs`).
`CultMeshCdnChunkRef` (`src/GameCult.Mesh/CultMeshCdn.cs:73`) has a derived id, `[CultElementId(nameof(Offset))]`: the decimal text of its offset, never random, so packing the same body encodes the same manifest bytes. `contracts/cultmesh/content-vectors.*.json` and the Rust runtime carry it. A consumer with content-addressed elements declares the same way.

View file

@ -0,0 +1,153 @@
# CultCache Document Variants: Target
Date: 2026-09-17
Status: target, before any substrate map or cut map. This document owns the ends.
A cut map, written after an Imagination pass over the Body, will own the means.
Branch: `codex/document-variants`.
## Why
A CultCache record today is always complete. To make a bigger laser from a
laser, an author copies the record and re-specifies every stat. For a ship hull
with several hardpoint layouts, every layout is a full copy of the hull. When
the base changes, each copy has to be found and edited by hand, and copies drift
apart silently.
The first consumer is Aetheria's catalog (`F:\Projects\Aetheria\GameData\Aetheria.cc`),
edited in CultCache Studio:
- **Hull variants:** one hull with several hardpoint layout options. A variant
replaces the whole `HullData.Hardpoints` member.
- **Item variants:** a bigger laser. A variant changes stats nested inside the
design's behavior list, for example one behavior's `PerformanceStat.Max`.
## Rulings (operator, 2026-09-17)
- **This is a CultCache feature, not an Aetheria mechanism.** Operator: "Yes, this
is absolutely a CultCache feature, write it up in CultLib".
- **A variant is stored as a delta from its base and resolved by the cache.** A
record stores a reference to its base plus a sparse set of overrides. The
cache resolves it; readers see complete documents. The operator chose this
over two alternatives. Authoring-only variants in Studio over flattened stored
copies leave a derived cache pretending to be data. Per-type variant records
in each consumer rebuild the feature bespoke for every type.
## Ends
- **Readers see complete documents.** `Get`, `GetAll`, lookups by name and by
index, watches and snapshots return resolved documents. No consumer outside
the cache and its authoring tools handles a delta.
- **A base change reaches its variants.** Editing a base changes every variant
that does not override the edited value. Watches, indexes and name lookups
observe that change on the variants too, in the same commit that changed the
base.
- **A variant is a record.** It has its own key and name, and references to it
behave like references to any record. In Aetheria a lot's design and a
product's design can name a variant directly.
- **Studio edits the delta.** It shows which values a variant inherits and which
it overrides, and it can clear an override so the base value applies again.
- **Loading refuses a broken variant loudly and names the records.** Cases:
- a cycle of bases;
- a base that does not exist;
- a base whose type the variant's type cannot inherit from;
- ~~an override naming a member that does not exist on the type~~ **superseded 2026-09-29
(operator, variants Q4 c): a dangling override soft-drifts like any dropped member, ignored
with a warning; see `document-variants-cut.md` section 4.**
Deleting a base that still has variants is refused.
## Invariants that must survive
- **Wire parity** for the persisted format (`src/GameCult.Caching/Contracts/cultcache-persistence-format.md`,
`docs/persisted-record-encoding.md`). Every runtime that reads a store holding
variants resolves them identically, or refuses the store loudly. Which
runtimes resolve is decided in the map, against
`docs/runtime-parity-scope.md`; none may misread silently.
- **One home store per type**, and atomic single-store commit
(`cultcache-store-composition.md`). A base change and the resulting variant
changes are one commit, not a repair that runs afterwards.
- **Schema compatibility** (`cultcache-schema-compatibility.md`): overrides are
subject to the same semantic identity, soft drift and hard rejection rules as
full records.
- **Compare-exchange** (`Expect`, `ExpectUnchanged`) keeps a single meaning for
variants. The map settles whether it tests the stored delta or the resolved
document; it must not mean both.
## Fork rulings (operator, 2026-09-17)
- **1, granularity: stable element identity, ids everywhere** (operator, 2026-09-17: "ids
everywhere").
- Every object element in a list carries a stable id, minted when the element is created.
This is the approach of sequence CRDTs (RGA, YATA/Yjs, Automerge, Fugue).
- Matching algorithms (Myers, diff3, GumTree) are heuristics. They fail on look-alike
elements such as two `EnergyDraw` behaviors, which is why identity is minted, not
inferred.
- An override addresses an element id plus a member path in CultNet's grammar. An
insertion is a new element anchored after an existing id; a removal names the
removed id. Index addressing is out.
- Rebasing is deterministic. An override whose element id exists in the new base
re-applies. One whose element is missing is an explicit conflict that Studio shows,
to keep, retarget or clear; it is never silently retargeted.
- Existing stores get ids minted once, by a rewrite.
- **2, where resolution lives: at load, kept in memory** (operator: "agreed"). Resolution
computes a variant's complete document from its base chain and its overrides.
- The cache holds both the stored delta and the resolved document.
- A base edit re-resolves its variants inside the same commit, so watches and indexes
observe the change atomically.
- Compare-exchange (`Expect`) tests the stored delta. The resolved document is derived.
- **3, cross-store bases: no.** A variant and its base live in the same store.
*Future direction (operator, 2026-09-30, from Aetheria moddable ships):* "we'll want cross-store variant
support eventually; modders should be able to add variants of existing items without modifying the
original". Ruling 3 holds for this campaign. A mod store layering variants over a shipped catalog is the
named future consumer that would reopen it. Until then, no consumer builds its own cross-store resolver:
Aetheria's compose step stays out of resolution.
- **4, CultNet: deltas.**
- **5, runtimes: C# first.** TypeScript, Python, Rust and Kotlin refuse a store
or snapshot containing variants, loudly, until each implements resolution.
- **6, type change: the same concrete type only** (operator: "agreed").
- An override can only name members the base type has, and rebasing requires the same
type.
- A variant whose type differs from its base's is refused loudly.
- Subtype variants are parked until real content needs a template base. A record
flagged as a template, not a real item, may be the smaller feature.
- **7, clearing and rebasing.**
- Clearing an override removes that member from the variant's stored delta. A member
that is not overridden is not stored.
- Rebasing (changing a variant's base) is supported. Operator: "rebasing is a nice
tool to have, things can get annoying otherwise".
## Open forks, for the Imagination pass to map and the operator to rule
1. **Override granularity.**
- Top-level members are enough for hardpoint layouts.
- A bigger laser needs overrides inside list elements. Addressing list
elements by index is fragile: an insertion into the base silently retargets
every variant's overrides.
- Candidates: stable element identity (for example a behavior's type within its
design); replacing whole list elements only; or addressing paths.
- CultNet's selection work already resolves member paths per leaf type,
inherited members included (`docs/cultnet-selection-cut.md`). A variant's
delta addressing should reuse that grammar, not invent a second one.
2. **Where resolution lives:** at load in the cache core, lazily on read, or as a
materialized view the cache maintains. This affects memory, watch cost, and
the compare-exchange meaning.
3. **Base and variant across stores.** Can a variant in one store name a base in
another (a run-store variant of a catalog record)? This interacts with
one-home-store routing and read-only stores.
4. **CultNet.** Do variants travel as deltas, or resolved? A subscriber that
receives only resolved documents needs no resolver; one that edits needs
deltas.
5. **Runtime scope.** Which of C#, TypeScript, Python, Rust and Kotlin resolve,
and which refuse a store containing variants.
6. **Type change.** May a variant's type be a subtype of its base's type,
adding members, or must the two types match?
7. **Clearing and reparenting.** What clearing an override writes, and whether
a variant can change its base.
## Not in scope
- Scaling or formula variants ("every stat times 1.5"). An override states a
value. Generated authoring can compute values.
- Any Aetheria catalog content. Aetheria adopts the feature after a release, as
its own cut.

159
docs/mutation-testing.md Normal file
View file

@ -0,0 +1,159 @@
# Mutation Testing
CultLib measures its C# test suites with Stryker.NET, pinned as a repo-local
tool in `dotnet-tools.json`. Coverage is added project by project; a project
with no `stryker-config.json` next to its test project has not been scoped
yet (see "Follow-ups" below for what's outstanding).
## Running it
Restore the tool once per checkout:
```powershell
dotnet tool restore
```
Run a full mutation pass from a covered test project's directory, for
example CultMath:
```powershell
cd packages/cultmath/tests/CultMath.Tests
dotnet stryker -t mtp
```
`-t mtp` selects Stryker's Microsoft.Testing.Platform runner. It is required
for test projects on `xunit.v3` (CultMath.Tests is one): Stryker's default
`vstest` runner's coverage capture does not work against xunit.v3's native
test host and the run stalls retrying test sessions indefinitely. Projects
still on `xunit` v2 with `xunit.runner.visualstudio` can use the default
`vstest` runner.
For a single cut, scope the run to what changed instead of the whole
project:
```powershell
dotnet stryker -t mtp --since:<base-commit-or-branch>
```
Use a full commit SHA for `--since`, not an abbreviation: `git rev-parse
<short-sha>` first, then pass the full result. Stryker resolves `--since`
through the same git plumbing a short SHA can resolve ambiguously (or fail to
resolve at all) depending on the checkout's object database, and a
misresolved base silently changes what "since" means instead of erroring.
In a container, also set `git config --global --add safe.directory '*'`
before running Stryker (or any other command that shells out to `git`
against this checkout) - git refuses to operate on a repository owned by a
different user by default, which is the common case for a bind-mounted
checkout in a container.
Reports land in `StrykerOutput/` next to the test project (git-ignored).
Open the `reports/mutation-report.html` file for a browsable view, or read
`reports/mutation-report.json` for a scriptable one.
## Triage rules
The mutation score is not a gate (`break` threshold is 0). Every surviving
mutant is a finding about the tests, not a build failure, and gets triaged
into one of:
- **missing behavioural test** — name the test that should exist and what
branch, edge case, or component it would exercise.
- **degenerate fixture** — the code path is exercised, but the fixture's
values make two different mutants produce the same observable result (a
zero component that erases a `+`/`-` distinction, an already-normalized
input that erases a `*`/`/` distinction, all-equal or all-same-sign inputs
that erase a boundary or `&&`/`||` distinction). Fix the fixture, don't
invent a new one that only kills that one mutant.
- **equivalent** — the mutant cannot change behaviour. Float-threshold
boundary flips (`>` to `>=`) are equivalent by default. A reasoned
equivalence claim (for example, `>>` and `>>>` on an already-unsigned
operand) needs its one-line reason recorded, not assumed.
Do not write a hand-rolled mutation suite or anchor test as a substitute.
Where the tool can't reach (native/shader/editor code), the defence is a
behavioural test at the layer the rule is decided, not a committed mutation
harness.
## Known tool caveat
Stryker 5.0.0's `mtp` runner is documented as preview. Its per-test coverage
capture was found to report false "Survived" verdicts specifically for
mutations inside `static readonly` field initializers that a test does
cover (confirmed by hand-mutating `bool3.@false` and `bool4.@true` and
running the full suite directly with `dotnet test`, both of which fail
against the mutation that Stryker reported as surviving). Treat a survivor
inside a `static readonly` field initializer as unproven until confirmed
with a direct `dotnet test` run against the same mutation; do not triage it
as a missing test without that check.
A survivor that looks like it must be covered is unproven until confirmed
with a direct `dotnet test` run against the same hand-applied mutation - but
confirm what the mutant actually is before drawing a tooling conclusion from
it. An earlier pass here reported three survivors in
`CultGeometrySurfaceNets.TryGetOtherEndpoint`'s per-axis `if (x + 1 >=
sizeX) return false;` guards and initially triaged them as a `vstest`
coverage false-survivor (the same shape as the `mtp`/`static readonly`
caveat above). That triage was wrong: the actual mutant Stryker reports
there is `return false` -> `return true` inside the guard, which is
equivalent, not a coverage miss. Returning `true` from
`TryGetOtherEndpoint` still leaves the out `other` coordinate equal to the
current coordinate (see the method: `other` is cloned from `coord` and only
one component is incremented when the method returns `true`), so the caller
compares a sample against itself, the inside/outside flags always match,
and the crossing is skipped exactly as it would have been had the method
correctly returned `false`. No test can distinguish the two branches because
there is nothing to distinguish - it is a legitimate equivalent mutant, not
evidence of a `vstest` false survivor.
`CultGeometrySurfaceNets.Extract`'s `if (inside == (OrientationSign[axis] <
0))` (the sole authority for quad winding after the geometric cross-product
fallback was deleted) has three mutants on that line: `==` -> `!=`, and `< 0`
negated to `> 0` or widened to `<= 0`. The first two, `inside != (OrientationSign[axis]
< 0)` and `OrientationSign[axis] > 0`, are both reported `Killed`. The third,
`OrientationSign[axis] <= 0`, survives, but it is an equivalent mutant, not a
coverage gap: `OrientationSign` is the fixed table `{ 1, -1, 1 }` and never
holds `0`, so `< 0` and `<= 0` agree on every value the table can actually
produce. There is nothing for a test to distinguish, the same shape as the
`TryGetOtherEndpoint` equivalent mutant above.
No `vstest` coverage-based false survivor has actually been confirmed for
GameCult.Geometry. Every survivor triaged here so far has turned out to be a
genuine equivalent mutant once hand-mutated and checked directly against
`dotnet test`; treat any survivor that looks like it must obviously be
covered as unproven until confirmed the same way, but do not cite this
project as precedent for the `vstest` caveat itself - that caveat is
`mtp`-runner-specific and documented above for `static readonly` field
initializers, and GameCult.Geometry has never actually hit it.
## Follow-ups
CultMath.Tests and GameCult.Geometry.Tests are the only projects currently
scoped. GameCult.Geometry's `stryker-config.json` has no `test-runner`
entry: the project is NUnit 4 with `NUnit3TestAdapter` and
`Microsoft.NET.Test.Sdk`, so Stryker's default `vstest` runner applies
directly (no `mtp` workaround needed, unlike CultMath.Tests's xunit.v3
suite).
GameCult.Geometry has only ever been run `--since:`-scoped to the cut that
added surface nets; a full-project baseline (running mutation against all of
`CultGeometryIsoSurface`, `CultGeometryDocuments`, and
`CultGeometryPrimitives`, not just the diff) is a recorded follow-up, not
something either cut did.
That first `--since` pass did not actually stay diff-scoped: adding
`stryker-config.json` itself in the same cut that first used it made Stryker
log "391 mutants will be tested because: Non-CSharp files in test project
were changed" and mutate the whole `mutate` glob (all four existing
GameCult.Geometry source files), not just the new surface-nets files. A cut
that changes a non-C# file inside the test project (a fixture, this config
file, anything under `Fixtures/`) loses `--since` line-level scoping for that
run; a future cut whose diff is C#-only, against a base commit that already
has `stryker-config.json`, should scope correctly.
`GameCult.Caching`, `GameCult.Mesh` (+ `.Quic`, `.Quic.Native`),
`GameCult.Networking` (+ `.WebSockets`), and the non-.NET runtimes
(`cultcache-rs/py/ts`, `cultmesh-*`, `cultnet-*`) each still need their own
`stryker-config.json` (or ecosystem-equivalent tool: `cargo-mutants` for
Rust, `StrykerJS` for TypeScript, `mutmut` for Python) and a baseline pass.
None of that is wired into CI yet.

View file

@ -1,5 +1,8 @@
# NuGet Packaging
See `docs/semver-policy.md` for what a version bump on these packages is
required to mean and how a release is checked against that before it ships.
CultLib publishes the managed dependency graph as separate packages. The leaf
package is `GameCult.Mesh`; its NuGet dependencies preserve the CultMesh,
CultNet, and CultCache ownership boundaries.
@ -8,9 +11,12 @@ CultNet, and CultCache ownership boundaries.
powershell -ExecutionPolicy Bypass -File .\scripts\pack-nuget.ps1
```
The command creates a local feed under `artifacts/nuget`, verifies that every
internal dependency package exists at the same version, then restores and runs
a clean .NET consumer using only `PackageReference Include="GameCult.Mesh"`.
The command packs `GameCult.Mesh`, `GameCult.Networking.WebSockets`, and every
project they reach through `ProjectReference` (including `GameCult.Math`, which
carries its own version) into a local feed under `artifacts/nuget`. It verifies
that each of those packages exists at the identity MSBuild reports for it, then
restores and runs a clean .NET consumer using only
`PackageReference Include="GameCult.Mesh"`.
The Unity distribution remains `org.gamecult.cultlib`, built by
`scripts/build-unity-package.ps1`, because Unity consumes the managed assembly
@ -19,5 +25,5 @@ closure through UPM rather than NuGet restore.
Install the Unity package from the immutable release tag:
```text
https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.46
https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v2.0.0
```

View file

@ -0,0 +1,170 @@
# Content-addressed HTTP answers: what the specs and sources say
Fork served: **cultmesh-ts-content C2 provider HTTP answers** (session label
self-2026-10-19-cm-content). Eyes pass, 2026-10-19. Facts with evidence pointers
only; no recommendation. Line numbers refer to the raw files fetched on
2026-10-19 (master/main at that date), so they drift.
Source shorthands:
- **TG** = IPFS Trustless Gateway spec, https://specs.ipfs.tech/http-gateways/trustless-gateway/ (source: https://raw.githubusercontent.com/ipfs/specs/main/src/http-gateways/trustless-gateway.md)
- **PG** = IPFS Path Gateway spec, https://specs.ipfs.tech/http-gateways/path-gateway/ (source: https://raw.githubusercontent.com/ipfs/specs/main/src/http-gateways/path-gateway.md)
- **OCI** = OCI Distribution Spec, https://raw.githubusercontent.com/opencontainers/distribution-spec/main/spec.md
- **Fetch** = WHATWG Fetch Standard, https://fetch.spec.whatwg.org/
- **RFC 9110/9111/9112**, https://www.rfc-editor.org/rfc/rfc9110.txt (and 9111, 9112, 8246)
Two things the draft model page said "from memory" are checked at the end
(section 8).
## 1. IPFS Trustless Gateway (`application/vnd.ipld.raw`)
| Case | What the spec says | Pointer |
|---|---|---|
| Found | Body is "An opaque bytes matching the requested block CID"; "The Body hash MUST match the Multihash from the requested CID." | TG "Block Responses (application/vnd.ipld.raw)" (src l.341-346) |
| Unknown/absent hash | `404 Not Found` MUST be returned when the root block is not in the gateway's storage; applies to HEAD for any CID and GET of raw blocks. Body not specified. For non-recursive gateways it "definitively signals" the content is not in the dataset. | TG "Response Status Codes / 404 Not Found" (src l.274-283); "Recursion" (l.606-612) |
| Malformed request | TG defines no status for a malformed CID. It says servers SHOULD "validate CID format and encoding before processing requests". PG says `400 Bad Request` is a "generic client error", example "malformed CID". TG gives 400 for missing `Accept` in strict trustless mode (l.108) and for an out-of-range `entity-bytes` (l.210). | TG "Security Considerations / HTTP Servers" (src l.616-628); PG "Response Status Codes" |
| Stored object failing its digest | **Not specified for servers.** The only requirement is client-side: clients MUST verify block digest against the CID before processing or storing. TG `500` is "only for genuine server errors, not for content unavailability" (storage failure, resource exhaustion, unexpected internal errors); corruption is not named. | TG "HTTP Clients / Block Validation" (src l.629-631); "500 Internal Server Error" (l.294-296) |
| Range | TG defines no Range support for raw blocks. PG says Range is "for requesting specific byte range of UnixFS files and raw blocks"; gateways SHOULD support single-range, multi-range optional; success is `206` with `Content-Range`; `Accept-Ranges` is optional, `none` is the example for non-deterministic CAR order. | PG "Range (request header)" (src l.184-193), "Content-Range" (l.626), "Accept-Ranges" (l.632-640) |
| HEAD | `HEAD /ipfs/{cid}` is GET without payload; missing root block is 404 for HEAD too. PG: gateways SHOULD do minimal work to produce headers such as `Content-Length`. For the probe path, all HEAD headers are optional. | TG "HEAD /ipfs/{cid}" (src l.68-72), "404" (l.274-283), "Dedicated Probe Paths" (l.525-548); PG HEAD text |
| CORS | **TG defines none.** PG only says gateways serving cross-origin traffic SHOULD list `Ipfs-Uri` (and `X-Ipfs-Path`) in `Access-Control-Expose-Headers`. Kubo's config doc says its default `Gateway.HTTPHeaders` is `{}` "+ implicit CORS headers from `boxo/gateway#AddAccessControlHeaders` and ipfs/specs#423"; the values themselves are not in that file. | PG header sections; https://raw.githubusercontent.com/ipfs/kubo/master/docs/config.md l.1484-1488 |
| Cache-Control | Response value is in PG, not TG: `Cache-Control: public, max-age=29030400, immutable` "MUST be returned for every immutable resource under `/ipfs/` namespace". (Request-side `Cache-Control: only-if-cached` returns `412` when the root block is not immediately available.) | PG "Cache-Control (response header)" (src l.488-500); TG "Cache-Control: only-if-cached" (l.120-126) |
| ETag | TG: "MUST be returned and follow the recommendations in" PG. PG default is the requested CID in quotes: `Etag: "bafy…foo"`; format-specific variants add a suffix (`"bafy…foo.raw"`); non-deterministic streams use `W/`. PG: gateway MUST compare `If-None-Match` against its `Etag` (answer 304). TG: clients SHOULD NOT assume ETags are portable across gateways. | TG "Etag (response header)" (src l.324-338); PG l.130-136, l.453-486 |
| Content-Type | "MUST be returned"; for raw blocks the type is `application/vnd.ipld.raw` (IANA registered). No charset parameter is specified. | TG "Content-Type (response header)" (src l.300-308); IANA entry linked at l.343 |
| Other headers | `Content-Disposition` MUST be `attachment` with `filename="<cid>.bin"` for raw blocks, "to ensure requested bytes are not rendered by a web browser". PG: `X-Content-Type-Options: nosniff` should be returned with `application/vnd.ipld.raw` and `.car` (from the PG fetch summary; line not re-grepped). | TG "Content-Disposition" (src l.310-316); PG "X-Content-Type-Options" |
| Content-Encoding / Length | PG: `Content-Length` "Represents the length of returned HTTP payload", and warns "the value may differ from the real size of requested data if compression or chunked `Transfer-Encoding` are used", citing ipfs/specs#461 (an open TODO in the spec). | PG "Content-Length (response header)" (src l.613-624) |
| Size cap | Clients SHOULD cap block size; the spec names 2 MiB as the p2p block limit (see Block Limits). | TG "Block Limits" (src l.558-573) and "HTTP Clients" (l.629-640) |
## 2. OCI Distribution Spec, blob `GET`/`HEAD /v2/<name>/blobs/<digest>`
| Case | What the spec says | Pointer |
|---|---|---|
| Found (GET) | "MUST provide the expected blob, with a response code that MUST be `200 OK`." Response MUST contain `Docker-Content-Digest`; "If present, the value of this header MUST be a digest matching that of the response body." Clients MAY ignore it, but if used MUST verify it; "Clients SHOULD verify that the response body matches the requested digest." | OCI "Pulling blobs" (src l.180-195) |
| Unknown | `404 Not Found` MUST be returned (GET and HEAD). The endpoint table lists only `200` success and `404` failure. Error code names `BLOB_UNKNOWN` (code-1) exist in the error table; this section does not require a JSON body. | OCI l.194, l.209-216; Endpoints table l.846 (end-2); Error Codes table l.891 |
| Malformed digest | **Not specified for blob pull.** The table lists only 404. (Other endpoints: PUT blob 400 on digest problems; DELETE blob 404/400/405.) `DIGEST_INVALID` is defined as "provided digest did not match uploaded content" (upload semantics). | OCI table l.846-858; code-4 l.894 |
| Stored object failing its digest | **Not specified for servers.** Client-side verification only (above). | OCI l.188-193 |
| Range | "A registry SHOULD support the `Range` request header in accordance with RFC 9110 (section 14)." No `206`/`416`/`Accept-Ranges` text for blob pull (416 appears only in the upload flow, l.392, 421). | OCI l.196 |
| HEAD | HEAD on an existing blob MUST return `200`; MUST contain `Docker-Content-Digest` and `Content-Length` ("the size in bytes"); not found is `404`. Note: older registries may omit the digest header; clients "can reasonably assume the digest algorithm used is sha256". | OCI "Checking if content exists" (src l.198-216) |
| CORS | **Not in spec.** | whole OCI spec (no `Access-Control` occurrence found in the `grep` of the file) |
| Cache-Control/ETag | **Not in spec.** | OCI spec |
| Content-Type | **Not specified for blob GET**; `application/octet-stream` appears only for upload requests. | OCI upload sections |
| Redirects | Registries may answer blob GET with a redirect; clients must not forward `Authorization` across hosts. | OCI pull text (from the WebFetch of the same file; not re-grepped) |
Known failures near this area (NVD records, fetched 2026-10-19):
- CVE-2024-3727 (containers/image, Red Hat): "integrity-check flaw" allowing unexpected authenticated registry access, resource exhaustion, local path traversal. It affects the client library, not a registry server. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-3727
- CVE-2026-85747 (distribution/distribution, fixed 3.1.2): the client's manifest `Get()` does not hash the body against the requested digest, so a malicious registry can substitute content. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-85747 (status "Awaiting Analysis")
- CVE-2026-93317 (Docker build): a registry or OCI-layout blob source could supply content not matching the claimed digest and have it cached under that digest. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-93317 (status "Awaiting Analysis")
- Searched for a path traversal CVE in a registry server's digest-to-path mapping: none surfaced.
## 3. Nix binary cache (HTTP)
The manual's Binary Cache page defines layout only (a `nix-cache-info` file, a
`.narinfo` per object with "a (usually relative) URL to the corresponding
compressed NAR", the NAR). It states no status codes, headers, CORS, Range or
Cache-Control. https://nix.dev/manual/nix/2.35/protocols/binary-cache/ (page head).
The behavior below is from the Nix client/writer source.
| Case | What the source shows | Pointer |
|---|---|---|
| Found | Writer stores NARs at `nar/<FileHash nix32>.nar<ext>`, i.e. named by the hash of the compressed file; narinfo records `FileHash`, `FileSize`, plus `NarHash`. | https://raw.githubusercontent.com/NixOS/nix/master/src/libstore/binary-cache-store.cc l.156-181 |
| Absent | Client maps HTTP `404` and `410` to `NotFound` ("The file is definitely not there"); `403` to `Forbidden`; `408`/`429` to `Transient`. `fileExists` issues a **HEAD** and treats `NotFound` **and `Forbidden`** as absent, "S3 buckets return 403 if a file doesn't exist and the bucket is unlistable". | https://raw.githubusercontent.com/NixOS/nix/master/src/libstore/filetransfer.cc l.826-838; .../http-binary-cache-store.cc l.184-202 |
| Malformed request | Not specified. | n/a |
| Failing digest | Client side only; the writer's `FileHash`/`NarHash` are checked by the consumer. No server answer defined. | binary-cache-store.cc l.175-181, l.313 |
| Range | Client records `acceptRanges` for retries (client-side use of Range). No server rule. | filetransfer.cc l.219 |
| HEAD | Used for existence probing (above). | http-binary-cache-store.cc l.190 |
| CORS / Cache-Control / ETag | Not in spec or the files read. | n/a |
| Content-Type | Writer uploads `text/x-nix-narinfo` (narinfo), `text/x-nix-cache-info`, and `application/x-nix-nar` (NAR). | binary-cache-store.cc l.57, l.135, l.270 |
| Content-Encoding | Writer can upload with a `Content-Encoding` header naming the compression (http-binary-cache-store.cc l.232). Client sets `CURLOPT_ACCEPT_ENCODING ""` (accept any supported) and tracks a `hasContentEncoding` flag. The NAR itself is stored already compressed (named by compressed-file hash), so transport encoding and stored compression are separate. | filetransfer.cc l.223-224, l.616 |
## 4. Bazel remote cache, HTTP
| Case | What the source/spec says | Pointer |
|---|---|---|
| Protocol | Bazel docs: "Binary data (BLOB) is uploaded via PUT requests and downloaded via GET requests"; action results under `/ac/`, files under `/cas/`; any "HTTP/1.1 server that supports PUT and GET" can serve. The doc page states **no** status codes, no digest verification on PUT, no Content-Type, no compression, no Range. HEAD is not mentioned there. | https://bazel.build/remote/caching "HTTP caching protocol" |
| bazel-remote README | CAS key is "the lowercase SHA256 hash of the entry"; `/cas/<key>` or `/<instance>/cas/<key>` (instance "optional but ignored"); "HEAD requests can be used to confirm whether a key exists"; the empty blob is "always available, even if the cache is empty" (HEAD example: `200` with `Content-Length: 0`); GET with `Accept-Encoding: zstd` "may" return zstd data; PUT of zstd requires `Content-Encoding: zstd` and `X-Digest-SizeBytes`. | https://raw.githubusercontent.com/buchgr/bazel-remote/master/README.md (HTTP section) |
| Found | `200`, `Content-Type: application/octet-stream`, `Content-Length` set to the stored size (uncompressed path). | https://raw.githubusercontent.com/buchgr/bazel-remote/master/server/http.go l.275-282 |
| Absent | `404`, body "Not found" via `http.Error`. | http.go l.268-271 |
| Malformed | Path must match `^/?(.*/)?(ac/\|cas/)([a-f0-9]{64})$` (lowercase 64-hex); otherwise `400` with the escaped URL echoed in the message (`html.EscapeString`). | http.go l.33, l.101-112, l.216-223 |
| Failing digest (read) | No digest check on the GET path in this handler (reader returned by `cache.Get`, `io.Copy` to the client). A failure after headers are sent is logged and cannot change the status ("we've already started writing data"). | http.go l.284-297 |
| Digest check (write) | PUT checks size match (`400` "sizes don't match") and `Content-Encoding` is `identity`/`zstd`/absent (else `400`); the remaining digest verification is in code not read here. | http.go l.306-345, l.372-388 |
| Compression | When zstd is chosen the handler sets `Content-Encoding: zstd` and **omits `Content-Length`** ("TODO: calculate Content-Length for compressed blobs too"). | http.go l.246-250, l.275-281 |
| HEAD | `200` with `Content-Length` of the size, or `404`. | http.go l.468-491 |
| Range / CORS / Cache-Control / ETag | Not present in the handler excerpts read. | n/a |
## 5. Git over HTTP
Smart HTTP does not address single objects. Dumb HTTP does: URL
`$GIT_URL/objects/XX/YYYY…` for a loose object.
https://git-scm.com/docs/http-protocol (introduction example).
| Case | What the spec/source says | Pointer |
|---|---|---|
| Absent | Server "MUST NOT respond with `200 OK`"; "SHOULD respond with `404 Not Found`, `410 Gone`, or any other suitable HTTP status code which does not imply the resource exists". Access denied MUST be `403`. | git http-protocol "General Request Processing" |
| Caching | Clients MAY reuse a cached entity per `Cache-Control`/`Expires` (follow RFC 2616); servers MAY return `304`; clients treat `304` as `200`. Smart services must prevent caching. The page states no Cache-Control value for object files. | same section |
| Content-Type | Specified only for `info/refs` (`text/plain; charset=utf-8` SHOULD, clients MUST NOT validate); nothing for object files. | same page |
| Failing digest | Client side: the dumb walker compares the received object's computed id with the requested id and errors `File %s has bad hash`. | https://raw.githubusercontent.com/git/git/master/http-walker.c l.540-541 |
| Missing target | The client treats a missing-target result as `HTTP_MISSING_TARGET` (http.c l.1944). | https://raw.githubusercontent.com/git/git/master/http.c |
| Content-Encoding | Client sets `CURLOPT_ENCODING ""` at http.c l.2284 (accept any encoding curl supports, curl decodes transparently) and sends `Pragma: no-cache` (l.1452). The http-protocol page says nothing about gzip for objects. | http.c |
| Range, CORS, ETag | Not in the page. | n/a |
## 6. Browser fetch facts (verifying reader)
| Fact | Evidence |
|---|---|
| CORS-safelisted response-header names are exactly `Cache-Control`, `Content-Language`, `Content-Length`, `Content-Type`, `Expires`, `Last-Modified`, `Pragma`. So `Content-Length` is readable cross-origin from a CORS response without `Access-Control-Expose-Headers`. `ETag` is **not** on the list; it needs `Access-Control-Expose-Headers` to be visible. | Fetch 2.2.2 "Headers", CORS-safelisted response-header name (https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) |
| The CORS-exposed header-name list comes from `Access-Control-Expose-Headers`; a CORS filtered response exposes safelisted names plus that list. | Fetch 2.2.6 "Responses" (https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list); 3.3.3 |
| CORS check: failure if `Access-Control-Allow-Origin` is absent; "If request's credentials mode is not "include" and origin is `*`, then return success." With credentials mode `include`, `*` is not enough (needs exact origin plus `Access-Control-Allow-Credentials: true`). | Fetch 4.10 "CORS check"; 3.3.5 |
| A CORS-preflight happens for non-safelisted methods/headers (`GET`/`HEAD` with no custom headers are simple requests). | Fetch 3.3 (general); not re-quoted here |
| Fetch decodes content codings before exposing the body. In HTTP-network fetch: it records encoded size from transmitted bytes, sets `bytes` to "the result of handling content codings", then increases decoded size by the decoded length. Unsupported codings: bytes are returned as-is; decoding error: failure. | Fetch 4.7 "HTTP-network fetch"; 2.2.4 "handle content codings" (https://fetch.spec.whatwg.org/#handle-content-codings) |
| The spec itself notes: "This makes the `Content-Length` header unreliable to the extent that it was reliable to begin with." (the header describes the encoded body; the body the page reads is decoded.) | Fetch 4.7 (fetch_t.txt l.9670) |
| When a request has a `Range` header, the browser appends `Accept-Encoding: identity`, "This avoids a failure when handling content codings with a part of an encoded response." | Fetch 4.6 "HTTP-network-or-cache fetch" (l.8760-8767) |
| HTTP framing: with a valid `Content-Length`, a response is incomplete if fewer octets arrive than `Content-Length`; a client MUST record it as incomplete. `Content-Length` "refers specifically to the amount of data enclosed" (encoded form on the wire). | RFC 9112 section 8 "Handling Incomplete Messages"; RFC 9110 section 8.6 |
| `Cache-Control: no-transform`: an intermediary "MUST NOT transform the content". | RFC 9111 section 5.2.2.6 |
| `immutable` extension: identifies responses that will not be updated during freshness lifetime so clients need not revalidate. | RFC 8246 section 2 |
## 7. HTTP core facts that apply to all five systems
| Fact | Evidence |
|---|---|
| Range: "A server MAY ignore the Range header field"; a server MUST ignore Range for methods without defined range handling (GET is the only one). | RFC 9110 section 14.2 "Range" (l.6476-6484) |
| HEAD: "server SHOULD send the same header fields" as for GET; a `Content-Length` on HEAD MUST equal the GET length if sent. | RFC 9110 section 9.3.2 (l.3993), section 8.6 (l.3225) |
| `304`: a `Content-Length` in 304 MUST equal what a 200 would carry, if present. | RFC 9110 section 8.6 |
| `Content-Length` MUST NOT be sent on `204`. | RFC 9110 section 8.6 |
## 8. Cross-check against the draft model page's from-memory claims
| Draft claim (map l.44-49) | Finding |
|---|---|
| IPFS trustless gateway answers raw blocks with `Cache-Control: public, max-age=29030400, immutable` | In **PG**, not TG: the `/ipfs/` immutable value is a MUST there; TG itself does not state the response value. |
| IPFS uses the CID as `ETag` | PG: default `Etag: "<cid>"` quoted; variants add suffixes (raw: `"<cid>.raw"`). |
| IPFS returns 400 for an unparseable CID | PG lists `400` as "generic client error" with malformed-CID as the example. TG itself lists no malformed-CID rule. |
| OCI `GET`/`HEAD` returns 200 with the digest header | Confirmed (`Docker-Content-Digest` MUST on GET and HEAD; `Content-Length` MUST on HEAD). |
| OCI 404 `BLOB_UNKNOWN` | 404 confirmed; `BLOB_UNKNOWN` exists as a code name; the blob-pull text does not mandate a JSON body. |
| OCI 400 `DIGEST_INVALID` on a bad digest | **Not in the blob-pull text.** Endpoint table lists only 404 for blob GET/HEAD; `DIGEST_INVALID` means "digest did not match uploaded content". |
| OCI Range (206/416) optional | Spec says "SHOULD support" Range per RFC 9110; no 206/416/Accept-Ranges text for pull. |
| Nix `<hash>.narinfo` 200 or 404, probed with HEAD | Client source: HEAD for existence; 404 and 410 both map to NotFound; 403 also treated as absent in `fileExists`. |
| Map's own claims (open `*` CORS, 500 on corrupt, Range ignored, Cache-Control immutable) | `*` with non-`include` credentials passes the CORS check (Fetch 4.10). No system surveyed specifies a status for a corrupt stored object. Ignoring Range is permitted (RFC 9110 14.2). |
## 9. Where the systems agree and differ (matrix of what was found)
| Item | IPFS TG/PG | OCI | Nix | Bazel (bazel-remote) | Git dumb |
|---|---|---|---|---|---|
| Absent | 404 (MUST, TG) | 404 (MUST) | 404/410 -> NotFound; 403 also "absent" in HEAD probe | 404 "Not found" | 404/410/other non-200 |
| Malformed id | PG 400 generic; TG silent | silent for blob pull | silent | 400 with message | silent |
| Corrupt stored object (server) | silent; client MUST verify | silent; client SHOULD verify | silent; client verifies hashes | no read-path check in handler | silent; client errors "bad hash" |
| Range | PG: 206 supported (raw blocks); TG silent | SHOULD support | client-side retry only | not in handler | not in spec |
| HEAD | same as GET minus body; 404 | 200 + digest + Content-Length MUST | used to probe | 200 + Content-Length or 404 | not in spec |
| CORS | none in TG/PG (Kubo adds implicit headers) | none | none | none | none |
| Cache-Control on content | PG: `public, max-age=29030400, immutable` MUST | none | none | none | defers to RFC 2616 |
| ETag | PG: quoted CID | none | none | none | 304 allowed |
| Content-Type | `application/vnd.ipld.raw` | unspecified (pull) | `application/x-nix-nar` etc. | `application/octet-stream` | unspecified (objects) |
| Content-Length vs encoding | PG warns they may differ | n/a | transport `Content-Encoding` separate from stored compression | zstd path omits Content-Length | client auto-decodes |
| nosniff / attachment | TG: Content-Disposition attachment MUST; PG: nosniff | none | none | none | none |
## 10. Limits of this pass
- Fetch spec 4.7 was read from the HTML text; the exact algorithm line that fails the fetch on a body shorter than Content-Length was not found in the Fetch text (framing is RFC 9112 section 8, which says the client records the message incomplete).
- bazel-remote digest verification on PUT was not read to the end; the read-path (GET) has no digest check in the handler.
- Kubo's actual default CORS header values were not retrieved (only the pointer to boxo and ipfs/specs#423).
- git CVEs were not searched; Nix and Bazel CVEs were not searched.
- OCI "CORS: not in spec" is from a search of the spec file, not from a registry implementation (distribution/distribution's own headers were not read).

View file

@ -0,0 +1,59 @@
# Consumer pinning at a breaking land: prior art and internal state
Serves fork `variants:question:unpinned-consumers-at-land` (campaign `variants`, session `self-2026-10-22-eureka-c2a`).
Eyes pass, 2026-10-22. Facts and evidence pointers only; no recommendation.
Question as admitted: Eve.Surface and Brokkr build against an unpinned sibling `..\CultLib` source checkout. C2a (refuse element types with no id) lands on main. Options: pin-then-land, sweep-then-land, land-and-break.
## Sources
| # | Source | URL | Status |
|---|---|---|---|
| S1 | Abseil, About / philosophy | https://abseil.io/about/philosophy | fetched |
| S2 | Abseil, compatibility guidelines | https://abseil.io/about/compatibility | fetched |
| S3 | Software Engineering at Google, ch. 21 Dependency Management | https://abseil.io/resources/swe-book/html/ch21.html | fetched |
| S4 | Software Engineering at Google, ch. 16 Version Control and Branch Management | https://abseil.io/resources/swe-book/html/ch16.html | fetched (does not cover live at head) |
| S5 | Russ Cox, Minimal Version Selection | https://research.swtch.com/vgo-mvs | fetched |
| S6 | Cargo book, Cargo.toml vs Cargo.lock | https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | fetched |
| S7 | npm docs, package-lock.json | https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json | fetched |
| S8 | Chromium src, dependencies doc (DEPS, gclient, roll-dep) | https://chromium.googlesource.com/chromium/src/+/main/docs/dependencies.md | fetched |
| S9 | Pro Git, Submodules | https://git-scm.com/book/en/v2/Git-Tools-Submodules | fetched |
| S10 | chromium infra dependencies.md | https://chromium.googlesource.com/infra/infra/+/main/docs/dependencies.md | 404, not retrieved |
| L1 | `F:\Projects\Aetheria\Directory.Build.targets`, `Directory.Build.props`, git history | local files | read |
| L2 | `F:\Projects\Aetheria\docs\cultcache-migration-cut.md` (Q6 table, lines ~565-580; pin guard notes ~1900-1925) | local file | read |
| L3 | `F:\Projects\Eve` and `F:\Projects\Brokkr` csprojs and git file lists | local files | read |
| L4 | voidbot `search_history` (two queries), `search_sources` (one query) | MCP | queried |
Anything not in a "fetched" row above is not claimed from the web.
## 1. Established practice: who pays for a breaking upstream change, and when
- **Abseil / live at head (S1, S2).** Recommends users track the latest code. Says it will provide a tool to perform an API refactor for well-behaved code (S1). Requires of downstream: show how you call it, stay within the compatibility guidelines; otherwise fork or use periodic supported branches (about every 6 months, patched 2 years) (S1). Says every change is potentially breaking for someone; breaks are announced in commit messages and LTS notes, and users are asked to report undetected breaks on GitHub (S2). Users should be able to temporarily pin to an earlier commit if a newer change is a problem (S2). S1/S2 do not mention Rosie, the Beyonce rule, or caller-fixing; the fetched pages describe tooling and a feedback channel only.
- **Live at Head as described in SWE at Google ch. 21 (S3).** Providers are expected to test changes against downstream dependents before committing; breaking changes wait until dependents are updated or an automated migration tool exists. Consumers must keep meaningful tests. Stated preconditions: unit tests across dependencies run in CI, a visible reverse-dependency graph, substantial compute, no pinning of dependencies, provider acceptance of the downstream-testing duty. Called "costly but effective" inside Google and "largely unproven at scale." Notes that under SemVer a major bump costs maintainers little and many indirect users a lot. Ch. 16 (S4) does not address live at head; it states large-scale changes across trunk are costly and tooling-supported.
- **Go MVS (S5).** Selects the oldest version satisfying all declared minimums. Publishing a new version changes nobody's build; a consumer gets it only when a requirement names it, directly or through another module. Upgrades are explicit edits to `go.mod` by the top-level module. Compatibility rule: newer versions inside one major version are expected to work where older did; a major version is a different module identity. Cost of a breaking change falls on whoever opts into the new version, at the time they opt in.
- **Cargo (S6).** Git dependency without `rev` takes the default-branch tip at first build and fixes that commit in `Cargo.lock`; changes arrive only on `cargo update`. A `rev` in `Cargo.toml` pins explicitly; the Cargo book calls hand-managed SHAs "tedious and error prone" and prefers the lockfile. Consumer pays at `cargo update` time.
- **npm (S7).** `package-lock.json` records the exact tree; installs stay on it until the lockfile changes and is committed. It is not published with a package. The page gives no timeline for when updates reach a consumer.
- **Chromium DEPS (S8).** `DEPS` stores dependency URLs and commit hashes (often in `vars`). Developers roll with `gclient setdep`, `roll-dep`, or manual `git update-index`; some projects (Skia) are autorolled. The page says manual rolling after an upstream change is good practice to confirm the roll compiles and tests pass. It does not describe revert procedure. Cost is paid by the roller at roll time.
- **Git submodules (S9).** The superproject records one commit SHA (gitlink, mode 160000). Upstream commits never move the pin; it moves when someone updates the checkout and commits the new SHA in the superproject. Collaborators receive the pin via pull and must run `git submodule update`.
## 2. Internal precedent
- **Aetheria pin (L1).** `Directory.Build.props` holds `CultLibRevision` (currently `45c2f4006f5f7dab357426cabd3197e8d0a17f66`) and `CultMathRevision` (`9c1b06cd703bfa44352050e819b0d0659cf5978a`). `Directory.Build.targets` target `VerifyCultLibRevision` (BeforeTargets `ResolveProjectReferences`) errors if the sibling checkout's `git rev-parse HEAD` differs from the pin, or if `git status --porcelain` is non-empty; escape hatch `SkipCultLibRevisionCheck=true`. File is 39 lines; the pin is one pair of properties per pinned package.
- **Why added (L1 git).** Commit `4f4beb0f` (2026-09-14, "Pin CultLib for headless builds"): "Directory.Build.targets carries Delvehold's VerifyCultLibRevision guard, checking for CultMath.csproj: HEAD must equal the pin and the worktree must be clean." Commit `e91ba369` (2026-09-24, "Pin CultLib per package, as Unity's manifest does"): headless build must compile the revisions Unity consumes; a single pin would have compiled six commits of newer CultCache under headless tests while Unity stays on its tag. The Aetheria cut doc (L2, ~line 1905) calls the guard "the only working pin for a sibling checkout." Aetheria's `Packages/manifest.json` pins Unity packages by git tag.
- **Pin guard already in Delvehold (L4/search_sources hit, `Delvehold:Directory.Build.targets`)** and a variant in AetheriaEve (archived, `Aetheria.State.Dependencies.targets`, also checks a Unity package tree hash). Delvehold's guard checks `GameCult.Mesh.csproj` exists, HEAD equals `CultLibRevision`, tree clean. Date of Delvehold's first guard: not determined.
- **Consumer table (L2, Q6 in Aetheria `docs/cultcache-migration-cut.md`).** Aquarium, Ymir, AquaSynth, Mimir, Gjallar: ".NET sibling, unpinned", compile against main at once. Brokkr, Eve, EveUnity, EvePlugins: "Unity package / sibling", pin "various". Delvehold: sibling with revision guard at `334e60f`, 52 behind, "Not until it re-pins." Heimdall: submodule `b6b1d9c`. Rust consumers (Idunn, Epiphany, Odin, Ghostlight, Muninn, Ratatoskr, CodexConnector): Cargo by commit.
- **Operator discussion (L4).** voidbot `search_history` ("pin CultLib revision vs track main for consumers"; "Brokkr Eve sibling CultLib checkout build pin") returned no relevant operator statement on pinning versus tracking; hits were unrelated Discord chatter. Nearest operator words found in the Discord index: Metacrat, 2026-05-09, "Oh, anyone using CultLib will still be paying the blood price, you can count on that" (message 1502671320406687894; context not fetched, so its subject is not confirmed). No operator quote on pinning versus tracking was found in memory files grepped (`C:\Users\Meta\.claude\projects\F--Projects\memory`, pattern pin/track main/unpinned/sibling) beyond a retired Idunn pin line and Odin/Muninn pin-branch coordination notes.
- Aetheria cut doc operator mentions near pins are about content (Schematic-shape drawer, 2026-09-14; Studio grouping, 2026-09-17), not pinning policy.
## 3. Current state of the two consumers (L3)
- **Eve.** `packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj:16` `CultLibRoot` defaults to `$(MSBuildThisFileDirectory)..\..\..\CultLib`; lines 20-21 ProjectReference `$(CultLibRoot)\src\GameCult.Caching\...` and `...\GameCult.Mesh\...`. `tests/GameCult.Eve.Surface.Serialization.Tests/*.csproj:19` references `$(CultLibRoot)\src\GameCult.Mesh\...` (and Eve.Surface by relative path). `git ls-files` shows only `packages/org.gamecult.eve.surface/Directory.Build.props` (sets only BaseOutputPath/BaseIntermediateOutputPath/MSBuildProjectExtensionsPath, no revision). No `Directory.Build.targets`, no `global.json`, no `.gitmodules`, no revision property or check. Eve also holds Cargo.toml/Cargo.lock (Rust side, not examined).
- **Brokkr.** `brokkr-command/Brokkr.Command.csproj:7`, `brokkr-contracts.Tests/Brokkr.Contracts.Tests.csproj:7`, `brokkr-contracts/Brokkr.Contracts.csproj:8`: `CultLibRoot` defaults to `..\..\CultLib`. ProjectReferences: Caching, Caching.MessagePack, Mesh, Networking (by project). No `Directory.Build.*`, no `.gitmodules` in `git ls-files`; no revision property or check.
- Neither repo contains `CultLibRevision`, a `VerifyCultLibRevision` target, or any HEAD check. Both build whatever `F:\Projects\CultLib` currently holds (including dirty or other-branch states).
- Fork text claims (not re-verified here): Eve.Surface has 0 `[CultElementId]`; Brokkr `[CultDocument]` files show no object lists by grep.
## Gaps
- S10 (Chromium infra) 404; Chromium autoroller behavior beyond S8 not fetched.
- Rosie / Beyonce rule not found in fetched pages; not cited from memory.
- No operator statement on pin-versus-track found in voidbot or the memory grep.

View file

@ -0,0 +1,74 @@
Serves the C2a fork on finding variants:finding:cut-land-c2a-gate.s1.reflist-record-read-only-at-head (campaign variants), 2026-10-23.
# Prior art: read with an old writer schema, then write back
Facts only. Pages were fetched 2026-10-23 through a summarising fetcher; "not stated" means the fetched page did not say it, not that no source does. Items marked (memory) are my background knowledge, not verified from a fetched page.
## 1. Apache Avro
- Source: https://avro.apache.org/docs/current/specification/ (Schema Resolution; summarised via apache/avro doc source), Java `DataFileWriter` javadoc (1.11.1).
- Model: the writer schema travels with the data. The reader resolves it against its own reader schema. Resolution is a read-time projection; the spec defines no write-back.
- Data loss on projection: "the writer's value for that field is ignored" when the reader schema lacks the field. A reader-only field takes its default, or the read errors if it has none.
- Write-back: the spec states no rule. Object container files store one schema in the header ("Each datum in a file is of the same schema"). `appendTo` is documented only as "Open a writer appending to an existing file"; it does not say which schema the append uses. (memory: Java `appendTo` reuses the file's header schema, so an append never restamps.)
- Stated reason for the read-time design: readers and writers evolve separately (fields added or removed in a different software version).
## 2. Protocol Buffers (proto3)
- Source: https://protobuf.dev/programming-guides/proto3/ (Unknown Fields).
- On write: unknown fields are "included during parsing and in the serialized output" (matches proto2). Round-trip keeps fields the current type does not know.
- Loss cases the page lists: serializing to JSON, and copying a message field by field into a new message (it recommends `CopyFrom`/`MergeFrom`).
- No schema id or version is written. The wire format carries only field numbers, so "restamp" has no meaning here.
- Not stated: behaviour in versions before 3.5 (memory: proto3 dropped unknown fields before 3.5, then reversed). The Java reference page says nothing on preservation.
- Stated reason: preserve data across a mixed-version fleet.
## 3. Confluent Schema Registry
- Source: https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html
- BACKWARD (default): consumers on the new schema read data written with the new or the previous version. FORWARD: data written with the new schema is readable by consumers on the old one. FULL: both.
- Write: "Each schema version gets a unique ID and an incremented version number." A producer writes under the schema it holds (its own current version). The id is stamped per message at serialisation, from the producer's schema.
- Upgrade order is how the rules prevent loss. BACKWARD means upgrade consumers before producers. FORWARD means upgrade producers first, and keep old-schema data away from consumers until they are upgraded.
- Not stated on the page: what a consume-modify-produce app writes. (memory: it re-serialises under its own latest schema, so fields it does not model are lost unless a compatibility rule kept them.)
- The registry gates registration. It does not gate individual rewrites.
## 4. Document DBs and ORMs with stored versions
### MongoDB schema-versioning pattern
- Sources: https://www.mongodb.com/blog/post/building-with-patterns-the-schema-versioning-pattern ; https://www.mongodb.com/docs/manual/data-modeling/design-patterns/data-versioning/schema-versioning/
- Documents carry `schema_version` (absent means 1). The app keeps a handler per version and writes new documents at the current version.
- Migration strategy is the app's choice: migrate all, migrate "when accessed" (lazy), or never.
- Not stated: whether an update of an old document restamps it. The manual's update example branches on `schemaVersion` and writes into each version's own shape. It notes its `else: null` branch adds stray null fields to the other shape.
- Stated reasons: no downtime, failed migrations easier to handle, timing flexibility. Cost: handler code per live version.
### Realm (Atlas Device SDKs, now deprecated)
- Source: https://www.mongodb.com/docs/atlas/device-sdks/sdk/swift/model-data/change-an-object-model.md
- Per-file schema version. A changed schema needs an incremented `schemaVersion`, plus a `migrationBlock` for non-trivial changes. The file is migrated whole at open, before any read or write, and Realm "will throw an error" if the version is lower or the same but the schema differs.
- No lazy per-object restamp. Writes happen only on a migrated file.
- Data loss: the manual says "Deleting a property will not impact existing objects", i.e. removal is automatic and needs no developer step. Required new properties and renames need a manual block. (memory: removed-property values are dropped from the file.)
### Core Data
- Source: web search results only (Apple forum threads 745114 and 785427, WWDC22-10120, "staged migrations" doc). The Apple docs URL I tried returned 404.
- A store whose model hash differs from the current model fails to open with an incompatible-version error. Opening migrates the whole store when `NSMigratePersistentStoresAutomaticallyOption` and `NSInferMappingModelAutomaticallyOption` are set. Lightweight migration covers only obvious patterns (add entity, remove relationship, required to optional, rename). Anything else needs a staged or custom migration.
- Migration is store-wide at open. It does not happen per record on write.
### EF Core
- Not fetched. (memory: no per-row schema stamp. Migrations are explicit, ordered DDL, tracked in a `__EFMigrationsHistory` table.)
## 5. Old-schema data held read-only until explicit migration
### Elasticsearch archive indices
- Sources: https://www.elastic.co/docs/deploy-manage/upgrade/deployment-or-cluster/reading-indices-from-older-elasticsearch-versions ; https://www.elastic.co/guide/en/elasticsearch/reference/current/archive-indices.html ; 8.0 migration notes (7.0+ indices readable, 6.x or earlier must be reindexed).
- Indices from an older major are readable through search and field-caps. "They do not support the Get API or any write APIs."
- The route to a writable copy is reindex into a new index that is "fully compatible with the current Elasticsearch version". `_source` is readable, so no data is lost on reindex.
- Stated reason: the page does not say why they are read-only. It describes archive access as "slower read-only access", with limited query capability. Some legacy mappings cannot be imported (the original is kept in `_meta`), and that is the one place the page shows a lossy translation.
- Third-party (Search Guard blog, unverified against Elastic): in 9.x a plain `index.blocks.write` does not satisfy the boot check; `index.verified_read_only` is needed.
- This is the closest match to option (b) in the fetched set: an old format, readable but not writable, left until the owner reindexes it.
## Where restamp-on-write loses data (from the sources above)
- Avro: a field present in the writer schema and absent from the reader schema is dropped at read, "ignored". Any rewrite from the resolved value would omit it.
- Protobuf: unknown fields survive a binary round trip. They are lost via JSON, or when copied field by field.
- Realm: removed properties are deleted by the automatic migration.
- MongoDB: a handler that writes only the current shape can lose or relocate old-shape fields (the manual's example adds nulls).
- Confluent: loss is prevented by compatibility rules at registration time, not at each write.
## Pattern across the set
- No fetched system restamps a single record on write when the record's writer schema differs from the application's type, on an app-visible, per-record basis.
- Avro, protobuf and Confluent carry the schema per file or message.
- Realm, Core Data and Elasticsearch move or block the whole store or index at open.
- MongoDB leaves it to the app.
- Whole-store migration at open is the usual explicit step (Realm, Core Data). Elasticsearch is the one case of read-only-until-reindex.
- Not found: a system that declares per-schema-id compatibility, as in option (b). Avro schema resolution and Confluent compatibility rules are the nearest analogues. Both are read-side or registration-side declarations.

View file

@ -0,0 +1,53 @@
Serves question variants:question:store-hold-user-code (campaign variants), 2026-10-23.
# Prior-art check: user code under a lock
Facts and pointers only. Each source was fetched or searched this session; what was not reachable is marked.
## 1. Goetz et al., Java Concurrency in Practice (Addison-Wesley, 2006), ch. 10 "Avoiding Liveness Hazards"
Status: EXISTS, SECTION NUMBER CORRECTED.
- "Open Calls" is **10.1.4**, not 10.1.3. 10.1.3 is "Deadlocks Between Cooperating Objects"; 10.1.2 is "Dynamic Lock Order Deadlocks"; 10.1.5 is "Resource Deadlocks". Source: text extracted from a PDF copy of the book (its TOC lists Listing 10.6 "Using Open Calls to Avoiding Deadlock Between Cooperating Objects", p. 133). The copy is an unofficial one at library.megu.edu.ua; a publisher link was not checked.
- (a) 10.1.3 gives the warning sign: an "alien method" (defined p. 40 as a method whose behavior the calling class does not fully specify) called while holding a lock. Paraphrase: such a call may take other locks (deadlock risk) or block a long time, stalling threads that need the lock you hold. 10.1.4 defines an open call as a call made with no locks held (credited to Lea, CPJ 2.4.1.3) and says programs restricted to open calls are far easier to analyze for deadlock-freedom than ones that allow alien calls under lock.
- Cost it names: moving a call out of a synchronized block makes an atomic operation non-atomic (its getImage example stops being a snapshot). Where atomicity is needed it suggests structuring so only one thread can run the code path after the open call (service shutdown example).
- (b), (c): not addressed in this section.
- Pointer: JCIP 10.1.3 and 10.1.4, pp. ~132-134 (chapter 10 starts p. 128).
## 2. Sutter, "Effective Concurrency: Avoid Calling Unknown Code While Inside a Critical Section"
Status: VERIFIED, with date caveat.
- Dr. Dobb's column in the "Effective Concurrency" series. Sutter's blog teaser is dated 2007-11-06: https://herbsutter.com/2007/11/06/effective-concurrency-avoid-calling-unknown-code-while-inside-a-critical-section/ . Original link ddj.com/architect/202802983; Dr. Dobb's mirror https://drdobbs.com/cpp/avoid-calling-unknown-code-while-inside/202802983 (4 pages). The Dr. Dobb's issue date was not shown in anything fetched; the blog date is the only date confirmed. www.drdobbs.com did not resolve from this machine, so the column body comes from search-result summaries and the blog teaser, not a full first-hand read.
- (a) Core claim: locks do not compose; two separately correct modules can deadlock when combined. Trigger: caller inside a critical section, callee enters another critical section or blocks, another thread can take the same two in the opposite order. "Unknown code" listed as: library functions, plug-ins, callbacks/function pointers, virtual methods, generic types.
- Mitigations listed: move the call outside the critical section; pass copies instead of shared data; narrow the critical section (finer locks, reader-writer); require callees not to block (documentation/discipline only).
- (b), (c): not addressed.
## 3. Linux lockdep
Status: VERIFIED, with a nuance.
- Source: kernel docs "Runtime locking correctness validator", https://docs.kernel.org/locking/lockdep-design.html .
- (b) Lockdep tracks lock classes, not instances, and records dependencies between classes as sequences are observed. It reports a circular order as a potential deadlock even if the deadlock never happened. Its "Proof of 100% correctness" section says that once each simple single-task locking sequence has occurred at least once, no timing of them can deadlock. Each new lock chain is validated once and hashed; repeats skip validation.
- Nuance: it flags the dependency edge that closes a cycle, so both orders must have been exercised at some point (possibly by different, non-concurrent runs). A lone nesting is flagged only if it breaks a single-lock rule (IRQ-safe vs unsafe, same-class recursion). "Refuses on first sight" fits the closing edge or a single-lock rule violation only. It warns; it does not block the acquisition.
## 4. ConcurrentHashMap re-check after locking
Status: VERIFIED.
- Source: OpenJDK java/util/concurrent/ConcurrentHashMap.java, `putVal` and `computeIfAbsent`: head node `f` is read unlocked, `synchronized (f)` is taken, then `tabAt(tab, i) == f` is re-checked; on failure nothing is done and the loop retries. The overview comment gives the reason: the locked node can be replaced, so first-in-bin must be confirmed after locking. (Only the first 100k of 269k chars were read; both methods were reported found there.) Empty-bin `computeIfAbsent` uses a ReservationNode plus CAS instead.
- (c) This is the pattern: choose the lock from an unlocked read, validate the read after acquiring.
## 5. LMDB
Status: VERIFIED (paraphrase). Not about alien code or lock order.
- Source: lmdb.h, `mdb_txn_begin` and `mdb_env_open` (https://github.com/LMDB/lmdb, libraries/liblmdb/lmdb.h). A thread may hold one transaction at a time (plus children); a transaction and its cursors belong to one thread; one write transaction at a time, so writers cannot deadlock each other; MDB_NOTLS ties reader slots to the txn object instead of the thread, and the application must then serialize writers itself because write locking is unaware of user threads.
- Relevance: "one hold per thread" as a deliberate rule, parallel to CultCache's one-cache-gate-per-thread.
## 6. SQLite btmutex.c
Status: VERIFIED (summary of trunk source).
- Source: https://github.com/sqlite/sqlite/blob/master/src/btmutex.c (read via raw.githubusercontent.com; sqlite.org served a bot check).
- (b) Prevention by ordering, not detection: sharable B-trees on a connection sit in a list sorted by BtShared address and are always locked ascending (`sqlite3BtreeEnterAll`). `sqlite3BtreeEnter` try-locks first; on failure it releases later-ordered locks, waits, then reacquires in order. `wantToLock` is a count that makes a non-recursive mutex behave recursively.
- No alien-code or callback discussion in these comments.
## Counter-positions and how they stay safe
- Callbacks under lock, documented: Java `ConcurrentHashMap.compute`/`computeIfAbsent`. Javadoc (Java 21): the function should be short and simple because other threads' updates may block meanwhile, and must not modify the map. The javadoc does not say "internal lock"; that it runs under the bin lock is inferred from the source in item 4. Safety rests on documented restrictions, not enforcement.
- Opposite choice: .NET `ConcurrentDictionary.GetOrAdd(key, valueFactory)`. Writes use fine-grained locks but valueFactory runs outside them, "to avoid the problems that can arise from executing unknown code under a lock" (doc remarks). Price: not atomic; the factory may run more than once; a recheck decides the winner (another thread's insert wins). https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd . Same shape as option `encode-first` (user code outside the lock, then recheck and commit) and as item 4.
- JCIP and Sutter both name the cost of leaving the lock: lost atomicity. Sutter also offers "pass copies" and "callee must not block" as alternatives to leaving.
- Not researched (no source fetched): Linux notifier chains, Java `synchronized` event listeners, other systems that call back under lock by design. No verified citation for them.
## Unverified / not done
- JCIP: section numbers and text come from one PDF copy, not the publisher edition.
- Sutter: full column body not read first-hand.
- No Discord or voidbot content read.

View file

@ -0,0 +1,90 @@
# Prior art: a process-global default registry deciding write ownership
Serves `variants:question:registryless-write-reads-shared` (campaign `variants`). Eyes pass, session `self-2026-10-27b-eureka`, 2026-10-27. Facts only; items marked "summary" rest on search summaries not opened.
# Prior art: process-global type registries and write/overwrite ownership
Fork: `variants:question:registryless-write-reads-shared` (viewed in full via eureka-state, status InForce, options `shared-decides` / `written-type-only`, recommended `shared-decides` at ~65%).
Facts only. No conclusion. Date 2026-10-27.
Method note: web pages were read through WebFetch (a small model summarises the page) and WebSearch summaries. Quotes below are what those tools returned; where only a summary was available it is marked "summary". Nothing was run.
## A. Local mechanics (CultLib, read-only, `C:\cl-wt\c2a-land\src\GameCult.Caching\CultCache.cs`)
- `CultDocumentRegistry.Shared` is `Lazy<CultDocumentRegistry>` (line 639-645). Its public constructor calls `Refresh()` (647-650), which scans `ReflectionExtensions.GetAttributedDocumentTypes()` (688; defined in `ReflectionExtensions.cs:10`) and rebuilds the indexes under `_registrationGate`. So Shared's initial content is whatever `[CultDocument]` types are in loaded assemblies at first touch of `Shared`.
- `GetRequired(Type)` (697-711) registers an unregistered `[CultDocument]` type on demand. So Shared also grows whenever any caller asks for a type. `Refresh()` is public and rebuilds the index from currently loaded assemblies.
- `ForTypes(IEnumerable<Type>)` (658-669) builds a registry with `discoverLoadedDocuments:false`, containing only the given types (plus those `GetRequired` pulls in).
- `CultCache(registry = null, ...)` sets `_registry = registry ?? CultDocumentRegistry.Shared` (2202-2205).
- `WriteDocument<TDocument>(key, doc, storedAt, registry = null)` (4515-4545): `resolvedRegistry = registry ?? Shared`; `catalogEntry = resolvedRegistry.GetRequired<TDocument>().ToCatalogEntry()`; then `Cacheless(resolvedRegistry, ...)` lends that registry to the store for the call. XML doc (4510) says Shared "can only refuse more".
- `WriteRecord(..., registry = null)` (4491-4497): XML doc (4489) says that with registry omitted "it decides nothing for copied records and the one entry is the only claimant".
- `OwnsOnDisk(upserts)` (4741-4759): when the store is cacheless and upserts are given, ownership of an on-disk record is decided by `CultDocumentRegistry.Owns(id, catalog, declared, new CatalogEntryLookup(writes, AttachedRegistry), ..., claimant => writes.Any(entry => entry.SchemaId == claimant.SchemaId))`. The comment (4731-4735, 4738-4740) says written entries shadow the lent registry's type for their ids, the registry adds other claimants, and the record is owned only when the claimant is a written entry, "a registry can only refuse more". `CatalogEntryLookup` ctor at 604 takes `(entries, CultDocumentRegistry? others)`.
- Net: with `registry == null` the lent registry's claimant set is Shared's current contents, which depend on assemblies loaded and on every prior `GetRequired` call in the process.
## B. Q1. Process-global, lazily filled registries in established libraries
### MongoDB C# driver, `BsonClassMap`
- Global: yes. `BsonClassMap.RegisterClassMap` is process-wide static; unregistered types are auto-mapped on first use. Docs: "You must register a class map _before_ it's needed in your code." and "We recommend registering class maps before initializing a connection with MongoDB." https://www.mongodb.com/docs/drivers/csharp/current/serialization/class-mapping/ (WebFetch; the page itself says nothing on freezing or late-registration errors).
- Behaviour depends on what has been touched: EF Core provider issue EF-88 reports "Unknown discriminator value 'List`1'" when the first operation after startup is a read; reporter links it to a class map that is created only on write. Status Closed/Fixed in 8.0.0; page has no maintainer comment. https://jira.mongodb.org/browse/EF-88 (WebFetch). Reporter says explicit `RegisterClassMap` at startup did not fix it for them.
- A MongoDB community answer (via search summary, not opened): discriminator mappings persist once registered, so a read after an insert works; fix offered is `[BsonKnownTypes]`. https://mongodb.com/community/forums/t/c-driver-why-is-bsonknowntypes-attribute-needed-for-polymorphism/2183 (summary only).
- Register-once contract: CSHARP-4424 states the three `RegisterClassMap` methods are "meant to only be called once for a given type"; requested because several libraries share data models and do not control init order. Closed Done, fix version 2.19.0; the page does not say which API shipped. https://jira.mongodb.org/browse/CSHARP-4424 (WebFetch). Community workaround named in search summary: `BsonClassMap.IsClassMapRegistered`.
- CSHARP-1879 (search summary): `RegisterClassMap` accepted an unfrozen map and failed later on use; fixed 2.4.2. https://jira.mongodb.com/browse/CSHARP-1879 (not opened).
- CSHARP-5597 (search summary): `BsonSerializer.RegisterDiscriminator` has no effect; discriminator dictionary populated at class-map registration. https://jira.mongodb.org/browse/CSHARP-5597 (not opened).
- Polymorphic docs: "You must specify hierarchical discriminators in root-to-leaf order." and the relationship of all classes in a hierarchy must be documented before deserialization. https://www.mongodb.com/docs/drivers/csharp/current/serialization/polymorphic-objects/ (WebFetch).
- Particular docs report an order-dependent GuidSerializer failure ("incorrect order of class mappings"). https://docs.particular.net/persistence/upgrades/mongodb-4to5 (search summary, not opened).
- Did they move away: not found. Class maps remain global static; mitigation is documentation plus the IfNotRegistered API.
### protobuf-net `RuntimeTypeModel.Default`
- Global: yes. README: "The Serializer.* methods are basically just shortcuts to RuntimeTypeModel.Default.*". https://cdn.jsdelivr.net/gh/protobuf-net/protobuf-net@main/README.md (WebFetch).
- Lazy fill, freezing, order-dependent bugs: NOT verified. README is silent; a direct docs fetch (docs.protobuf-net.dev/runtime) was redirected and not followed. No issue found.
### MessagePack-CSharp
- Global mutable default exists: `MessagePackSerializer.DefaultOptions`. README (WebFetch): labelled "Global state; Not recommended when writing library code." Safe if you control all MessagePack code; libraries should pass options explicitly because others "may change this static property." Libraries "must not use StaticCompositeResolver - as it is global state". https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/master/README.md
- Resolvers are an ordered list (`CompositeResolver`): order matters by construction.
- Whether it moved away from the global: not found.
### System.Text.Json
- `JsonSerializerOptions.Default`: "Gets a read-only, singleton instance"; remarks give a performance reason: shared instance avoids "redundant recomputation of converters". https://learn.microsoft.com/dotnet/api/system.text.json.jsonserializeroptions.default (WebFetch). The page states no rationale about cross-component interference; any such reading is inference. Design-discussion issue in dotnet/runtime: searched, not found. Whether Default was ever mutable: not verified.
- Type-info resolvers: not researched beyond the above.
### Json.NET
- `TypeNameHandling` is a security concern, not an order concern: CA2326 says "An attacker could modify the serialized data to include unexpected types"; mitigation is a custom `ISerializationBinder` restricting types. https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2326 (WebFetch).
- `JsonConvert.DefaultSettings` global behaviour/guidance: the Newtonsoft page fetched says nothing about global effects. Not verified.
### Orleans
- Serializer discovery is build-time source generation (`[GenerateSerializer]`, `[Id]`), not a runtime scan; the docs' older code-generation section lists scan of assemblies referencing the core library plus `KnownType`/`KnownAssembly` attributes. Aliases: "Type aliases are globally scoped, and you cannot have two aliases with the same value in an application." https://learn.microsoft.com/en-us/dotnet/orleans/host/configuration-guide/serialization (saved locally: `C:\Users\Meta\.claude\projects\F--Projects-CultLib\0840c7c7-0e31-410e-9012-afb79c3c0742\tool-results\toolu_0148m7no1AbrJzWmNLicn6N9.txt`, lines 134, 839-841).
- Type manifest / codec registry runtime behaviour, issues: not researched.
### EF Core
- Model is built per `DbContext` type and cached (IMemoryCache; "the built model has a cache size of 100"); initialised at first operation: "just creating a DbContext instance does not cause the EF model to be initialized." https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics
- Assembly scan: `ApplyConfigurationsFromAssembly` — "The order in which the configurations will be applied is undefined, therefore this method should only be used when the order doesn't matter." https://learn.microsoft.com/en-us/ef/core/modeling/ ; and "`EntityTypeConfigurationAttribute` types will not be automatically discovered in an assembly."
- So EF's registry is per context type, not process-global; assembly scan is opt-in and documented as order-undefined.
### Marten
- Storage is a table and upsert function per document type, alias `mt_doc_<alias>`; alias collisions are resolved with `DocumentAlias`. https://martendb.io/schema/storage (WebFetch). Hierarchies share one table via `AddSubClass<T>()`; subclass docs are "stored as the parent type" in the identity map. https://www.martendb.io/documents/hierarchies (WebFetch). Schema is configured in `StoreOptions` per store, not a process-global.
### Java ServiceLoader
- "Service providers are loaded and instantiated lazily, that is, on demand." Providers on the class path load in the order `getResources` finds config files; "The ordering of modules in a module layer is not defined." "Instances of this class are not safe for use by multiple concurrent threads." Caches cleared by `reload`. https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ServiceLoader.html (WebFetch). Visibility is class-loader dependent ("must be visible to the class loader").
## C. Q2. Write/overwrite decisions against an existing record of another type
- RavenDB: REST put docs, request body section: updates must "include its collection name in the metadata" and "A document's collection cannot be modified." Server error code for mismatch: the first fetch summary said HTTP 500; not confirmed on a second fetch. https://docs.ravendb.net/7.1/client-api/rest-api/document-commands/put-documents (WebFetch twice). Ayende, RavenDB 4.0 storage: document key namespace is global, same key cannot exist in two collections (summary). https://ayende.com/blog/173954/the-design-of-ravendb-4-0-physically-segregating-collections The check is against the collection named in the write (derived from the writer's type by the client), not a registry; the "client maps entity type to collection" part is inference from the search summary, not a quoted source. `PutDocumentCommand` page: a missing collection means `@empty`; concurrency via `changeVector`, null "to skip the check". https://docs.ravendb.net/client-api/commands/documents/put
- Marten: per-type tables, so the same id under two types are two rows in two tables; no cross-type check. Same id across subclasses of one hierarchy table: not covered by docs fetched; unverified. Marten docs: upsert is used so users "don't have to check whether a document is new or existing" (search summary of https://www.martendb.io/documents/storing, not opened).
- MongoDB: the server has no type; `_id` unique per collection; the C# driver writes a `_t` discriminator from the written object's class map. Whether the driver checks an existing document's `_t` on replace: not researched; nothing found stating it does.
- Couchbase: key unique within a collection; upsert replaces; no server-side type field enforcement; guidance is one type per collection. https://docs.couchbase.com/server/7.0/learn/data/scopes-and-collections.html (search summary, not opened).
- Orleans: grain identity is type plus key ("type/key"), so same key under two grain classes is two identities, not a conflict. https://learn.microsoft.com/en-ca/%20dotnet/orleans/grains/grain-identity (search summary, not opened). How storage providers key state: not found.
- Result of this pass: no store found in which a process-global type registry decides whether a write may replace an existing record of another type. Found: collection/type fixed by metadata of the writer (RavenDB), type as part of the key (Orleans, Marten tables), or no check (Couchbase upsert). This is a negative search result, not proof of absence. Not searched: Cosmos DB, DynamoDB, EF Core TPH update of an existing row with another discriminator, Realm, LiteDB, Akka.Persistence.
## D. Q3. Guidance on ambient/global registries and determinism
- Mark Seemann (ploeh, 2019): after discussion with co-author van Deursen, "Ambient Context really is an anti-pattern." Reasons given: he has never used it and superior alternatives exist; he says the claim is unproven. https://blog.ploeh.dk/2019/01/21/some-thoughts-on-anti-patterns/ (WebFetch). Examples he cites as found in the BCL (search summary, not verified on page): `Thread.CurrentPrincipal`, `CultureInfo.CurrentCulture`, `HttpContext.Current`. DI book 2nd ed. chapter 5 lists it among DI anti-patterns: https://www.oreilly.com/library/view/dependency-injection-principles/9781617294730/OEBPS/Text/c05.xhtml (not opened).
- MessagePack README (above) is itself a library-author statement on global mutable defaults. MongoDB CSHARP-4424 (above) states the shared-library init-order problem. EF Core docs (above) state assembly-scan order is undefined.
- No .NET guidance specifically about AppDomain-wide/assembly-scan registries and write decisions found.
## E. Not verified / gaps
- protobuf-net lazy registration, freezing and bug history.
- Why System.Text.Json made Default immutable; no maintainer statement found.
- Json.NET DefaultSettings guidance.
- RavenDB exact error text and HTTP status; whether the stored document is unchanged after refusal.
- Marten same-id-across-subclasses behaviour.
- MongoDB driver behaviour on replacing a document whose `_t` differs.
- Several items above rest on WebSearch summaries I did not open (marked "summary").

View file

@ -0,0 +1,90 @@
# Prior art: structural versus name-inclusive schema ids
Serves the store-identity question raised 2026-10-27 in campaign `variants` (operator: member and declared names vary with runtime naming conventions). Eyes pass, session `self-2026-10-27b-eureka`. Facts only; items marked "summary" rest on search summaries not opened.
# Prior art: structural vs name-inclusive schema identity (campaign variants)
Eyes pass, 2026-10-27. Facts and evidence pointers only. "(fetched)" = page opened via WebFetch (the answer is a small-model extraction of the page, not raw text). "(summary)" = WebSearch result summary, page not opened.
## 0. Local facts (read-only)
- `F:\Projects\CultLib\src\GameCult.Caching\CultCache.cs:717-718` schemaId = Sha256(BuildSemanticFingerprint(...)). `:735-761` fingerprint = `schemaName|schemaVersion` then per member ordered by Slot: `slot:MemberName:TypeName:ref|value:TargetSchemaName:one|many`. `contentHash` (`:716`) is a separate Sha256 of canonical JSON (`:763+`).
- `TypeName` = `CultSchemaTypeNames.FromType`: CLR `Type.FullName` (namespace-qualified), generics as `Name<args>`, arrays `T[]` (`src\GameCult.Caching\CultSchemaTypeNames.cs:9-31`). So the fingerprint contains .NET type names as well as member names.
- Contract `Contracts\cultcache-persistence-format.md:257-284` lists what canonicalization must preserve: slots, types, required/default, nullability, collection semantics, enum constraints, encoding shape. Member names and declared schema name are not in that list. `:171-179` says the id is not derived from C#/Rust/TS type names and the same shape should give the same id "regardless of origin language". `:180-190` defines schemaId = "same semantic persistence shape?" vs contentHash = "exact same canonical schema document?". Example catalog entry (`:140-170`) carries optional `supersedesSchemaIds`, `migrationFamily`; its example canonicalSchema fields do carry `"name": "Name"` (PascalCase). Soft-migration reader flow `:286-345` (exact_match / soft_migrate / reject).
- Runtimes do not derive ids; they emulate C# names:
- Python: `packages\cultcache-py\src\cultcache_py\documents.py:326` `member_name=_pascal_case(field.name)`; `:516-517` splits on `_` and capitalises. `schema_id` defaults to `self.type` (`:218`); `compatible_schema_ids` field (`:188,242`).
- TypeScript: `packages\cultcache-ts\src\codec.ts:226-232` `pascalCase` (splits on non-alphanumerics), `:267` `memberName: pascalCase(field.name)`; throws if a codec has no C# type name (`:~262`).
- Rust: `packages\cultcache-rs\src\lib.rs:295,308` `serde(rename_all = "camelCase")` on envelope structs; `schema_id: Some(T::TYPE.to_string())` at `:2856`. I did not trace how Rust document types obtain a sha256 id.
- Python test pins a literal id from shared vectors: `packages\cultcache-py\tests\test_cultcache.py:808` (`tests\vectors\document-variants-c0`).
- C# also has `CompatibleSchemaIds` (`CultCache.cs:28,313,570`): a record whose persisted id is in the list is accepted (an alias-list mechanism; I saw only the grep lines, not the logic).
- Ruling `variants:ruling:wire-id-is-version` (eureka-state, InForce, ruled 2026-09-30, operator quote "Agreed on schema identity"): wire schema id = declared version string `<name>.vN`; every runtime stamps/resolves it exactly; a wire binding without `.v<digits>` is refused; "Store ids are unchanged." Cited source: document-variants-cut.md:552-555 (not opened).
## 1. How established formats define identity/compatibility, and whether names participate
Protocol Buffers (fetched https://protobuf.dev/programming-guides/proto3/ and /json/)
- Field number identifies the field in the binary wire; changing it = delete + add. Field names appear only in TextProto/JSON encodings; reserve deleted numbers and, for JSON/text, names. The proto3 page has no explicit safe/unsafe entry for renaming; names matter only for JSON/text.
- JSON mapping: names become lowerCamelCase unless `json_name` is set; "Parsers accept both the lowerCamelCase name" and the original proto name (/programming-guides/json/).
- Message type identity in `Any` is by name: default type URL `type.googleapis.com/<package>.<message>`, "a globally unique identifier" (proto3 page, #any). Type identity is nominal; compatibility is by field numbers.
Apache Avro (fetched https://avro.apache.org/docs/1.11.1/specification/)
- Parsing Canonical Form (PCF): STRIP keeps only attributes relevant to parsing (type, name, fields, symbols, items, values, size); doc and aliases dropped; FULLNAMES replaces short names with fullnames and drops namespace attributes. Textually equal PCF means "the same" for any reader. Names ARE in the canonical form (record and field names), as is field declaration order. Defaults are absent from the kept list (extractor's inference; spec text not confirmed).
- Fingerprints: CRC-64-AVRO in single-object encoding (marker C3 01 + 8-byte LE fingerprint); spec recommends at least a 64-bit Rabin, with SHA-256 / MD5 options for larger sets.
- Name rules: `[A-Za-z_][A-Za-z0-9_]*`, dot-separated namespaces, equality on case-sensitive fullname.
- Resolution: records match on same unqualified name; "Named types and fields may have aliases"; implementations "may optionally" use aliases to map writer to reader. So in Avro names participate in identity (fingerprint) and resolution; aliases are the rename mechanism and are excluded from the fingerprint.
Cap'n Proto (fetched https://capnproto.org/language.html)
- Files and types carry 64-bit IDs; `capnp id` generates them randomly. Default ID for a declaration without one = first 8 bytes of MD5 of (parent scope ID, declaration name). "You cannot change the name of a type that doesn't have an explicit ID"; symbolic names can change if ID/ordinals stay. Stated rationale for IDs: short, unambiguous, full names waste wire space. The page does not say why structural hashes were rejected; I found no Varda statement (search pointed at a mailing-list thread "Schema Refactoring and Unique IDs", Aug 2023, unopened).
- Encoding page (https://capnproto.org/encoding.html, fetched): adding fields is backward compatible because field positions depend only on lower-numbered fields; nothing on type identity.
FlatBuffers (fetched https://flatbuffers.dev/evolution/ and /schema/)
- Field positions by declaration order or explicit `id: n` (contiguous from 0, on all fields if used). Renaming "generally OK" because names are not in the binary. `file_identifier` is exactly 4 chars at bytes 4-7; docs frame it as a file-level check and recommend unions for distinguishing messages.
Thrift (fetched https://raw.githubusercontent.com/apache/thrift/master/doc/specs/thrift-binary-protocol.md)
- Binary protocol: field header = type + field id; "the field name is not encoded". (IDL page gave nothing on omitted ids.)
MessagePack-CSharp (fetched https://github.com/MessagePack-CSharp/MessagePack-CSharp, first 100k of 105k chars)
- `[Key(int)]` on all members => array encoding; string keys => map. Keys should be sequential from 0 (gaps cost nulls); do not reuse indexes of removed members; keep obsolete members until clients update. Union keys are ints, unique, serialized as [key, object]. No type identity in the payload.
Microsoft Bond (fetched https://microsoft.github.io/bond/manual/bond_cpp.html)
- Changing field ordinals breaks wire compat; no explicit versioning; Simple JSON matches by field name so a rename breaks it. No schema hash/GUID documented there. Bebop: wiki page empty (points to docs.bebop.sh, not opened).
Unison (fetched https://www.unison-lang.org/docs/the-big-idea/ and /docs/language-reference/unique-types; hash size from search summary)
- Definitions identified by hash of syntax tree; names are "separately stored metadata"; hash excludes names (512-bit SHA3 per search summary, unopened).
- Structural types: "Unison does not uniquely identify a type by its name"; same-shape types are the same type (Suit/Direction, four nullary constructors, interchangeable). `unique` types: compiler generates a UUID that feeds the hash, so identity becomes nominal. Which is the default keyword was not confirmed.
IPLD (fetched https://ipld.io/docs/schemas/)
- Matching is structural: "the names of types aren't expected to be found in the serialized data"; match is by structural outline. The page does not say schemas are themselves content-addressed. Versioning is left to developers; "Migrations" subpage not opened.
## 2. Failure modes of purely structural identity
- Unison documents it directly: structurally identical enums are one type unless declared `unique` (unison-lang.org/docs/language-reference/unique-types, fetched). Its mitigation is a per-declaration UUID mixed into the hash (identity re-nominalised).
- Language literature (summary, pages not opened): structural typing is prone to accidental conformance/type equivalence; nominal typing prevents it at the cost of rigidity (Wikipedia "Nominal type system"; arXiv 1007.2123 lecture notes; Erlang nominals doc https://www.erlang.org/doc/system/nominals.html says nominal types exist to prevent misuse of same-structure types). TypeScript "brand" = phantom discriminator field. No serialization-specific source found.
- Formats that avoid the collision by carrying a name or tag outside the shape: protobuf Any type URL (full name), Avro fullname in PCF, Cap'n Proto explicit IDs, Unison `unique`, MessagePack-CSharp union key ints, FlatBuffers file_identifier/unions.
- Not found: a documented incident of two same-shape serialized types colliding under a structural id (one search, nothing).
## 3. Failure modes of name-inclusive identity across languages
- Protobuf: default camelCase conversion is "always valid but not necessarily unique"; proto3 compiler rejects JSON-name conflicts; serialized duplicate keys parse differently by runtime (Edition Zero design doc, summary: https://flutter.googlesource.com/third_party/protobuf/+/HEAD/docs/design/editions/edition-zero-json-handling.md). Snake_case vs camelCase JSON mismatches in practice: https://github.com/fullstorydev/grpcurl/issues/105 (summary); https://github.com/open-telemetry/opentelemetry-proto/issues/476 (summary: spec contradicted itself on camelCase vs original names). Protobuf's mitigation: parsers accept both forms.
- Serde (fetched https://serde.rs/container-attrs.html): `rename_all` accepts lowercase, UPPERCASE, PascalCase, camelCase, snake_case; per-field `rename`.
- Avro restricts names to `[A-Za-z_][A-Za-z0-9_]*` with case-sensitive fullname equality, so snake_case and PascalCase spellings are distinct names; renames go through aliases, which PCF drops.
- Cap'n Proto: auto-derived IDs depend on the declaration name, so a rename without an explicit ID changes the ID.
- Bond Simple JSON: rename breaks compatibility (name matching).
- Local: Python/TS already convert to PascalCase to reproduce C#'s fingerprint (section 0); the fingerprint's TypeName is the CLR full name (`CultSchemaTypeNames.cs:30`) and TS throws when a codec lacks a C# type name.
- Registry normalisation (summary): Confluent Schema Registry `normalize.schemas=true` maps semantically-equal schemas to one id (https://www.confluent.io/blog/best-practices-for-confluent-schema-registry/); sources disagree on whether JSON Schema property order is normalised.
## 4. Migration precedents for changing identity derivation
- Git SHA-1 -> SHA-256 (fetched https://git-scm.com/docs/hash-function-transition): per-repo format marker (`extensions.objectFormat`, `compatObjectFormat`, `repositoryFormatVersion=1` makes old git refuse); bidirectional SHA-256<->SHA-1 mapping (pack idx v3, `loose-object-idx`); four transition modes (dark launch, early, late, post); conversion at the network boundary. It is a design doc, not a statement of what has shipped.
- IPFS CIDv0 -> CIDv1 (fetched https://docs.ipfs.tech/concepts/content-addressing/): v0 (46 chars, "Qm") and v1 (self-describing version/codec/multihash) coexist; `ipfs cid format -v 1` converts; v1->v0 only for dag-pb.
- Docker manifest schema 1 -> 2 (summary, mirror pages unopened): digests differ per manifest format; pull-by-digest gets no translation, pull-by-tag may be translated; repush converts. https://pulp.plan.io/issues/2099.
- Warpcast API hash v1 -> v2 (summary, https://warpcast.notion.site/Warpcast-API-Hash-Migration-Public-ccdd6de8ac604783b9596cfd623ddbac): two-week dual-hash period, then only v2.
- Cap'n Proto explicit-ID escape hatch for renamed/moved declarations; Avro aliases as the rename bridge in resolution; MessagePack-CSharp keep-obsolete-members (all fetched, above).
- Local: contract `supersedesSchemaIds` / `migrationFamily` (`cultcache-persistence-format.md:~140-170`) and C#/Py `CompatibleSchemaIds` exist as alias lists.
- Not found: Unison changing its hash derivation (one search, no docs); any serialization store that switched from name-inclusive to structural ids.
## Could not verify / not done
- Cap'n Proto designer's rationale against structural hashing (not in docs; mailing list unopened).
- Avro: whether PCF drops defaults (spec text) and any JIRA on it; spec page was summarised by the fetch model.
- Bebop, Thrift IDL id-omission rule, Unison default struct/unique keyword, IPLD Migrations page, Edition Zero doc (summary only), Confluent blog (summary only), nominal/structural papers (titles only).
- Rust runtime's sha256 derivation path; `document-variants-cut.md:552-555` not opened; `CompatibleSchemaIds` logic not read.
- eureka-state worked (whoami ok); only the one ruling was queried.

View file

@ -0,0 +1,28 @@
# Prior art: presence provider as a library, launcher as a consumer
Served: `cultlib-gaps:cut_spec:cut-cultnet-rs-presence-provider-a.r1` and the
Defaulted rulings `presence-provider-network-free-core`,
`presence-credentials-both-idunn-drivers` and `presence-provider-consumers-adopt`,
applying the operator's `gamecult-chat:ruling:presence-one-owner-library-launcher-consumer`.
Imagination pass, 2026-10-29, from recall of the cited manuals (not re-fetched).
- libsystemd `sd_notify(3)`: a service links a small library and reports its
own state (READY, STATUS, WATCHDOG); programs that cannot link it use
`systemd-notify(1)` or a wrapper. Same split as cultnet-rs provider plus
idunn-launch as the sidecar consumer.
- `sd_listen_fds_with_names(3)`: checks LISTEN_PID against the process, sets
close-on-exec on passed descriptors, finds them by name, and can unset the
LISTEN_* environment. This set is the descriptor rule the provider adopts.
- systemd watchdog (`WATCHDOG=1` within `WatchdogSec`): the model for the
warming heartbeat.
- Consul: TTL checks updated by the service through the client API, versus
script/HTTP checks the agent runs for services that cannot. In-process
versus sidecar again.
- Kubernetes: kubelet-run probes observe from outside; readiness gates are
reported by the app. Idunn's route challenge is the outside observer, the
signed presence the app's own report.
Existing copies compared: CodexConnector `src/idunn_health.rs` (two
descriptors, strict PID check), Idunn launcher (two descriptors, no PID check),
cultnet-ts `idunn-runtime-authority.ts` (descriptors by name, systemd private
PID case). The provider takes the strictest union.

View file

@ -0,0 +1,286 @@
# Analytic noise integration along warped rays: feasibility probe
Campaign `cultmath-tapes`, Imagination probe, 2026-10-03. This is a measurement,
not a spec. The tape target stays parked under ruling
`cultmath-tapes:ruling:tape-target-unparks-when-asura-stable`.
**Verdict.** Exact integration of the noise does not beat sampling for optical
depth on either shipped field.
- The exact line integral of CultMath `snoise` is cheap and correct. Affine warp
spans are long, at 100 to 300 world units.
- The exact noise integral is not the optical depth, because both densities are
nonlinear in the noise.
- The void's wall profile is `pow(·, 0.40)` with two clamps.
- Aetheria's fog is clamped at zero and goes opaque within a few world units.
- Given the *exact* span mean of the noise for free, the resulting transmittance
is still 3.6x worse than the shipped march on the void and 22x worse on the
wells.
- Exact integration wins only on the linear functional `∫ n dt` of octaves the
march undersamples. That gain does not survive the density's nonlinearity.
- Aetheria's triangle noise is integrable exactly as piecewise linear, but costs
3x to 22x the march, and its shipped form does not meet the preconditions.
## Scenarios and method
- **Void:** the shipped void (`NoiseBoundTests.VoidField.Shipped()` at
`hands/cultmath-interval-ops` 2c8585bd).
- Rh 198, S 50, Rc 248, K 1/30, A 20, F0 0.01 (cells of 100 and 25 world), warp
D 10.
- The shipped orbiting camera, N = 8 tiles at f = 935, one jittered pixel ray per
tile.
- **Wells:** the test's Aetheria wells at warp D = 60 (`FogField.Wells(60)`).
- F0 1/414.2167, A 20, B 10, extinction 0.5, flow frequency 1/512, period 8.
- Like the test, this uses the snoise field shape (r1's), not Aetheria's own
triangle noise. Triangle noise is measured separately below, on the same rays.
- **Rays:** 200 per scenario, and 120 for the triangle basis.
- The wells give 147 rays with an active interval; the rest look up and miss
the fog.
- The active interval is where the envelope leaves the density to the noise.
It is cut where the true optical depth reaches 6.
- Active length per ray: void median 140 world (p10 114, p90 193); wells median
268 (p10 137, p90 623).
- **March steps:** the void's march step (LodStep, 12.5 in z) is a median 14.9
world along the ray. The wells' quadratic grid gives a median of 9.1 in the
active region.
- **Warp:** `w(p) = flow(p) · shift`, so the affine error scales exactly with
`|shift|`.
- Spans are grown at the worst phase shift, Period / 2. A span is therefore
valid at every frame's phase, for any camera that crosses the same world
segment.
- Each span is anchored at p0 with J·d from a central difference. It grows in
1-unit (void) or 2-unit (wells) steps until `|w − (w0 + J·d·Δ)| > τ`.
- **Exact simplex integral:** CultMath `snoise(float3)` has kernel radius² 0.5,
so its support never leaves the incident simplices.
- Along a line it is therefore a sum over the lattice points within √0.5 of the
line. Each point contributes `∫ (0.5 − |x|²)⁴ (g·x) dt` on its chord, a
closed-form degree-9 polynomial.
- Validation:
- The lattice sum matches `snoise` within 2e-4 (float32 rounding at
coordinates near 300).
- The segment integral matches a 5e-4-step quadrature within 2.5e-5 per
unit length.
- **Run:** probe code was in a scratch clone (probe-clone@91c8cbf1, never merged)
and ran on Yggdrasil through the verify stopgap, in a dotnet 10 container with
4 CPUs. Timings are single-threaded C# on that host and are relative.
## 1. Affine-valid span length (ray length, world units)
| τ (× coverage cell) | Void p10 / median / p90 | Wells p10 / median / p90 |
|---|---|---|
| 0.05 | 94 / 129 / 211 | 71 / 103 / 169 |
| 0.10 | 141 / 197 / 324 | 108 / 151 / 253 |
| 0.25 | 263 / 420 / 1244 | 184 / 275 / 715 |
| 0.05 × detail cell | 44 / 63 / 97 | 34 / 47 / 75 |
- One affine span at τ = 0.1 covers about 13 void march steps and about 16 wells
march steps.
- The void shell is shorter than one span. The tiled spans are clipped by the
active interval, at a median of 27 world and 2 spans per ray.
- On the wells, the tiled spans run a median of 128 world, with 2 per ray (p90 4).
- The tangent anchor is the worst affine choice. A secant or Chebyshev fit over
the span cuts the error bound by 2x to 8x, so the spans could be about 1.4x to
2.8x longer at the same τ. That was not measured.
## 2. Simplex cells per span, per octave
Octave `xm` is the coverage octave's frequency times m, warped like the coverage
octave. The shipped detail octave is x4 and is *unwarped* in both fields, so it
needs no affine span at all.
Counts are for τ = 0.1 tiled spans with both warp phases, given as the median and
then the p90. The rows are simplex cells crossed, then lattice points touched,
which is the exact work.
| Octave | Void cells | Void lattice points | Wells cells | Wells lattice points |
|---|---|---|---|---|
| x1 | 6 / 16 | 10 / 16 | 4 / 8 | 8 / 10 |
| x2 | 8 / 27 | 10 / 26 | 8 / 12 | 10 / 14 |
| x4 | 13 / 52 | 14 / 44 | 12 / 20 | 14 / 20 |
| x8 | 20 / 100 | 20 / 80 | 22 / 38 | 22 / 34 |
| x16 | 40 / 201 | 35 / 154 | 44 / 72 | 38 / 60 |
| x32 | 75 / 396 | 62 / 302 | 86 / 144 | 70 / 112 |
| x64 | 151 / 793 | 118 / 592 | 172 / 287 | 132 / 218 |
- On the same spans, the march takes a median of 2 samples per span on the void
(p90 10) and 12 on the wells (p90 25).
- A straight line touches 3.44 lattice points per noise unit.
## 3. Cost: exact against the march
- **Measured cost.**
- One snoise is 530 to 610 ns in C# on this host.
- One lattice-point integral (hash, gradient, closed-form polynomial) is
85 ns, or 0.16 snoise.
- With this probe's naive box-scan enumeration it is 1.24 snoise. That is an
upper bound; a simplex walk visits only crossed cells.
- **How the table counts.** Exact is lattice points × 0.16 + 1 snoise per span
for the Jacobian. March is 1 snoise per sample. Both sides carry the two warp
phases.
- **Tolerance.** τ is 0.1 × *that octave's own* cell, so the affine error is
comparable across octaves.
- **Columns.** The two error columns are RMS errors of the active-length mean
of one warped octave. The last error column is what replacing the octave by its
expectation of 0 costs.
| Octave | Void exact/march cost | Void err exact / march / expectation | Wells exact/march cost | Wells err exact / march / expectation |
|---|---|---|---|---|
| x1 | 0.36 | 1.1e-2 / 1.6e-3 / 0.21 | 0.11 | 1.9e-2 / 9e-5 / 0.29 |
| x2 | 0.46 | 1.0e-2 / 4.7e-3 / 0.13 | 0.15 | 1.5e-2 / 2e-4 / 0.21 |
| x4 | 0.68 | 9.0e-3 / 4.0e-2 / 0.10 | 0.23 | 1.1e-2 / 5e-4 / 0.18 |
| x8 | **1.03** | 5.8e-3 / 8.0e-2 / 0.067 | 0.36 | 7.2e-3 / 1.1e-3 / 0.10 |
| x16 | 1.64 | 4.0e-3 / 9.0e-2 / 0.050 | 0.56 | 6.4e-3 / 9.4e-3 / 0.088 |
| x32 | 2.89 | 2.8e-3 / 0.105 / 0.036 | 0.87 | 4.7e-3 / 3.3e-2 / 0.057 |
| x64 | 5.26 | 2.0e-3 / 0.095 / 0.025 | **1.44** | 3.2e-3 / 5.3e-2 / 0.040 |
- **Where exact stops paying on cost:**
- At the measured kernel cost: x8 on the void and between x32 and x64 on the
wells.
- At the brief's model of one snoise per cell crossed: about x1 to x2 on the
void and x8 on the wells, from the per-span table in the probe log.
- **Exact against the march on accuracy:**
- At the shipped step the march is *more* accurate for octaves it oversamples:
void x1 to x2, wells x1 to x8. The exact path's error floor is the affine warp
error, about 1e-2 at τ = 0.1 per cell, and it scales with τ.
- Exact is more accurate at about 0.7x the cost where the march undersamples:
void x4 to x8, wells x16 to x32.
- Above about x16 on the void, the undersampled march is worse than simply
using the expectation (0.095 against 0.025 at x64). That is a band-limiting
result, which the footprint octave weights already exploit. It is not an
exact-integration result.
## 4. High-octave variance over a span
- **Span-mean statistics.** Over straight lines of noise length ℓ, the std of
the snoise span mean, against a point std of 0.358, is:
- 0.234 at ℓ = 1;
- 0.170 at ℓ = 2;
- 0.121 at ℓ = 4;
- 0.087 at ℓ = 8;
- 0.061 at ℓ = 16;
- 0.031 at ℓ = 64.
This is about ℓ^-0.5: the high octaves average out like a random field, not
like a periodic one.
- **Detail octave replaced by its expectation of 0,** on τ = 0.1 spans, measured
as |ΔOD| per span:
- Void: median 0.000 and p90 0.091. Relative to the span's OD (OD > 0.01) the
median is 2.5% and the p90 is 7.6%. The mean detail displacement is a median
1.0 world (p90 3.6) against the ramp width S = 50.
- Wells: median 0.11 and p90 5.7 optical depths. The relative median is 72%.
- **Why the wells are so bad.** Normalised to the clamp, the density moves
A·fade/B = 2 per unit of noise, and with extinction 0.5 that is 1 optical depth
per world unit per unit of noise. The mean detail displacement of 1.5 world
(p90 3.8) is 15% to 38% of the floor blend B = 10. The wells' optical depth is
set by where the clamp boundary lies, and that is exactly what the expectation
erases.
- So the expectation is adequate for the void's detail octave and is not
adequate for the wells.
## 5. The clamp and other nonlinearity
- **Void.** 50.0% of τ = 0.1 spans cross a regime boundary: 0 (d' ≤ Rh), the
wall ramp, or K (d' ≥ Rc). 45.6% of the active length lies in the ramp, where
the density is `K max(0, 1 − 1.5 (1 − (d'/Rc)²)^0.40)`. That is not a
polynomial in n, so exact ∫n does not give ∫ρ there. Outside the ramp the
density is constant and needs no noise.
- **Wells.** 42.3% of spans cross the clamp. Only 9.8% of the active length is
unclamped, so most of the length where the noise "matters" is length where the
noise only decides whether the density is zero.
- **The decisive check: transmittance error.** This is the sup over the ray of
|T_est − T_true|, with the truth taken on a 0.5- or 1-unit grid.
| Estimator | Void median / p90 | Wells median / p90 |
|---|---|---|
| Shipped march | 0.020 / 0.038 | 0.033 / 0.155 |
| March at 4x density | 0.006 / 0.009 | 0.030 / 0.049 |
| Density of the *exact* span-mean n (τ = 0.1 spans) | 0.072 / 0.125 | 0.719 / 0.997 |
The last row is an upper bound on what any exact-mean method can deliver
without also modelling the distribution inside the span. It is the
Heitz-style correction the survey describes. It loses to the march on both
fields.
## Triangle basis (operator's addition)
The triangle noise is `triNoise3d` in Aetheria `Assets/Shaders/Volumetric.cginc`
(origin/master f1dee184).
- `tri(x) = |frac(x) − 0.5|`.
- It runs two Nimitz-style iterations. Each iterates `p += tri3(bp·2)`, scales
`bp` by 1.8 and `p` by 1.2, and adds `tri(p.z + tri(p.x + tri(p.y)))/z`.
- It makes 18 tri calls per evaluation.
As shipped, `density()` uses it as follows:
- `pow(triNoise3d(·), exponent)`, with Noise.Exponent −0.25 multiplied by a
slope `flatness` read from a texture (NOISE_SLOPE is on).
- Two phases, warped by `globalFlow`, which is
`Tri3D = cross(normalize(tri3(P)), normalize(tri3(1.618P))) × 15`.
- The fine octave at ×8 is evaluated at a `pos` whose y the coarse octave has
already displaced.
- Amplitude −36.17.
Self's reading, tested:
- **"The warped ray is an exact polyline."** This holds for the warp *inside*
`triNoise3d`. It does not hold for Aetheria's flow: `normalize` and `cross`
make the flow piecewise rational. With the normalize dropped, the cross product
of two piecewise-linear vectors is piecewise quadratic. tri composed with a
piecewise polynomial stays piecewise polynomial of the same degree, with
breakpoints at quadratic roots, so that variant is still closed-form.
Counted along the real flow at the worst shift, the kinks run 15% to 30% above
the straight-ray counts, but the pieces between them are curved.
- **"The clamp roots are linear."** This holds only if the exponent is 1. With
−0.25·flatness, each piece is `(a + bt)^e`. One such piece has a closed-form
integral, but the clamp's root on a sum of two powered phases plus the
smoothstep fade has none.
- **"Whole periods give the exact mean."** This holds for one triangle wave at
whole periods, where the mean is exactly 0.25. It fails for the composed form,
which has no period along a general line; the scales 2, 1.8 and 1.2 and the
nested warp are incommensurate.
- Measured on lines, the span-mean std of `triNoise3d` is 0.029 at ℓ = 1,
0.010 at ℓ = 8 and 0.0042 at ℓ = 64, against a point std of 0.083. That is
about ℓ^-0.47, the same random-field decay as simplex.
- Its bias against the global mean of 0.1995 is within ±0.0015, so the
expectation is unbiased. It is not exact.
- **The cost decides it.** The exact piecewise-linear walk is verified, matching
a 1e-4 quadrature within 3.4e-8. But the nest has 77.7 linear pieces per noise
unit of straight line, against 3.44 lattice points for simplex.
- One piece costs 1.38 point evaluations with a derivative carried.
- At Aetheria's scale, on the wells rays, the pieces per world unit and the
exact cost relative to the march are:
| Octave | Pieces per world unit | Exact cost / march |
|---|---|---|
| x1 | 0.20 | 2.8x |
| x2 | 0.38 | 5.5x |
| x4 | 0.76 | 11x |
| x8 (shipped fine) | 1.5 | 22x |
| x64 | 12 | 176x |
- On the void rays it is 4.1x at x1 and 29x at x8.
- At the same time the march's RMS error on the span mean is already small:
2.4e-3 at x1 and 1.7e-2 at x8.
## Per-basis verdict
| Basis | Exact line integral | Under the shipped warp | Optical depth | Verdict |
|---|---|---|---|---|
| Simplex (CultMath `snoise`) | Closed form, 0.16 snoise per lattice point, 3.44 points per noise unit; validated | Affine spans of 100 to 300 world at τ = 0.1 cell; error floor about 1e-2 | Not delivered: the void's pow profile and the wells' clamp make ρ nonlinear in n; the exact-mean transmittance loses 3.6x and 22x | **Viable kernel, wrong target.** Pays only if the density were affine in n over the span, or as a control variate for a march (survey §4, Kettunen 2021) |
| Triangle (`triNoise3d`) | Closed form per linear piece; validated | Not piecewise linear under Aetheria's normalized-cross flow or the −0.25 power; made linear by exponent 1 and a non-normalized flow | Same nonlinearity, plus 78 pieces per noise unit | **Loses.** 3x to 22x the march even where its preconditions hold |
| Gabor kernels (survey §1.2, not probed) | Closed form for any affine-transformed kernel (complex erf, at most 16 terms) | The affine warp spans above transfer directly, with each span a whitening transform | Same density-nonlinearity limit | **Candidate smooth basis for large structure**, to be measured only if the density is first made linear in the noise over a span |
## What would change the verdict
- **A density that is affine in the noise inside a span.** For example, the
void's ramp replaced by a linear ramp in d', with clamp crossings bracketed by
the interval bound. Exact simplex integration would then deliver optical depth
directly in the 54% of void length that is outside the ramp's clamps. As
measured, that 54% is constant density and needs no noise at all.
- **Exact integration as a control variate,** not a replacement. The exact mean
is cheap and unbiased for ∫n, and the march corrects the nonlinear residual.
That is the Kettunen construction. It was not measured here.
- **Wider skipping.** The operator's first idea, skipping space outside the warp
range, is already what the interval envelope does, and the probe's active
intervals are its output.
- On the void the noise matters over only about 140 world per ray.
- On the wells, opacity ends the ray within a few world units of entering the
unclamped region.

View file

@ -0,0 +1,425 @@
# Analytic Noise Integration Along Warped Rays: Prior Art Survey
This is the facts file for an idea the operator raised on 2026-10-03:
> "I would be happy if we could just skip the space we know has no meaningful structure
> because it's outside of the noise warp range. But what you said about affine arithmetic
> makes me wonder if we can somehow, even within the distorted domain, figure out a series
> of optimal (in terms of extracting structure over multiple frames) spans for which we can
> analytically integrate the noise gradients over those ray segments."
Self's sketch, as given to this pass:
- Simplex noise along a straight ray is piecewise polynomial in t, so its line integral
is closed-form per crossed cell.
- A domain warp p + w(p) is nearly affine over a short span, with an affine-arithmetic
error bound, so within that span the warped ray is a straight line in noise space.
- Low octaves are integrated exactly. High octaves are replaced by their expectation,
with a variance bound.
- Clamped density max(0, n - c) is integrated by root-finding per piece.
- Spans are cached and refined across frames.
Eyes pass, 2026-10-03. This file gives facts and evidence pointers only. It makes no
recommendation.
Verification marks:
- **[fetched]** The claim was read in the cited source's full text during this pass.
PDFs were converted with `pdftotext -layout`.
- **[search]** The claim came from a search-result summary or abstract, not a full read.
- **[derivation]** The claim is elementary mathematics worked out in this pass from a
fetched definition. It is not a claim made by any cited source.
- **[not found]** The pass searched for this kind of work and found nothing.
## 0. Premises of the sketch, checked against definitions
- **[fetched] + [derivation]** Simplex noise kernel degree. Gustavson, *Simplex noise
demystified* (2005), gives the 3D reference code. Each of the four simplex corners
contributes `t^4 * dot(g, d)` with `t = 0.6 - |d|^2`, and the contribution is zero
when `t < 0`. On a straight ray `d` is affine in the ray parameter, so `|d|^2` is
quadratic and each corner term is a polynomial of degree 8 + 1 = 9. The pieces have
two kinds of breakpoint: the simplex faces the ray crosses (where the corner set
changes), and the entry and exit of each corner's support sphere (`|d|^2 = 0.6`).
Both kinds are roots of polynomials of degree 2 or lower in the ray parameter, so the
breakpoints are closed-form. With the 0.6 radius, the support extends past the
simplex, and the reference code truncates it at the simplex boundary. The resulting
small discontinuity across faces is a known property of that constant. It does not
change the piecewise-polynomial structure.
- **[derivation]** Classic Perlin noise (Perlin 2002 quintic fade `6t^5 - 15t^4 + 10t^3`,
read in Gustavson's text **[fetched]**) is, within one lattice cube along a straight
ray, a polynomial of degree 3 x 5 + 1 = 16. Its breakpoints are the cube faces.
- **[fetched]** Piece count grows with frequency. Balint (CESCG 2019, section 2)
states that many noise functions are piecewise integrable, but the number of pieces
is linear in the noise frequency. Their cost is therefore similar to numerical
integration. Balint designed cosine noise to escape this limit (section 1.1 below).
- **[derivation]** For the clamp `max(0, n - c)` on a degree-9 or degree-16 piece,
the roots of `n - c` are needed. Polynomials of degree 5 or more have no general
closed-form roots (Abel-Ruffini), so the clamp needs numerical root isolation per
piece, for example Sturm sequences, Descartes' rule or Bernstein-basis subdivision.
The integral between the isolated roots is closed-form.
## 1. Closed-form line integrals of noise, and analytic optical depth
### 1.1 Cosine noise: Balint, *Closed Form Transmittance in Heterogeneous Media Using Cosine Noise*, CESCG 2019 (non-peer-reviewed)
- **[fetched]** It computes noise defined as an offset plus a sum of cosines of
`x . S_i`. The paper uses 24 quasirandom vectors S_i, with Sobol directions on the
sphere and lengths in [0.8, 1.2], to look roughly like Perlin noise. Fractal octaves
are further cosine sets. Its line integral is a sum of sines, so the optical depth
over any segment is exact. The paper says this takes two noise samples, one at each
segment end. It also gives closed-form distance sampling, by inverting the integral
with binary search.
- **[fetched]** Cost: two noise-sized evaluations per segment, independent of octave
count. Table 1 reports transmittance evaluation at 179 ms closed-form against
1250 ms for delta tracking, about 7x faster. A real-time demo with 16 samples ran
at 80 fps at 675 x 1200 (RTX 2080 Ti).
- **[fetched]** Error: exact, with no error term, for the bare sum. The limits are
stated in section 6. Multiplying by a shape function, or composing with a shaping
function, generally destroys the elementary antiderivative. The workarounds
recover exactness only piecewise: constant multipliers between bounding isosurfaces,
or a cubic polynomial interpolant of the multiplier along the ray, since the product
of a polynomial and cosines has an elementary antiderivative. Negative densities from
a low offset are handled by clamping samples and integrals, which is not an exact
`max(0, .)` integral. Domain warp is not discussed.
### 1.2 Gabor kernels: Condor, Hermann, Yurtsever, Didyk, *Gabor Fields: Orientation-Selective Level-of-Detail for Volume Rendering*, ACM TOG 45(4) art. 62, 2026 (arXiv 2602.05081)
- **[fetched]** It computes closed-form definite line integrals of anisotropic 3D Gabor
kernels along an arbitrary ray segment (eq. 9). Each kernel is whitened into its
canonical space with a ZCA matrix, so any covariance, that is any affine-transformed
kernel, uses the same formula. The segment integral is the real part of a
complex-erf (Faddeeva) difference.
- **[fetched]** Cost: the complex erf is evaluated by a truncated Taylor series with
early exit, capped at 16 terms. Segments shorter than 1e-4 in whitened units fall back
to a single midpoint evaluation. Kernels are spatially clamped with an adaptive extent
that shrinks with frequency (eq. 15). The paper also renders procedural clouds as
Gaussians modulated by Gabor noise.
- **[fetched]** Error: each segment integral is exact apart from the erf truncation.
Orientation-selective masking of kernels whose line integral is near zero trades a
small error, or variance under stochastic selection, for speed. The paper reports
about 1.78x to 1.96x speedups. Per segment the density-integral estimator is
unbiased, but transmittance is biased by Jensen's inequality. Eq. 19 gives the
second-order bias as `0.5 * Var(tau) * exp(-E[tau])`.
### 1.3 Analytically integrable media, older
- **[search]** Iser, *Real-time Light Transport in Analytically Integrable
Quasi-heterogeneous Media*, CESCG 2018. Balint cites it **[fetched]** as using only
exponential and spherical density functions.
- **[fetched via Gabor Fields related work]** The closed-form Gaussian-primitive
transmittance line (Knoll et al. 2021; Condor et al. 2025, *Don't Splat your
Gaussians*) is the base that Gabor Fields extends.
### 1.4 Piecewise-polynomial segment integration in scientific volume rendering
- **[fetched]** Engel, Kraus, Ertl, *High-Quality Pre-Integrated Volume Rendering Using
Hardware-Accelerated Pixel Shading*, Graphics Hardware 2001. The scalar field along
the ray is assumed piecewise linear between samples. The volume rendering integral of
each linear segment, through an arbitrary nonlinear transfer function, is then one
lookup in a table indexed by front scalar, back scalar and segment length. Cost: one
texture lookup per segment, plus recomputing the table when the transfer function
changes. Error: exact for the piecewise-linear model. The model error is the
scalar field's deviation from linear between samples.
- **[search]** Williams, Max, Stein, *A High Accuracy Volume Renderer for Unstructured
Data*, IEEE TVCG 4(1), 1998. Exact integration through tetrahedra with linearly
varying scalar and piecewise-linear transfer functions.
### 1.5 Polynomial noise along a ray
- **[not found]** No published closed-form segment integral of Perlin or simplex noise
was found, as a method or a code library. Balint's remark in section 0 is the only
published statement found on the piecewise approach, and it dismisses it on cost
grounds.
## 2. Noise designed for analytic filtering or integration
- **[fetched]** Lagae, Lefebvre, Drettakis, Dutré, *Procedural Noise using Sparse Gabor
Convolution*, ACM TOG 28(3), 2009. It computes a sparse-convolution noise of Poisson
impulses with Gabor kernels. Each kernel is a Gaussian in frequency, so filtering with
a Gaussian pixel filter multiplies Gaussians, which gives an analytically filtered
kernel. Cost: the paper typically uses 100 impulses per kernel, or 25 to 50 at lower
densities. Table 2 (GTX 280) reports, at 20 impulses per cell, 290 fps for 2D noise,
111 fps for unfiltered surface noise and 50 fps for filtered surface noise. Error:
the filtering is exact for a Gaussian filter. The noise statistics converge with
impulse density.
- **[fetched]** Lagae, Drettakis, *Filtering Solid Gabor Noise*, ACM TOG 30(4), 2011. It
slices solid Gabor noise, filters each slice of each Gabor kernel, and gets a
generalisation of the projection-slice theorem. It also states that wavelet noise
integrates solid noise perpendicular to the surface. That is a line integral of noise
along the normal, done in the basis.
- **[search]** Cook, DeRose, *Wavelet Noise*, ACM TOG 24(3), 2005. A quadratic B-spline
noise band, which is almost band-limited, built from a precomputed coefficient tile.
Its 3D-to-surface projection integrates the noise along the normal. The original PDF
was not retrievable in this pass. Pixar and ACM both returned HTML.
- **[fetched, survey]** Lagae et al., *A Survey of Procedural Noise Functions*, CGF 2010.
- Lewis's sparse convolution noise has a power spectrum equal to the kernel's,
scaled.
- Frequency clamping (Norton, Rockwood, Skolmoski 1982) cancels bands above the filter
rate. It works only for narrowly band-pass noise, and Perlin noise is only weakly
band-pass.
- Nonlinear functions of noise, such as thresholds and colour maps, are not filtered
by filtering the noise. That is unsolved in general.
- Heidrich, Slusallek, Seidel 1998 (below) computes an area average of a procedural
shader with an error bound, using affine arithmetic.
- Perlin noise's amplitude distribution is visibly non-Gaussian, because it is zero at
every lattice point and has few gradients. The other noises surveyed are
approximately Gaussian.
- **[fetched, via the survey above]** "Filtering procedural textures" lineage:
- Rhoades et al. 1992 filters the colour table over `[N - e, N + e]`.
- Hart et al. 1999 sets `e` from the local noise gradient.
- Lagae 2009 sets the range from the analytically known loss of variance.
## 3. Affine arithmetic, reduced AA, Taylor models, and bounds on warps
### 3.1 Gamito and Maddock
- **[fetched]** Gamito, Maddock, *Ray Casting Implicit Fractal Surfaces with Reduced
Affine Arithmetic*, The Visual Computer 23:155-165, 2007. The full method is read in
Gamito's PhD thesis (Sheffield), sections 5.3 to 5.4.
- **Computes:** a root of `g(t) = 0` along a ray, by subdividing intervals of t. Each
interval is tested with an inclusion `G(T)` computed in reduced AA (Messine's AF1).
The form is `t0 + t1 e1 + t2 e2`: e1 is the shared ray-parameter symbol, and e2
absorbs every new nonlinearity by condensation after each operation.
- **Noise in RAA:** gradient-noise kernels lose no correlation under RAA, because the
three multiplications are local to one kernel and the cubic Hermite fade is done by
a single Chebyshev affine approximation. The same holds for sparse convolution
noise. Cellular noise loses accuracy, because `min` needs cancellation between
condensed terms, and the F2 and F3 kernels were not implementable.
- **Interval optimisation:** the parallelogram `g1/t1` slope plus or minus `g2`
shrinks `[ta, tb]` before subdividing (eq. 5.27).
- **Cost:** Table 5.2, gradient-noise hypertexture, average evaluations per ray and
time:
| Method | Evaluations per ray | Time |
|---|---|---|
| IA | 116.57 | 7m35s |
| Standard AA | 61.69 | 40m41s |
| RAA | 61.69 | 6m43s |
| RAA + interval optimisation | 46.39 | 5m04s |
RAA costs three numbers per quantity against two for IA. Standard AA is tight
but slow because its error-symbol lists grow.
- **Error bound:** conservative inclusion, as proven for AA by Comba and Stolfi 1993.
Rounding error is ignored.
- **[fetched]** Gamito thesis, chapter 9, *Surface Deformation with Flow Mapping*.
- Domain deformation `f(d(x))` with `d` defined by streamlines, that is by an ODE.
This is the case closest to a domain warp in this survey.
- Gamito states that a robust and tight AA estimate of the deformed ray `s(t)` was not
obtainable through the numerical ODE solution, so robustness was given up.
- The replacement test uses Lipschitz unbounding spheres at `s(ta)` and `s(tb)`. A
span is skipped if `|s(tb) - s(ta)| < min(ra, rb)` (eq. 9.23). A sharp turn of the
deformed ray between the endpoints can make this test wrong.
- He names truncated Taylor-series range estimation (Nedialkov et al. 2004) as the
possible fix, at higher cost and complexity.
### 3.2 Other affine and interval arithmetic sources
- **[fetched, bibliographic]** Comba, Stolfi, *Affine Arithmetic and its Applications to
Computer Graphics*, SIBGRAPI 1993. de Figueiredo, Stolfi, *Affine Arithmetic: Concepts
and Applications*, Numerical Algorithms 37, 2004. Gamito cites both. AA keeps
first-order correlations through shared noise symbols. Each nonaffine operation adds
one symbol, whose coefficient bounds the linearisation error. Chebyshev or min-range
affine approximations give that coefficient for unary functions.
- **[search]** Knoll, Hijazi, Kensler, Schott, Hansen, Hagen, *Fast Ray Tracing of
Arbitrary Implicit Surfaces with Interval and Affine Arithmetic*, CGF 28(1), 2009.
Inclusion-preserving reduced AA, SIMD CPU at interactive rates, and a stackless GPU
version.
- **[fetched, abstract]** Fryazinov, Pasko, Comninos, *Fast Reliable Ray-tracing of
Procedurally Defined Implicit Surfaces Using Revised Affine Arithmetic*, Bournemouth
TR-NCCA-2009-04. Revised AA, which also keeps one accumulated error symbol, is
reported fastest among the IA and AA variants compared, including on pseudo-random
procedural objects.
- **[fetched]** Sharp, Jacobson, *Spelunking the Deep: Guaranteed Queries on General
Neural Implicit Surfaces via Range Analysis*, ACM TOG 41(4), 2022. This is AA through
long compositions of nonlinear layers, which is structurally like noise composed with
a warp. Table 1 compares the variants. Time is relative to one scalar evaluation,
length is the 1D region that can be certified, and raycast time is relative to the
best method:
| Variant | Time | Length | Raycast time |
|---|---|---|---|
| Interval | 2.4x | 0.011 | 34.3x |
| Affine-full | 95.3x | 0.821 | 8.4x |
| Affine-fixed (no new symbols kept, extra error condensed into one) | 4.7x | 0.306 | 1.0x |
The paper uses affine-fixed for ray casting and affine-full for 3D queries.
- **[fetched, survey quote of abstract]** Heidrich, Slusallek, Seidel, *Sampling of
Procedural Shaders using Affine Arithmetic*, ACM TOG 17(3):158-176, 1998. It computes
the average of a procedural shader over a finite area, with an AA error bound.
- **[fetched, project page]** Keeter, *Massively Parallel Rendering of Complex
Closed-Form Implicit Surfaces*, ACM TOG 39(4), 2020. Interval arithmetic over a
shallow, high-branching tile hierarchy does two things: it skips empty tiles, and it
prunes the expression tape per tile. One benchmark drops the expression size by about
two orders of magnitude. It requires only C0 continuity, so warps and blends that break
Lipschitz continuity are allowed.
### 3.3 Lipschitz bounds through warps
- **[fetched]** Galin, Guérin, Paris, Peytavie, *Segment Tracing Using Local Lipschitz
Bounds*, CGF 39(2), 2020. It computes a local Lipschitz bound over a ray segment
instead of a global one. Section 4.4 covers warps `f o w^-1`, and bounds the gradient
by the product of the field gradient bound over the transformed segment and the
Jacobian norm bound over the segment. Cost: fewer field queries than sphere tracing,
with no acceleration structure. Error: conservative, if the local bounds are
conservative.
- **[fetched]** Seyb, Jacobson, Nowrouzezahrai, Jarosz, *Non-linear Sphere Tracing for
Rendering Deformed Signed Distance Fields*, ACM TOG 38(6), 2019.
- **Computes:** the ray in deformed space as a curve in undeformed space, by
integrating an ODE whose derivative is the inverse-deformation Jacobian times the ray
direction. An initial value comes from rasterising a deformed hull mesh.
- **Error control:** step requirements for the curve are separated from those for the
SDF. Adaptive Runge-Kutta (RKF45 and DP54 evaluated) is used far from the surface,
and forward Euler near it, against an error tolerance.
- **Scope:** surfaces only. There is no density integration.
- **[fetched]** Moinet, Neyret, *Fast Sphere Tracing of Procedural Volumetric Noise for
Very Large and Detailed Scenes*, CGF 2025 (Eurographics), HAL hal-05046040.
- **Computes:** sphere tracing of FBM density fields, using first- and second-order
(2-Lipschitz) forward inclusion functions. The FBM partial sums act as a nested
bounding volume hierarchy: the remaining octaves are bounded by their amplitude sum
`s(i+1, n)`, so evaluation stops early once a point is provably outside.
- **Cost:** up to 12x faster than classical sphere tracing on opaque or constant-density
scenes. Only up to 1.8x on non-constant density, and that gain comes from empty-space
skipping alone. One example frame: 16 ms against 110 ms naive at 1024 x 1024 on a
4080 Ti.
- **Stated limits:** FBM used as a domain warp is not supported, nor are nonlinear
transforms of FBM. The paper names analytic estimation of the contribution of
sub-pixel detail as an open problem.
### 3.4 Taylor models
- **[search]** Berz, Makino, *Verified Integration of ODEs and Flows Using Differential
Algebraic Methods on High-Order Taylor Models*, Reliable Computing 4, 1998. Makino,
Berz, *Taylor Models and Other Validated Functional Inclusion Methods*, IJPAM 2003.
- **Computes:** a Taylor model is a polynomial of order n plus an interval remainder
bound, which encloses any function given as code over a box. The sharpness of the
remainder scales with the box width to the power n + 1. The method also gives
verified ODE flows.
- **Verified integration:** Berz and Makino also published *New Methods for
High-Dimensional Verified Quadrature* (Reliable Computing 1999). There the integral
of the polynomial part is exact, and the remainder bound times the domain measure
bounds the error.
- First-order AA is the n = 1 case in spirit: an affine part plus a bounded remainder.
### 3.5 Affine bound on a warp over a span
- **[derivation]** Over a segment of half-length r, the best affine approximation
(Chebyshev) of a C2 coordinate function has sup error at most `r^2 / 4` times the
maximum of its second derivative along the segment. Linear interpolation of the
endpoints has `r^2 / 2` times that maximum. The noise error caused by a displacement
error of size `delta` is at most `L_noise * delta`. This is the same chain-rule
structure Galin uses in section 4.4.
## 4. Statistical or expected-value treatment of high-frequency detail
- **[fetched]** Heitz, Nowrouzezahrai, Poulin, Neyret, *Filtering Color Mapped Textures
and Surfaces*, I3D 2013. It computes the filtered value of a colour map applied to a
procedural function as `<C, N(mu, sigma^2)>`. The noise's footprint distribution is
replaced by a Gaussian with the footprint's mean and variance. This equals the colour
map convolved with `N(0, sigma^2)` and evaluated at the mean, which is precomputed into
a 2D table indexed by (mu, sigma). Cost: one lookup after mu and sigma are computed.
Error: exact when the driving function's footprint distribution is Gaussian. For a
specific noise instance it converges as the footprint grows, and the paper states this
limit.
- **[derivation]** For `X ~ N(mu, sigma^2)`, `E[max(0, X - c)] = (mu - c) Phi(z) +
sigma phi(z)` with `z = (mu - c) / sigma`. This is the rectified-Gaussian mean, and the
scalar instance of the Heitz 2013 construction for the map `max(0, . - c)`. The
survey's fetched finding that Perlin noise is non-Gaussian bears on how accurate it is.
- **[search]** Olano, Baker, *LEAN Mapping*, I3D 2010. Dupuy et al., *LEADR Mapping*, ACM
TOG 32(6), 2013. They store the first and second moments of slopes in mipmaps. Linear
filtering of the moments gives a Gaussian or Beckmann slope distribution per
footprint. Cost: two extra texture channels and linear filtering. Error: the Gaussian
assumption.
- **[fetched]** Gabor Fields (1.2): band and orientation masking of high-frequency
kernels, with the Jensen bias of transmittance `~ 0.5 Var(tau) exp(-E[tau])`.
- **[search]** Vicini, Jakob, Kaplanyan, *A Non-exponential Transmittance Model for
Volumetric Scene Representations*, ACM TOG 40(4), 2021. Bitterli et al., *A Radiative
Transfer Framework for Non-exponential Media*, ACM TOG 37(6), 2018. Jarabo, Aliaga,
Gutierrez 2018, arXiv 1805.02651. Unresolved, correlated sub-voxel density makes
transmittance non-exponential. Positive correlation gives slower decay than exponential.
- **[search]** Kettunen, d'Eon, Pantaleoni, Novák, *An Unbiased Ray-Marching
Transmittance Estimator*, ACM TOG 40(4), 2021. A power-series estimator built around a
control variate. The control variate is a ray-marched mean-density estimate, and rare
higher-order terms remove the bias. It is relevant as the known way to make a
deterministic or expected-value optical depth unbiased.
- **[fetched]** Schneider, *The Real-time Volumetric Cloudscapes of Horizon: Zero Dawn*,
SIGGRAPH 2015 Advances course. The high-detail noise is applied only as erosion where
the low-detail sample is non-zero. Rays take cheap large steps until the low-detail
isosurface is hit, step back once, and then take full samples. After several zero
samples they return to cheap stepping. This is an octave-level skip heuristic with no
bound. Moinet (3.3) is the bounded version for FBM.
## 5. Temporal reuse of per-pixel or per-tile raymarch structure
- **[fetched]** Schneider 2015 (above). The full raymarch cost about 20 ms. Each frame
renders 1 of 16 pixels per 4 x 4 block into a quarter-resolution buffer, and
reprojects the previous frame. Where reprojection fails, for example at screen edges,
a low-resolution buffer is substituted. The paper reports 10x or more faster, with a
target of about 2 ms on PS4.
- **[search]** Klein, Strengert, Stegmaier, Ertl, *Exploiting Frame-to-Frame Coherence
for Accelerating High-Quality Volume Raycasting on Graphics Hardware*, IEEE
Visualization 2005. First-hit depths of the previous frame are reprojected by point
splatting to estimate per-pixel ray start points. About 2x on typical datasets.
- **[search]** Yagel, Shi, *Accelerating Volume Animation by Space-Leaping*, IEEE
Visualization 1993. A C-buffer of first non-transparent voxel coordinates, updated
across frames. Gudmundsson, Randén 1990 introduced inter-frame empty-space skipping.
- **[search]** Scherzer et al., *Temporal Coherence Methods in Real-Time Rendering*,
CGF 2012 STAR (Eurographics 2011). Reprojection caches, with depth-tolerance validity
tests for disocclusion.
- **[search]** Hillaire, *Physically Based Sky, Atmosphere and Cloud Rendering in
Frostbite*, SIGGRAPH 2016 course. Temporal reprojection for volumetric clouds. Details
were not read in this pass.
- **[fetched]** Gamito thesis, chapter 7: progressive refinement rendering of
Lipschitz-bounded and AA-bounded surfaces. It refines across passes within one image,
not across animated frames.
- **[not found]** No source was found that caches per-ray span partitions, affine
forms or certified intervals across frames and refines them.
## 6. Warp linearisation combined with exact segment integration
- **[not found]** No source was found that combines piecewise affine linearisation of a
domain warp, with an error bound, and exact closed-form integration of noise over the
resulting segments.
- Nearest pieces, each covering part of the combination:
| Source | What it covers | What it leaves out |
|---|---|---|
| Gamito ch. 9 | Bounding a deformed ray | No integral. AA gave up on the ODE warp |
| Seyb 2019 | ODE curved ray with adaptive error control | Surfaces only |
| Galin 2020 | Jacobian-norm local bounds through warps | Surfaces, not integration |
| Keeter 2020 | Interval pruning that tolerates warps | Surfaces |
| Gabor Fields 2026 | Closed form for any affine-transformed Gabor kernel along a segment | No warp |
| Balint 2019 | Closed form for cosine noise | Composition and warps break it |
| Pre-integration (Engel 2001) | Exact per-segment integral | Under a piecewise-linear model of the scalar |
| Moinet 2025 | Bounded FBM octave skipping | Excludes FBM used as a domain warp |
- **[search]** Rezk-Salama et al., *Fast Volumetric Deformation on General Purpose
Hardware*, Graphics Hardware 2001. Fetched text: piecewise-trilinear deformation
patches with adaptive subdivision, rendered by object-aligned slicing. The
approximate inverse is `x + O(delta^2)`. A search summary describes the deformed rays
as polylines, but the fetched text uses slicing, not per-ray exact integration.
## Sources
- Balint 2019: https://www.cl.cam.ac.uk/~rkm38/pdfs/balint2019cosine_noise.pdf
- Condor et al. 2026, Gabor Fields: https://arxiv.org/pdf/2602.05081
- Gamito PhD thesis: https://staffwww.dcs.shef.ac.uk/people/S.Maddock/phd_theses/gamito/thesis_small.pdf
- Gamito, Maddock 2007: https://link.springer.com/article/10.1007/s00371-006-0090-7
- Lagae et al. 2009, Gabor noise: https://www-sop.inria.fr/reves/Basilic/2009/LLDD09/LLDD09PNSGC_paper.pdf
- Lagae, Drettakis 2011: https://inria.hal.science/inria-00606907/file/paper_0049_preprint.pdf
- Lagae et al. 2010 survey: https://www-sop.inria.fr/reves/Basilic/2010/LLCDDELPZ10/LLCDDELPZ10STARPNF.pdf
- Heitz et al. 2013: https://inria.hal.science/hal-00765799
- Sharp, Jacobson 2022: https://arxiv.org/pdf/2202.02444
- Seyb et al. 2019: https://cs.dartmouth.edu/~wjarosz/publications/seyb19nonlinear.html
- Galin et al. 2020: https://hal.science/hal-02507361
- Moinet, Neyret 2025: https://inria.hal.science/hal-05046040
- Keeter 2020: https://www.mattkeeter.com/research/mpr/
- Fryazinov et al. 2009: https://eprints.bournemouth.ac.uk/11708/1/TR-NCCA-2009-04.pdf
- Knoll et al. 2009: https://onlinelibrary.wiley.com/doi/10.1111/j.1467-8659.2008.01189.x
- Berz, Makino 1998: https://link.springer.com/article/10.1023/A:1024467732637
- Engel et al. 2001: https://www3.cs.stonybrook.edu/~mueller/teaching/cse616/engel.pdf
- Gustavson 2005: https://cgvr.cs.uni-bremen.de/teaching/cg_literatur/simplexnoise.pdf
- Schneider 2015: https://advances.realtimerendering.com/s2015/
- Kettunen et al. 2021: https://jannovak.info/publications/URM/URM.pdf
- Vicini et al. 2021: https://dvicini.github.io/non-exponential-representation/
- Rezk-Salama et al. 2001: https://www3.cs.stonybrook.edu/~mueller/teaching/cse616/p17-rezk-salama.pdf
- Klein et al. 2005: https://www.researchgate.net/publication/4188150

View file

@ -0,0 +1,158 @@
# Cross-process file locking and atomic replace: prior art
Facts only. Gathered 2026-10-02 for fork `variants:question:ts-python-write-lock`. No conclusions, no recommendation.
Provenance legend: **[src]** read from CultLib source this pass; **[primary]** vendor/spec/upstream doc or source fetched this pass (the fetch tool summarises pages through a small model, so quoted phrasing is paraphrase-grade unless shown in quotes); **[secondary]** search-result snippet or third-party issue, weak; **[UNVERIFIED]** not confirmed by any source read, or derived by inference only. Nothing here was tested by running code on Windows or Linux.
Dates caveat: the doc-site fetches reported release dates for `fs2 0.4.3` ("September 9, 2026") and `fs4 1.1.0` ("September 23, 2026"). The fs2 date is implausible against my memory of that crate (long stable, last release years earlier); treat crate dates as **[UNVERIFIED]**.
---
## 0. What CultLib does today (source, this checkout)
| Runtime | Lock | Temp name | Replace | Pointer |
|---|---|---|---|---|
| C# | Sidecar `<file>.lock`, `new FileStream(lockPath, OpenOrCreate, ReadWrite, FileShare.None, bufferSize 1)`. On `IOException`: if `wait` is false return null, else sleep 10 ms and retry for up to 30 s, after which the exception propagates. | `.{name}.{Guid:N}.tmp`, `FileMode.CreateNew`, `FileShare.None`, `WriteThrough`, `Flush(true)` | `File.Replace(temp, path, null)` if target exists, else `File.Move` | `F:\Projects\CultLib\src\GameCult.Caching\CultCache.cs` lines ~3475-3530 |
| Rust | Sidecar `<file>.lock` (`.lock` appended to full file name), opened `create/read/write/truncate(false)` (std default share mode), then `fs2::FileExt::lock_exclusive` / `lock_shared` / `try_lock_exclusive` (WouldBlock => `None`). Unlock explicit, then drop. | `{name}.{uuid v4}.tmp`; staged file `sync_all` then closed | Unix: `fs::rename`. Windows: `MoveFileExW(MOVEFILE_REPLACE_EXISTING \| MOVEFILE_WRITE_THROUGH)` retried 25 times, 20 ms apart, on ERROR_ACCESS_DENIED(5)/SHARING_VIOLATION(32)/LOCK_VIOLATION(33). Comment cites a Muninn crash on 2026-09-11. | `F:\Projects\CultLib\packages\cultcache-rs\src\lib.rs` ~925-945, 1040-1084, 2870-3030; `Cargo.toml`: `fs2 = "0.4"`, edition 2024 |
| TypeScript | None. In-process promise queue only (`next`/`result` chain). | `${file}.tmp-${pid}-${Date.now()}-${random}` (unique per writer) | `fs/promises.rename`, retried 10 times with 10 ms doubling on EPERM/EBUSY/EACCES; temp removed on error. No fsync of temp. | `F:\Projects\CultLib\packages\cultcache-ts\src\single-file-messagepack-backing-store.ts` ~143-205 |
| Python | None. `threading.RLock` per store object (in-process only). | Fixed `path.with_suffix(suffix + ".tmp")`, i.e. `store.cc.tmp`, shared by all writers | `Path.replace` (`os.replace`); no fsync | `F:\Projects\CultLib\packages\cultcache-py\src\cultcache_py\stores.py` lines 70-82, 121-126 |
Both C# and Rust name the lock `<store path>.lock`. The same-name sidecar is the only thing the two currently share. Both also hold the lock around read-modify-write of the snapshot **[src]**. A lock file is never deleted by either (no stale-file problem; the lock is the kernel object, not the file's existence) **[src, inferred from absence of delete]**.
Note re the fork text: C# does use a lock (stated in the C# code comment: "excludes other handles in this process and in others alike").
---
## 1. Language and OS primitives
### 1.1 .NET `FileStream` / `FileShare`
- **Windows**: `FileShare` maps to the `dwShareMode` of `CreateFile`. `FileShare.None` = share mode 0: "Any request to open the file (by this process or another process) will fail until the file is closed." **[primary]** https://learn.microsoft.com/en-us/dotnet/api/system.io.fileshare . Win32 side: dwShareMode 0 "cannot be opened again, either by the application that opened it or by another application, until its handle has been closed"; failure is ERROR_SHARING_VIOLATION. **[primary]** https://learn.microsoft.com/en-us/windows/win32/fileio/creating-and-opening-files . Share modes are checked at **open** time and are not byte-range locks; they do not call `LockFileEx` **[inferred]**.
- **Unix**: .NET emulates `FileShare` with advisory `flock(2)`. Source (`SafeFileHandle.Unix.cs`, runtime main): `lockOperation = (share == FileShare.None) ? LOCK_EX : LOCK_SH`, called as `FLock(handle, lockOperation | LOCK_NB)`; on `EWOULDBLOCK` an `IOException` is thrown; handle close issues `LOCK_UN`. **[primary]** https://raw.githubusercontent.com/dotnet/runtime/main/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
- So non-`None` opens take a **shared** flock; `None` takes an **exclusive** flock; non-blocking in all cases. A plain read with `FileShare.ReadWrite | Delete` still takes `LOCK_SH`, which would conflict with a writer holding `LOCK_EX` on the same file (only relevant if the same file is opened both ways). **[inferred from source above]**
- Disable switch: env `DOTNET_SYSTEM_IO_DISABLEFILELOCKING`; locking is off by default on Browser/WASI and iOS/tvOS (not Mac Catalyst). **[primary]**
- Because it is advisory flock, it excludes only other openers that also take flock (other .NET processes, or any program calling flock on that file). It does not stop a process that merely `open()`s and writes. **[inferred]**
- Since which .NET version: **[UNVERIFIED]**. I believe the flock-based FileShare emulation has existed since .NET Core 1.x/2.x, and .NET 6 rewrote FileStream into strategies while keeping it, but no source read this pass dates it.
- Windows `FileShare.Delete` is meaningful; the Unix code does not differentiate it. **[primary]**
- `File.Replace(src, dst, backup)` -> Win32 `ReplaceFile`; throws if volumes differ. **[primary]** https://learn.microsoft.com/en-us/dotnet/api/system.io.file.replace . Unix mapping to `rename` **[UNVERIFIED]**.
### 1.2 Rust
- `std::fs::File::lock`, `lock_shared`, `try_lock`, `try_lock_shared`, `unlock`: stable since **Rust 1.89.0**. **[primary]** https://doc.rust-lang.org/std/fs/struct.File.html
- Unix/WASI: `flock` (`LOCK_EX`, `LOCK_SH`, `+LOCK_NB`, `LOCK_UN`). Windows: `LockFileEx` (flags `LOCKFILE_EXCLUSIVE_LOCK`, `LOCKFILE_FAIL_IMMEDIATELY`) and `UnlockFile`.
- Docs: lock "may be advisory or mandatory", platform-dependent; on Windows the file must be opened for write (or read+append) for an exclusive lock; re-locking the same handle or a clone is unspecified, possibly deadlock; locks released when the file and all duplicated handles close; "this may change in the future".
- `fs2` 0.4.x (what CultCache Rust uses): Unix `flock(2)`; Windows `LockFileEx` (source `src/windows.rs`: `lock_shared` passes 0, `lock_exclusive` passes `LOCKFILE_EXCLUSIVE_LOCK`, try variants add `LOCKFILE_FAIL_IMMEDIATELY`; `unlock` uses `UnlockFile`). Docs: "advisory"; do not lock one `File` concurrently; duplicated files need great care. **[primary]** https://docs.rs/fs2/latest/fs2/trait.FileExt.html , https://raw.githubusercontent.com/danburkert/fs2-rs/master/src/windows.rs . An earlier summary of the fs2 docs page said "LockFile" for Windows; the source file says `LockFileEx`. Maintenance status of fs2: **[UNVERIFIED]**.
- `fs4`: fork of fs2 adding async, rustix instead of libc; docs say `flock`/`fcntl` on Unix, `LockFileEx` on Windows. Whether std's `File::lock` is mentioned or deprecates it: not stated on the page read. **[primary]** https://docs.rs/fs4/latest/fs4/ . Which Unix call it picks by default (flock vs fcntl): **[UNVERIFIED]**.
- Windows default share mode of Rust `OpenOptions`: `FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE`; `share_mode(0)` makes exclusive. **[primary]** https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html
- `std::fs::rename`: Unix `rename(2)`; Windows 10 1607+ `MoveFileExW(MOVEFILE_REPLACE_EXISTING)` or `SetFileInformationByHandle(FileRenameInfoEx)` (POSIX semantics); earlier, MoveFileExW only. "may change in future". **[primary]** https://doc.rust-lang.org/std/fs/fn.rename.html . (CultCache Rust bypasses std rename on Windows and calls MoveFileExW itself, per 0.)
### 1.3 Node
- No built-in advisory file lock in `node:fs`; **[UNVERIFIED]** by a direct docs read this pass, but nothing in the packages read contradicts it. Options seen:
- `proper-lockfile`: "mkdir strategy" - creates `<target>.lock` as a **directory**; works across local and network FS; stale detection by lock dir **mtime** (default `stale` 10 s, min 5 s; mtime refreshed every `update` = stale/2, min 1 s); `onCompromised` callback when refresh fails or is late; avoids `O_EXCL` because "O_EXCL is broken on NFS" (their README); contrasts with `lockfile` package which uses ctime for staleness, unsuitable for long-running holders. **[primary]** https://raw.githubusercontent.com/moxystudio/node-proper-lockfile/master/README.md
- `lockfile` (npm), `fs-ext` (native `flock`/`fcntl` bindings): not fetched. **[UNVERIFIED]**
- Libuv rename on Windows uses `MoveFileExW(MOVEFILE_REPLACE_EXISTING)`; reports say it fails with EPERM while any handle holds the destination open, and refuses a READONLY destination; some runtimes (Bun PR) fall back to POSIX-semantic replace. **[secondary]** search snippets: https://github.com/oven-sh/bun/pull/43997 , https://github.com/joyent/libuv/issues/283 . Not read from libuv source: **[UNVERIFIED]**.
- `npm/write-file-atomic`: temp name = `filename + "." + murmurhex(__filename, process.pid, ++invocations)` (plus worker thread id); same-process writes to one file queued and serialised; fsync on by default; no cross-process lock mentioned. **[primary]** https://raw.githubusercontent.com/npm/write-file-atomic/main/README.md
### 1.4 Python
- `fcntl.flock()` wraps `flock(2)`; docs: "On some systems, this function is emulated using fcntl()". `fcntl.lockf()` is "essentially a wrapper around the fcntl() locking calls". Unix only. "On at least some systems, LOCK_EX can only be used if the file descriptor refers to a file opened for writing." The page does not state whether flock and lockf interact. **[primary]** https://docs.python.org/3/library/fcntl.html
- `msvcrt.locking(fd, mode, nbytes)`: locks `nbytes` from the current file position (may extend past EOF); `LK_LOCK`/`LK_RLCK` retry once per second, 10 attempts, then `OSError`; `LK_NBLCK` fails immediately; regions cannot overlap; adjacent regions not merged. Underlying Win32 call not stated in docs. **[primary]** https://docs.python.org/3/library/msvcrt.html . (The CRT `_locking` is documented elsewhere as `LockFile`-based; **[UNVERIFIED]** here.)
- `filelock` (tox-dev): `FileLock` alias = `UnixFileLock` (`fcntl.flock`, described as kernel-enforced and released on crash) on Unix; `WindowsFileLock` described as `LockFileEx` byte-range locking on Windows (older versions used `msvcrt.locking`; version not checked, **[UNVERIFIED]**); `SoftFileLock` is a "cooperative file-existence marker", with same-host PID inspection and reclaim from dead processes; docs recommend verifying shared-filesystem semantics before deploying soft locks; no statement on cross-type exclusion. **[primary]** https://py-filelock.readthedocs.io/en/latest/index.html
- `portalocker`: README fetch 404; its primitives are **[UNVERIFIED]** (not read).
- `os.replace` on Windows semantics (MoveFileEx REPLACE_EXISTING): **[UNVERIFIED]**, not fetched.
### 1.5 Interoperability matrix (flock / fcntl / LockFileEx / share modes)
| Question | Finding | Source |
|---|---|---|
| Linux, local FS: does `flock` exclude `fcntl` (POSIX byte-range) locks? | No. man flock(2): on Linux "there is no interaction between the types of lock placed by flock() and fcntl(2)" (local filesystems). | **[primary]** https://man7.org/linux/man-pages/man2/flock.2.html |
| Linux over NFS (since 2.6.12) | flock is emulated as whole-file fcntl byte-range locks, so flock and fcntl **do** interact there. | same |
| Linux over CIFS (since 5.5) | flock emulated with SMB byte-range locks; flock and fcntl interact; and the lock becomes **mandatory**: I/O from other descriptors on a locked file fails with EACCES. | same |
| Duplicate fds / fork | A lock belongs to the open file description; shared by dup/fork; persists until `LOCK_UN` or **all** duplicates close. Independent `open()`s in the same process conflict with each other under flock. | same |
| Does a Rust `flock` (std or fs2) exclude a .NET-on-Unix `FileShare.None` open? | Both are `flock(LOCK_EX)` on the same inode; by the sources above they should conflict (.NET's non-blocking attempt gets EWOULDBLOCK -> IOException -> CultCache C# retries). **Not tested.** | **[inferred]**; **[UNVERIFIED]** empirically |
| Same, but .NET `FileShare.ReadWrite` opener (shared flock) vs Rust `lock_exclusive` | Conflicts (LOCK_SH vs LOCK_EX), so a reader opening the lock file with any FileShare would block/fail against a Rust holder. CultCache does not open the lock file for reading. | **[inferred]** |
| Windows: does a share-mode-0 open (.NET `FileShare.None`) exclude a Rust `LockFileEx` locker? | Order A: .NET holds the file with share mode 0. Rust's `OpenOptions::open` (share mode default RWD) then fails at `CreateFile` with ERROR_SHARING_VIOLATION, before it can lock. In `open_lock_file` that is a `with_context` error, **not a wait/retry** (looked at the source). Order B: Rust holds a handle (RWD) and `LockFileEx`; .NET `CreateFile` with share mode 0 conflicts with the existing open handle's access -> ERROR_SHARING_VIOLATION -> `IOException` -> C# retries up to 30 s. So mutual exclusion holds in both orders, but in order A Rust errors instead of waiting. | **[inferred]** from MS CreateFile share-mode table + Rust OpenOptionsExt default; **[UNVERIFIED]** empirically |
| Windows: is `LockFileEx` mandatory? | Exclusive region lock "denies all other processes both read and write access to the specified region"; shared lock denies writes; locks are not honoured through mapped views. If a process dies or closes the handle the OS releases the locks, but "the time it takes ... depends upon available system resources" (MS recommends explicit unlock). Handle needs GENERIC_READ or GENERIC_WRITE. | **[primary]** https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex |
| Windows: second handle in same process | "If the locking process opens the file a second time, it cannot access the specified region through this second handle until it unlocks the region." | same |
| Rust-on-Windows `LockFileEx` vs Python `msvcrt.locking` (same region?) | `LockFileEx` regions conflict if they overlap; fs2 locks which byte range (offset 0, len u32::MAX..?) : **[UNVERIFIED]**; msvcrt.locking locks `nbytes` from the current position (so depends on `lseek`); whether CRT `_locking` uses LockFile/LockFileEx and which range would need to match: **[UNVERIFIED]**. |
| Python Unix `flock` vs Rust/.NET flock | Same kernel primitive on Linux/macOS (Python's flock "may be emulated using fcntl on some systems", so platform-dependent). Python `lockf` is fcntl: independent from flock on local Linux FS. | **[primary]** python docs + man page |
| macOS flock vs fcntl | **[UNVERIFIED]** (not fetched). |
| POSIX fcntl lock semantics | Per-process: `close()` of **any** fd for the file by the process drops all of that process's fcntl locks (SQLite doc 2.2 below). flock locks are per open-file-description and do not have that hazard. | **[primary]** SQLite howtocorrupt; flock(2) |
| Python `fcntl.flock` on Windows | Module is Unix only (docs). | **[primary]** |
---
## 2. Lockfile protocols in established software
### git
- Lockfile API: lock for `foo` is `foo.lock`, created with `O_CREAT|O_EXCL`; writer writes new content into it; `commit_lock_file()` renames it over `foo`; `rollback_lock_file()` removes it; writers exclude each other; readers never block and see either old or new content (relies on atomic rename); cleanup of outstanding lockfiles is registered with `atexit` and signal handlers. **[primary]** https://git-scm.com/docs/api-lockfile
- `lockfile.c`: retry with quadratic backoff for at least `timeout_ms`; PID file registered as a tempfile for signal/atexit cleanup; failure message says another git process may be running "or the lock file may be stale" and tells the user to remove it manually. **[primary]** https://raw.githubusercontent.com/git/git/master/lockfile.c
- Consequences recorded in the docs: a hard kill (SIGKILL, power loss) leaves a `.lock` file and later writers fail until it is removed; git does not time-based-reclaim index.lock **[inferred from message text, not stated; UNVERIFIED]**. Note the git design makes the lock file **the temp file** (lock name = staging name); CultCache's current design has a separate sidecar lock plus uniquely named temp.
### SQLite (see also section 4)
- Unix default VFS: POSIX advisory locks via `fcntl()`; Windows VFS: `LockFile`/`LockFileEx`/`UnlockFile`. Five states UNLOCKED/SHARED/RESERVED/PENDING/EXCLUSIVE. **[primary]** https://www.sqlite.org/lockingv3.html (byte-range layout not stated on that page).
- Built-in unix VFS variants: `unix` (POSIX advisory), `unix-dotfile` (dot-file locking), `unix-excl` (holds exclusive lock, WAL index in heap), `unix-none` (no locking), `unix-namedsem` (VxWorks). "Except for 'unix' and 'unix-excl', the various Unix VFSes all use incompatible locking implementations"; processes using different VFSes on one DB "may not see each others locks ... resulting in database corruption". **[primary]** https://www.sqlite.org/vfs.html . (`unix-flock` / `unix-afp` / `unix-nfs` appear in SQLite's source as separate io-method variants; not listed on that page: **[UNVERIFIED]**.)
### LMDB
- Uses a separate `lock.mdb` file for the reader table and writer mutex. Header warnings: stale reader slots from aborted programs make the DB grow quickly (`mdb_reader_check`); stale writer lock clears automatically on Windows, BSD with SysV semaphores, and Linux with robust POSIX mutexes, otherwise all DB users must be closed; "Do not have open an LMDB database twice in the same process at the same time. Not even from a plain open() call - close()ing it breaks flock() advisory locking."; "Do not use LMDB databases on remote filesystems, even between processes on the same host. This breaks flock() on some OSes"; `MDB_NOLOCK` disables locking entirely. **[primary]** https://raw.githubusercontent.com/LMDB/lmdb/mdb.master/libraries/liblmdb/lmdb.h
- Note: that warning is about `flock` and `close()`, which contradicts the usual statement that only fcntl locks have the close() hazard; the source of the discrepancy (older Linux/BSD semantics, or the emulated flock on some OS) is **[UNVERIFIED]**.
### Cargo / npm / pip / Gradle
- Cargo: package-cache and build-dir lock using an OS file lock, prints "Blocking waiting for file lock"; source fetch (`src/cargo/util/flock.rs`) 404; details (flock/fs4/std, handling of filesystems without locking) **[UNVERIFIED]**.
- Gradle: `DefaultFileLockManager` holds cross-process locks on cache/daemon-registry files; reports of builds hanging on locks left by crashed or killed daemons. **[secondary]** web search snippets only (https://discuss.gradle.org/t/gradle-daemon-keeps-locking-intermediate-files-preventing-builds/38004 , https://www.javathinking.com/blog/gradle-build-is-hanging-without-failure-defaultfilelockmanager-acquiring-and-releasing-lock-on-daemon-addresses-registry/ ); Gradle's own docs on lock protocol not read. **[UNVERIFIED]**
- npm: `write-file-atomic` has no cross-process lock (above); npm's cache (cacache) locking: **[UNVERIFIED]**, not fetched.
- pip: **[UNVERIFIED]**, not fetched.
### Windows rename retry (Postgres)
- `pgrename()` on Windows calls `MoveFileEx(from, to, MOVEFILE_REPLACE_EXISTING)` and retries on ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION, ERROR_LOCK_VIOLATION: up to 100 retries, 0.1 s apart (~10 s). Comment: other applications may have the file open without sharing flags; LOCK_VIOLATION seen from some anti-virus software; bounded because the caller may hold locks affecting other backends. **[primary]** https://raw.githubusercontent.com/postgres/postgres/master/src/port/dirmod.c . CultCache Rust uses the same error set with 25 x 20 ms (0.5 s); TS retries EPERM/EBUSY/EACCES 10 times with doubling from 10 ms (about 10 s worst case) **[src]**.
---
## 3. Temp-file naming and rename atomicity
- Unique temp names per writer: C# GUID, Rust uuid v4, TS pid+time+random **[src]**; npm write-file-atomic pid + counter hash **[primary]**; Python CultCache fixed `.tmp` **[src]**. Rust additionally has a startup cleanup that deletes only temps matching `<store>.<uuid>.tmp` (tests at `lib.rs` ~3420, 4667-4689 distinguish `not-a-uuid.tmp`, bare `.tmp`, neighbour stores) **[src]**. Note the naming differs between runtimes: C# `.{name}.{guid:N}.tmp` (leading dot, no dashes) vs Rust `{name}.{uuid-with-dashes}.tmp` vs TS `{name}.tmp-...`; whether the Rust stale-temp sweeper recognises C#/TS temps: it matches the Rust format only (per the test names) **[src, partial reading]**.
- Fixed temp name consequence (inferred, not tested): two processes writing `store.cc.tmp` concurrently share one file: interleaved `write_bytes` (truncate-and-write) can yield a mixed or truncated file that one process then renames into place, and the other process's `replace` can fail with source-not-found. **[inferred]**
- POSIX `rename()`: if `new` exists it "shall be removed and old renamed to new"; the rationale requires the action to be atomic; a directory entry named `new` stays visible to other threads throughout and refers to either old or new file; on failure other than I/O errors `new` is unaffected; spec does not define crash durability. **[primary]** https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html
- Directory fsync: the usual pattern is write temp, fsync temp, rename, then fsync the parent directory so the rename survives power loss; many projects have filed bugs for omitting the directory fsync. **[secondary]** search snippets (e.g. https://github.com/npm/write-file-atomic/issues/64 titled "Rename atomicity is not enough"; https://0xkiire.com/crash-consistency-fsync-rename/). Linux ext4 `auto_da_alloc` replace-via-rename heuristic: **[UNVERIFIED]**, not fetched. Whether CultCache Rust/C# fsync the parent directory on Unix: **[UNVERIFIED]** (not seen in the excerpts read; Rust does `sync_all` on the temp, C# `Flush(true)` + `WriteThrough`; TS and Python do neither).
- Windows `MoveFileExW`:
- `MOVEFILE_REPLACE_EXISTING` replaces the destination if ACLs allow; errors if destination is an existing directory; `MOVEFILE_WRITE_THROUGH` waits for the move to be flushed (documented as guaranteeing flush of a copy+delete move). Same-volume rename is a metadata operation. The doc page does not use the word "atomic" and does not describe behavior when the destination is open by another handle. **[primary]** https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw
- Open-handle failure: reported from libuv/Bun and Postgres as ERROR_ACCESS_DENIED / SHARING_VIOLATION / LOCK_VIOLATION when another process has the destination open without FILE_SHARE_DELETE, or an anti-virus scan holds it. **[primary]** Postgres comment; **[secondary]** libuv/Bun.
- Rust's std rename (1.0+ Windows 10 1607) may use `FileRenameInfoEx` with POSIX semantics, which can replace an open destination; CultCache Rust calls `MoveFileExW` directly instead **[src; primary for std]**.
- Windows `ReplaceFileW` (used by .NET `File.Replace`): replaced file opened with GENERIC_READ|DELETE|SYNCHRONIZE and share mode `READ|WRITE|DELETE`; replacement file opened with **no sharing mode specified**; preserves creation time, short name, object id, DACLs, encryption, compression, named streams; resulting file has the replacement's file ID; all files must be on the same volume; documented failure states leave the system in partially-moved states: ERROR_UNABLE_TO_REMOVE_REPLACED (1175; names unchanged), ERROR_UNABLE_TO_MOVE_REPLACEMENT (1176; with no backup, the replaced file "no longer exists" and the replacement exists under its original name), ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 (1177). `REPLACEFILE_WRITE_THROUGH` "not supported". The page makes no statement that ReplaceFile is atomic; it describes it as combining steps. **[primary]** https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew
- So on Windows, C# (`File.Replace`) and Rust/TS/Python (MoveFileEx or equivalent) use different primitives with different failure windows. **[inferred from the two pages]**
- .NET `File.Move` with overwrite on Windows/Unix: **[UNVERIFIED]** (CultCache C# uses `File.Move(temp, path)` only when the target does not exist; there is a window in which another writer creates the target between the `File.Exists` check and the move; the lock covers it when taken **[src, inferred]**).
---
## 4. Known failures and warnings
SQLite "How To Corrupt An SQLite Database File" (https://www.sqlite.org/howtocorrupt.html) **[primary]**:
- 2.1 Filesystems with broken or missing lock implementations (network filesystems, NFS) can corrupt when multiple processes access.
- 2.2 POSIX advisory locks are cancelled by `close()`: `close()` on any descriptor for the file by any thread in the process releases the process's locks on that file; example: thread B opens, reads and closes the DB file for a backup and thread A's locks vanish silently. SQLite works around it with a mutex-protected global list of open files, which fails if two copies of SQLite are linked into one app (2.3; a commercial product shipped this bug with infrequent corruption on Linux/Mac). WAL multi-process defenses added in 3.51.0 (2025-11-04).
- 2.4 Two processes using different locking protocols (e.g. POSIX advisory vs dot-file) cannot see each other's locks; all connections must use the same protocol.
- 2.5 Unlinking or renaming the DB file while in use: processes end up with different physical files sharing one journal; SQLite warns (since 3.7.17) when the file is unlinked while open.
- 2.6 Multiple links (hard/symbolic links): same hazard; resolved by canonicalising symlinks since 3.10.0.
- 2.7 Carrying a connection across `fork()`: all locking mechanisms fail with unpredictable results.
- 1.3/1.4 Hot journal deleted, moved or mispaired with database.
- The SQLite VFS page: incompatible unix VFS locking => corruption (section 2 above).
Other recorded failure modes:
- Windows `LockFileEx` locks are released by the OS on process death but with a lag (MS doc). **[primary]**
- CIFS flock becomes mandatory (man flock(2)). **[primary]**
- Lock file left behind: git (cleanup via atexit/signal; a hard kill leaves `.lock`, message tells operator to remove) **[primary]**; Gradle hangs after killed daemons **[secondary]**; `proper-lockfile` and `filelock.SoftFileLock` provide mtime / PID-based stale reclaim as a mitigation **[primary]**.
- Lock protocols that are compatible only when everyone uses the same one: SQLite (above); `filelock` docs do not state cross-type exclusion between `UnixFileLock`/`SoftFileLock`. **[primary]**
- Lost-update by missing cross-process lock around read-modify-write: `write-file-atomic` documents only same-process serialisation; no source read this pass reports a concrete lost-update bug in it. A search-based report of corrupted/missing "settings.json" on Windows from a failed atomic write exists as an unvetted issue (https://github.com/BarganConstantin/ccdeck/issues/772, **[secondary]**, read-only destination). Reports specific to cross-language writers of one file, other than SQLite's different-VFS warning: not found this pass. **[UNVERIFIED]**
- Lock file deleted by one participant while others hold or wait on it (different inode afterward): general hazard of delete-on-unlock lock files, not sourced this pass beyond SQLite 2.5 (unlink of a live file); **[UNVERIFIED]** as a named issue for flock sidecars.
---
## 5. Open items not resolved by reading (candidates for an empirical check; nothing done)
1. Do the C# `FileShare.None` lock and Rust `fs2::lock_exclusive` actually exclude each other on Windows (both open orders) and on Linux? Only inferred above.
2. Does fs2 on Windows lock the same byte range as std 1.89 `File::lock` and as Python `msvcrt.locking`/`filelock`? Unread.
3. macOS behavior of flock vs fcntl and of .NET FileShare there. Unread.
4. .NET release that introduced Unix FileShare-via-flock. Unread.
5. Whether any runtime fsyncs the parent directory after rename on Unix. Unread.
6. Cargo, npm, pip, Gradle lock protocol primary sources. Unread.
7. `portalocker`, `fs-ext`, `lockfile`, `proper-lockfile` behavior under Windows share modes. Unread.

Some files were not shown because too many files have changed in this diff Show more