The header comment, semver-policy and F16 of the content map still listed cultcache-ts-v*
as a trigger. They now say publishing is deliberately off and that a tag run uses the
workflow at the tagged commit (cultcache-ts-v0.15.0 sits on 7d292085, before the guard).
semver-policy gains a "Before pushing a tag" note. The guard's comment moves above `tags:`:
inside the list it broke the three `workflow:` tests of check-changelog-semver.test.mjs,
which read `tags:` followed directly by its items.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Publishing @gamecult/cultcache-ts is deliberately disabled until ruled on, and whether
NPM_TOKEN is set is unknown. The other tag patterns and workflow_dispatch are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
0.14.1 and 0.14.2 were never tagged; cultcache-ts-v0.14.0 is the last release. The
Unreleased exports (SchemaConflictError, schemaIdentityOf, the document payload codec)
are in no tagged release, which is what failed the declared-ranges check. Package and
lockfile go to 0.15.0 and every consumer range (cultnet-ts, cultmesh-ts, cultmesh-browser)
moves to ^0.15.0, since a 0.x caret does not admit a new minor.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PR #20 made routingHint, code and details required on CultNetErrorMessage; six call sites in
cultmesh-ts still built the message without them, so build:ts failed with TS2345. cultnet-ts
now owns cultNetTextError(error), the legacy-union decode uses it too, and the call sites
and two test expectations call it. The types are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Unscoped npm names are unclaimed and open to dependency confusion; cultcache-ts was
already @gamecult/cultcache-ts. package.json names, intra-repo dependencies, package-lock,
import specifiers (src, tests, samples, docs/start, first-call docs test), the workspace
list in run-typescript-workspaces, the verify-eve module map and package READMEs now use
the scoped names. Directory names, workspace paths in CI and release tag prefixes are
unchanged. Historical cut docs and past CHANGELOG entries keep the old names.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
load_soa installs a whole table and checks it with refuse_second_globals, but no test
reached that call (PR #15 covered put paths and pull_all). A table holding two records
of a global type is refused with CultCacheSecondGlobal and the prior view survives; a
table with one global loads and replaces the view.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ruling (steward): add the code across C#, Rust, TypeScript and Python, each with a
captured-bytes case. details = { field: "schemaVersion", value: <offered version> }, no routingHint.
- schema (both copies), C# ForUnsupportedSchemaVersion + CultNetErrorCode.UnsupportedSchemaVersion in Rust,
TS code list, Python CULTNET_ERROR_CODES and schema catalog.
- bytes: error-vectors.json gains unsupported_schema_version (C# serializer output, checked by the
C# test, the Rust error_message.rs case, and the TS/Python vector tests; the Python builder reproduces them).
- cultnet-py interop_peer and cultmesh-py server now answer with the contract-shaped error (no extra
messageId, details {field,value}) via messages.unsupported_schema_version_error.
- tests that used unsupported_schema_version as the example of an unknown code now use not_a_code.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cultmesh-portability's getting-started job checked out GameCult/Eve (404 on the forge) and
ran verify-eve-getting-started.ps1 -EveRoot ./Eve, which is Eve end to end. Nothing in it
runs without Eve, so the job goes. reactive-and-resource-lifetime has no Eve dependency
and is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The package root imports node:fs, node:path and koffi through the single-file
store, so esbuild platform=browser fails on it. src/browser.ts is the root
surface minus that store; src/index.ts re-exports it and adds the store, so
the Node entry is unchanged. Exposed as ./browser and as the 'browser' export
condition on the root.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The sample ProjectReferences ../Eve and is not in CultLib.sln (PR #22).
README, getting-started 03/04 and browser-verse-transport now say so where they
tell readers to run it. Pages 01 and 03:73 only link Program.cs as source to
read, which needs nothing, so they are unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The sample ProjectReferences ../../../Eve, which is not checked out beside CultLib,
and the only error in the build was Program.cs CS0234 (GameCult.Eve missing).
The sample stays on disk and is built by scripts/verify-eve-browser-network.mjs
with --eve-root; it is just no longer part of the default solution build.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The forge runner is Ubuntu-only (Docker, one job), so Windows jobs queued on
push and pull_request can never run.
- cultnet-interop: the whole job is Windows-only; trigger is now workflow_dispatch.
- cultmesh-portability: both matrices keep windows-latest on workflow_dispatch
and run ubuntu-latest alone on push and pull_request.
- quic-native: unchanged; Build win32-x64 already runs only on dispatch or a tag.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Case one-version-rule runs version_ok/version_refused for notice_version, the sample id @N and terms_version; the Rust vector test handles both ops. README cites Heimdall PR #6 d6a8d42 for the rule.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ECDSA P-256 route certificate and session proof verification ported from
CultMeshAuthorityProof.cs using the p256 crate (new dependency, pure Rust,
no default features), judged against the shared contracts/cultmesh vectors.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ECDSA P-256 route certificate and session proof verification ported from
CultMeshAuthorityProof.cs, with typed refusals, judged against the shared
contracts/cultmesh authority route vectors (C# written and TS signed).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
check_version is the single rule; the sample id's @N and MemberTerms.terms_version both call it, so
terms_version "2026-11" is now refused and the golden vector carries 4294967295 (the harness now
validates the terms record). notice_version stays u32 and validate() refuses 0.
Tests: boundary table (1, 4294967295 accepted; 0, 4294967296, 4294967297, 01, +1, -1, empty, 2026-11
refused) across all three; each rule checked by mutation (u64 wrap, leading-zero, sign, terms unchecked,
returned value, empty sample, zero in both records) and killed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Answers Heimdall's review of PR #12 (review 4): tests for the exact U+001F joined key and the empty and
separator refusals; an expect_terms vector op (newcomer case defends the MemberTerms pair) and
expect_no_terms; a mismatched member or experiment acceptance is never training data; the three scope
conditions kept, with a test of the rule over every scope pair; sample versions with a leading zero
are refused (README notes Heimdall may rule normalise instead).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Required is the contract's five keys (schemaVersion, error, routingHint, code, details), code is the
closed enum (or null), details and routingHint are object-or-null, and the catalog has no messageId
property, as cultnet.error.schema.json is closed. The test compares required, property names and the
code enum to the contract file instead of pinning the stale two-key form.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tests/vectors/slot-nil was read by TS only. The Python reader accepts the same 15 rows
and refuses the same 2 as the C# reference and rewrites the reference's bytes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
push, push_all and delete hold <path>.lock (flock; LockFileEx over the whole file on
Windows, as cultcache-ts store-file.ts) across the read and the replace. The staged file
is <path>.tmp-<pid>-<uuid>, fsynced before os.replace, with a directory fsync after off
Windows; a failed write removes its staged file and leaves the store.
Tests fail when the rule breaks: a write waits while a flock holder (Rust's lock) and the
TypeScript runtime's withStoreWriteLock hold the lock, three processes lose no writes,
fsync precedes replace, staged names differ, a failed replace leaves no staged file.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pyproject (and cultnet-py) already declare msgpack as required; the code treated it
as optional behind a cultcache-py[msgpack] extra that does not exist.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#[cultcache(name)], #[cultcache(index[ = alias])] and struct-level #[cultcache(global)]
in the DatabaseEntry derive; get_by_name, get_by_index and get_global on CultCache; a
second record of a global type under another key is a typed CultCacheSecondGlobal refusal
on every write path and on load.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rulings: Heimdall PR #6 comment 759. Golden vectors for empty live text, empty revoked,
and received_at ties in both input orders.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Typed records, validate(), redacted([u8;32]) and the golden vectors follow
the nine rulings on Heimdall PR #6: u32 versions, 32-byte tombstone hash on
the exited Answer, scopeless replies stored with both fields absent, strict
training rule, fresh first reply per notice version. README cites doc lines.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
MessagePack encodes undefined as nil, and the peer rejects a document whose
sourceRuntimeId/sourceAgentId/sourceRole/tags are nil. Omit absent keys; test
that the encoded message carries none.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
NoticeAcceptance, Answer and MemberTerms cut from Heimdall PR #6
(docs/answer-collector-consent.md, head d836f29), with a golden vector
for versioned sample keys, scope-less exclusion from training, redaction
as hash-only tombstone and newcomer notice acceptance. Ambiguities are
listed in the README as questions for Heimdall.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each sends a typed document as a document_put_raw message over a loopback
pipe and applies it to a second cache; a second test per runtime fails when
docs/start/README.md stops showing the snippet verbatim.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Authority holds one arena document in a CultCache and serves it over
CultMesh; two cultmesh-browser clients join, move and leave, and see each
other. Tested with real WebSockets. Browser run, persistence, remote trust
and CI wiring are marked not yet on the recipe page.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
README: the constant 1 in weight = 1 + log_b(amount/unit) is paid once per identity, so splitting
always pays until the log goes negative and no base fixes it; the defence lives outside the function.
Golden vectors (single and split k=1,2,10,100, three bases) computed with 60-digit decimal ln,
pinned by tests/golden.rs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
weight = 1 + log_b(amount/unit) in integer cents and micro-weights with an integer-only
log, plus floor, per-identity cap and minimum amount per identity; splitting_gain/curve
report what splitting a budget over k identities does. 11 property tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both blocks now come from a snippet region in their test, and a second test
fails when docs/start/README.md stops showing it, as the Rust and Python
blocks already do. The TS block differs only in the package import specifier.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace ascending-id trimming with pro-rata scaling by largest remainder
(ascending originator id breaks exact remainder ties only). Receipts carry
counted weight and the originator's latest covering revocation time.
Delegation::revoked becomes Option<u64>. Property tests: trim
order-independence, cap never exceeded, no delegator scaled more than another.
README: prior-art section, placeholders labelled.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Re-delegation to max depth, estate-of-delegator counting, per-estate cap on
the final holder, scope narrowing, direct override, revocation, uncast on
cycle/depth/trim, same-estate-only switch, per-person public receipts.
No stake function; weights are caller-supplied.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pins that height rows built from math.phacelle (the C# mirror of cultmath_phacelle) are bit-identical
whatever order the rows are generated in, for a ring-buffer terrain. Golden fixture untouched.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Exact-case counts move: cycles_arcsine 154 -> 155 and cycles_arctan2 6 -> 7 on both routes; worst u1 and result hashes unchanged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each test file carries the snippet between markers; the docs show it verbatim and a second test in the file fails if the README block drifts. Matrix cells for Rust and Python CultCache now link them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The glsl-parity.json header gains recorded_on (generator writes it; goldens untouched). CyclesOracleTests allows
cycles_arcsine, cycles_arccosine and cycles_arctan2 (glsl-parity families 95-97) 1 ulp from Blender, with the measured
cases named; a test pins the cap at 1 ulp, the three names, and that a 2 ulp miss still fails.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A language x package matrix (each cell a tested snippet or "not yet"), a C# first-call test and a TypeScript first-call test that the docs link, and a pointer from the top of README.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Recorded on Yggdrasil under llvmpipe (LLVM 20.1.2) with
packages/cultmath/tools/device/run-lavapipe.sh, WRITE_BOUNDS=1, at 990521b3.
Adds the lavapipe-glsl and lavapipe-hlsl rows for cultmath_fbm only. The
cycles_arcsine/arctan2 rows are left as committed (pre-existing asin/atan
oracle failures at base 94f20eee are out of scope).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
max singular value of the (cosine, sine) Jacobian <= max(10, 1.25 |side|) / (1 - normalization)
for normalization in [0, 0.9], |side| in [0.25, 25]; method and tightness in design.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
math.fbm(p, octaves, lacunarity, gain) and math.fbm(p) (four octaves, lacunarity 2, gain 0.5);
cultmath_fbm in the shaders, the GLSL regenerated by GlslLowering, a golden-fixture family
appended, FbmTests for the rules.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
A staged write mints a later storedAt, so the 'rewrite kept storedAt' premise is gone; the pin is that a record rewritten under another schema fails Expect. The variant refusal names the renamed base through ChangedKeys, as C2a's typed exception carries it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Directory store now sets StoredSchemaId with StoredVersion after a page write (found by main's OwnPushOfALoadedDriftedEntryKeepsExpectHolding). RecordRewrittenUnderAnotherSchema stages the record, since a flush writes only the write set. Variant conflict test expects CultWriteConflictException.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
#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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
CommittedGlslEqualsLowering fails at this commit until the GLSL is regenerated in the next one.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
_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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Written by PythonWrittenStoreVectorTests under CULTCACHE_WRITE_VECTORS=1 on Yggdrasil; not hand-edited.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
(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>
(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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
ebd66e6f and cc8604ae do not build: the re-mint named MintStoredAt without its class.
Two tests pin the staleness rule for a write that would add a variant to a plain store.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The merge tests become the refuse, reload, retry rule; new tests pin copied records that
cannot be decoded, the later storedAt over a replaced record, a batch that removes a staged
key, and a Mesh write into a variant store. The persistence and composition contracts state
the rule for the parity cuts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
CULTLIB_REAL_STORE names a copy of a real store: one staged write leaves every other record and
catalog entry byte for byte. New: a copied record changed after the load keeps a marked header; what
this cache wrote counts as known id-free; a batch wins over a staged write of its key; a write is
judged on the staged edits plus the variants another writer left in the file.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The unconditional-commit last-writer-wins test now keeps another writer's record; the load-minted ids
stay for MintElementIds after an unconditional write; a detached store marks the variant it writes, not
the one it copies; a variant write supplies the indexed override; the conditional/unconditional
comparison uses one batch (a conditional commit refuses staged writes by design).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
New WriteSetTests (single-file and directory stores, flush and commit): another writer's write and
removal survive, a store that appeared after open is kept, an unstaged in-place change is not
written, a copied record keeps the catalog entry the store holds now, an entry's unknown trailing
fields survive byte for byte, conditional and unconditional commits write the same bytes, the
header drops only when every copied record is known id-free, v2 exactly with a variant. Mesh: a
write copies neighbours forward and replaces only its key. The lay-back and whole-view tests and
the flush-restamp tests are rewritten or removed with the rules they pinned.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
_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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
[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>
[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
683 changed files with 183047 additions and 4539 deletions
"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).",
"description":"Code-specific structured detail (R-N). Every field is populated only for the code that defines it; the others are null, not omitted.",
@ -36,8 +36,8 @@
"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\".","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.","type":["string","null"]},
"field":{"description":"selection_invalid: the selection field that failed. variant_unsupported: \"subscriptionId\", when a subscription was ended. unowned_schema: \"schemaId\". write_not_authorized: \"recordKey\". unsupported_schema_version: \"schemaVersion\".","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. unsupported_schema_version: the schemaVersion the peer does not speak.","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}
"provenance":"Captured from MessagePack C# 3.1.7 (CultNetErrorMessage/CultNetErrorDetails, Options = Standard.WithSecurity(UntrustedData)); pinned byte-for-byte by packages/cultnet-rs/tests/error_message.rs, whose inline hex equals messagePackHex here (test error_vectors_file_matches_the_inline_csharp_captures). Rust encodes to these exact bytes, so C#-written and Rust-written bytes are one set. Every key is written on the wire, nil included.",
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
`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. |
| 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)
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.
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.
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`
`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
- **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.
- `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.
| **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.
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` triggered on `cultcache-ts-v*`, `cultcache-py-v*`, `cultnet-py-v*` and `cultmesh-py-v*` when this was written; it now triggers on the three `-py-v*` patterns only, because npm publishing is deliberately off until ruled on (a tag run uses the workflow at the tagged commit, so `cultcache-ts-v0.15.0` on `7d292085`, before that change, still ran the old one). 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
| **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)`
- Shapes the walk does not own already behave as whole values. A keyed document holding
`ImmutableArray<PElem>`, `(List<PElem>, int)`, `KeyValuePair<string, List<PElem>>`, a non-enumerable
`[DataContract]` holder, or a string-keyed holder (`[Key("items")]`) gives `Inspect` problems 0, an empty
IdPath, `CanHoldIds` false, `Plan` 0 (random and deterministic), and `Holds` false with ids set.
- The marker and the copy check do not count them. Committed with ids `aaaaaaaaaaaa`/`bbbbbbbbbbbb`, the store
header stays `cultcache.store.v1`. A fresh cache that writes another record over that file (the copy-forward
check) keeps `v1`. Reload returns the ids exactly as written, and unset ids stay null after write and reload.
- Variants replace them whole, and that is the C2a resolver's behaviour for every list. A variant `Set` of the
`ImmutableArray` slot resolved to `[9:eeeeeeeeeeee]` with the base untouched, header `v2`, `HoldsIds` false.
A path step `[1:aaaaaaaaaaaa]` is refused: "this runtime resolves top-level Set overrides only". No C2a gap.
The gap belongs to C2b, in the cut sequence below.
- AllowPrivate is read from the most derived `[MessagePackObject]`. `ApDerived` (AllowPrivate over a base
without it) is written `[[],[],4,[]]`: base-chain privates and a private-getter property, with
`ObjectSerializationInfo` at `allowPrivate: true` = `[baseStash, own, N, Pg]`. `NoApDerived` (plain over an
AllowPrivate base) is written `[nil, 5]`. The walk read only `[N]` for both.
- A `[DataContract]``List<T>` subclass is written by `NonGenericListFormatter`, and its record does not read
back ("System.Object[] is not of type PElem"), whatever the walk does. That is a MessagePack limit, not an
id fact.
## 2. Model page (step 0b)
| Kind | Named by | Over time | Decided by |
@ -60,16 +214,16 @@ Other Body facts that shape the map:
| Base reference | the base's key, in the record's variant slot | changed only by an explicit rebase; a base cannot be deleted while it has variants, unless the same batch rebases or removes them | author; the cache refuses a cycle, a missing base, a different concrete type, a global type |
| Override entry | `(op, path, id, value)`; op is Set, Insert or Remove | added by editing; removed by clearing; survives base edits while its path resolves | author; the cache refuses entries a base edit strands (Q2); an entry whose member left the type soft-drifts with a warning (Q4 c) |
| Member path | a sequence of `(slot, elementId)` steps; `slot` is the MessagePack `[Key]`; `elementId` is `""` unless the step enters an object-list element | a type change can invalidate it | the type's owner (slot authority); ids by the element's creator |
| Element id | a `[CultElementId]` string member on an object-list element type; `""` means unset | minted by the cache on write when unset; copied unchanged into variants; a moved element is a new element | cache mints it; it is never retargeted |
| Element id | a `[CultElementId]` string member on the element type of an object list in a shape the walk owns (arrays, `IEnumerable<T>`, `IDictionary`/`IReadOnlyDictionary` values, `[MessagePackObject]`/`[Union]` types with integer keys), one MessagePack writes and reads back (integer `[Key]`, no `[IgnoreMember]`; public, or any visibility under the most derived `AllowPrivate`); `""` means unset. **Exception (q3 amended 2026-10-16):** a shape the walk does not own is one whole value: `ImmutableArray`, `ArraySegment`, `Memory`, `KeyValuePair`, tuples, `Lazy`, unkeyed or `[DataContract]` classes, string-keyed types, a type or member with `[MessagePackFormatter]`, and a non-public member without `AllowPrivate`. Its elements carry no ids: a `[CultElementId]` inside is a plain string | minted by the cache on write when unset; copied unchanged into variants; a moved element is a new element. Inside a whole value: never minted, never checked, never counted for `v3` | cache mints it; it is never retargeted. `CultElementIds.Classify` alone decides owned versus whole |
| Resolved view | `CultStoredDocument.Document` for a variant key | recomputed inside the hold that admits the variant, or any base on its chain | the cache only; never persisted, never written back (Q1) |
| Index and name entries | as today, from the resolved view | a dependent's entries are dropped and re-added in the same `Apply` as its base change | the cache; a variant must override the `[CultName]` member (rule R6) |
| Watches | as today; each re-resolved dependent emits `Updated` with its own `Sequence` | published in the same hold as the base change | the cache |
| Compare-exchange | `(schemaId, storedAt)` of the stored delta (ruling 2) | a base change does not stamp its variants | the cache; the resolved view is not an identity |
| Persisted format version | store header `formatVersion` | `cultcache.store.v2` when the store holds at least one variant; `v1` otherwise (Q5) | the writing store, from content |
| Persisted format version | store header `formatVersion` | `cultcache.store.v2` when the store holds at least one variant; `v1` otherwise (Q5); a marked `v3` (`v5` directory) stays while a write copies any record forward, and only a write that encodes every record drops it by content | the writing store, from content; the durable header alone says the bytes hold element ids |
| Single-file store file | the path; one MessagePack array `[header, catalog, records]` | replaced whole and atomically; refused when unreadable; see "Store authorities" | each runtime's one reader decides what is a store; the writer derives the header from it |
| Store lock and staging files | `<file>.lock`; a unique temp beside the store | the lock is taken by C# and Rust writers (CultMesh included, through `ReplaceDurable`); staging is removed on a failed write | the store, never CultMesh or a consumer |
| Variant slot on the wire (CultNet) | **empty cell**: no CultNet message carries a delta today | C5 | ruling 4 decides deltas; the message shape is C5's design |
| Dictionary entries, scalar lists, arrays | **empty cell**: no stable identity exists | whole-member Set only (Q7) | Q7 |
| Dictionary entries, scalar lists, arrays, whole-list holders | **empty cell**: no stable identity exists | whole-member Set only (Q7; whole-list holders by q3 amended) | Q7, q3 amended |
| Template (non-item) bases | **empty cell**: parked by ruling 6 | none | not in these cuts |
## 3. Where the Body contradicts the target
@ -181,6 +335,43 @@ Dictionary entries, scalar lists and arrays are replaced whole.
- **Recommend yes.** Aetheria's cases (`Hardpoints` whole; `Behaviors[id]...Max`) fit. Keyed
dictionary addressing can be added later without a format change. Affects C2.
## Cut sequence (2026-10-15)
Landed since 2026-10-12: `c2a-declared-apply` (r3, Soul s5) and `main-into-c2a` (r2, Soul s2); the
reconcile step is done, `hands/variants-c2a` is at the reconciled head `6afa8ed9`. Element ids part 1,
`c2a-element-id-holes` (r6), is held by Soul s5/s6 on `hands/variants-c2a-element-id-holes`. Mapped
now: part 2, `c2a-id-closed-world` r2 (2026-10-16, under the amended q3), not blocked. Its open fork
`variants:question:c2a-whole-list-opt-in` holds under every option. Later cuts, one line each, mapped
only when Self is about to dispatch them:
1. **reconcile**: done at `6afa8ed9`.
2. **parity-ts**: TypeScript write set, one record per key, undeclared records carried and refused,
unheld removals judged at commit. Waits on element ids parts 1 and 2 merged into C2a.
3. **parity-rs**: Rust `push_all` and `put_prepared_batch` apply a write set, ownership by
declaration, the same commit-time checks. Waits as parity-ts; can run beside it.
4. **parity-py**: Python write-set apply. Waits as parity-ts, and on `cultlib-gaps` cut
`py-msgpack-default` landing on main (then a main merge into C2a first).
5. **write-set-vectors**: one cross-runtime manifest, read and acted on by all four runtimes;
`directory-at-path-ts-py-pinned`. Waits on the three parity cuts.
6. **land-c2a**: final main merge, full verification, Soul, then `git merge --no-ff` onto main
with an empty diff against the verified head; no tag. Waits on write-set-vectors, Soul holding,
and `asura:cut_spec:cut-semver-measured` landed. Asura Cut 3 bases on its merge commit.
Element-id part 2: `c2a-id-closed-world` r2 decides deferred findings `s5.member-set-misses-allowprivate`, `s5.enumerating-rule-unpinned`, `s6.null-standard-untyped`, `s6.two-recognisers-one-rule`, `s4.unrecognised-holders-fail-open` and follow-up `c2a-datacontract-holder-unwalked` (see its `first`).
- **marker-for-whole-lists** (only if `c2a-whole-list-opt-in` is ruled `marker-when-ids-declared` or `marker-for-any-keyed-object`): a CarriesElements predicate, a refusal unless the member is marked, and a public attribute. It is additive after part 2.
- **C2b constraint** (owed to `variants:follow_up:remaining-variant-cuts`): nested path resolution must refuse a step `[slot:elementId]` that enters a whole value, naming the holder. A whole value is any shape `CultElementIds.Classify` calls Leaf, so C2b asks Classify and keeps no recogniser of its own. Today's C2a resolver refuses every non-top-level step, so no store can hold such an override yet.
Rationale. The 2026-10-02 order (parity, vectors, then merge main) was right when main was close.
Main has since moved 453 commits and rebuilt exactly what the parity cuts change: cultcache-ts
payload ownership, cultmesh-ts, cultcache-rs. Parity mapped before the merge would be mapped
twice. Merging first means every later cut is mapped once, on the code it changes. The C# fix
batch goes first because the operator's at-commit ruling is the contract the parity runtimes
copy; and it touches C# files main barely changed, so it can run beside the merge with one
small reconcile (the `MintStoredAt` region). The High finding is not on this path: h3 fixed it
and Soul s3 held it (body facts above).
## 5. Cuts, in order
All cuts are in `F:\Projects\CultLib`. Rebase `codex/document-variants` (docs only) onto
@ -1414,7 +1605,7 @@ The one door per runtime, and who walks through it:
- **Inputs.** The bytes at the path. **Outputs.** The decoded store with header and catalog; or a typed refusal (`CultStoreUnreadableException` in C#; `CultCacheStoreUnreadable{path, kind}` in Rust, kind `Undecodable` or `UnsupportedFormat`; `StoreUnreadableError` in TS and Python). A refusal names a header only as `cultcache.store.v<digits>`.
- **Derived state.** The write header is never decided by the writer: it is the reader's verdict (header read, whole-store flag, whether ids or variants are held). A rewrite of a file that cannot be read cannot happen, because the reader throws first.
- **Forbidden writers.** A second header classifier. Deleted: Rust `leading_header`, `read_array_header`, `read_string`; cultnet-ts's own decode of the store header; the TS async `readStore`; Rust `StoreUnreadableError` (negative grep `StoreUnreadableError` in `packages/cultcache-rs`: no match); Python `_decode_v1_snapshot`.
- **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore declares cultcache-ts `^0.15.0`, the first release that exports it.
- **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore needs a cultcache-ts release that exports it, which is the release that ships with this change; the manifests move to that version then (c0-ships-with-c2a), not before.
- **Pinned by.**`tests/vectors/document-variants-c2a/readability/manifest.txt`, walked by `StoreReadabilityTests.OpenReadsExactlyTheFilesTheVectorsSayItReads` (C#), `CultMeshSingleFileReadabilityTests.AWriteReplacesAFileExactlyWhenItReads` (Mesh), `a_file_is_replaced_exactly_when_it_reads` (RS:5132), "SingleFileMessagePackBackingStore replaces a file exactly when it reads" (TS test :2321) and `test_single_file_replaces_a_file_exactly_when_it_reads` (Python). AUTH cases: `packages/cultnet-ts/test/idunn-runtime-authority.test.ts`. Declared divergences in the manifest: `legacy-envelopes.bin` (C# refuses; the others read and replace it) and `variant-slot-v3.bin` (C# reads; the others refuse).
- **Known gaps.**`variants:follow_up:c0-bad-canonical-in-manifest` (`current-catalog-bad-canonical.msgpack` is not in the manifest; TS reads it, Rust refuses). `variants:follow_up:legacy-vectors-all-runtimes` (the three `legacy-catalog-*` c0 vectors have a verdict only in C# tests; all four runtimes refuse them, probed). `variants:follow_up:range-check-in-ci` (`npm run test:pack:ts` builds every admitted tag and checks exports, but `.github/workflows/publish-packages.yml` does not run it).
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.
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.
`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.
| 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
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-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
| 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)
| 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 |
- 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).
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.
| 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) |
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.
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.
- 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).
- 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.
- 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.
- 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.
- 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.
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) 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.
# 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).
- `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").
# 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.
- 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.
- 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.
- 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.
- 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
Eyes pass, 2026-10-10, session self-2026-10-10c-eureka. Facts only; no recommendation.
Tags: [fetched] read this session; [local] read in the repo this session; [background] prior knowledge, not refetched; [not found] the fetched page did not say.
## 1. Established systems
### Protocol Buffers
- Unknown fields: proto3 "preserve[s] unknown fields and include[s] them during parsing and in the serialized output", matching proto2. Lost on JSON serialization and on field-by-field copy into a new message; CopyFrom/MergeFrom avoid the loss. (protobuf.dev/programming-guides/proto3/, "Unknown Fields") [fetched]. The version proto3 started preserving (3.5) [background].
- Unknown oneof case: no distinct state. The page warns a NOT_SET result is ambiguous: the oneof may be unset, or set to a field from a newer schema [fetched]. A oneof member the reader lacks is an ordinary unknown field, so it sits in the unknown-field set and is re-emitted [background].
- Unknown enum values: "unrecognized enum values will be preserved in the message" (raw int in open-enum languages, accessor in Java) and re-serialized [fetched].
### Cap'n Proto
- Union: a separate tag says which member is set. Adding a member to a union is safe because "existing programs should already know to check the union's tag"; they "may or may not behave reasonably when the tag has a value they don't recognize". Moving an existing field into a new union can show an old reader "a garbage value or throw an exception" (capnproto.org/language.html, "Unions", "Evolving Your Protocol") [fetched].
- which() on an unknown discriminant: [not found] in the fetched language and encoding pages. C++ generated which() returns the tag as an enum and the reader is expected to switch with a default [background].
- Preservation: [not found] on the fetched pages; structs "can be traversed (e.g., copied) without knowing their type" (encoding.html, Structs) [fetched]. Copy preservation of unknown data is [background].
### Avro (schema resolution; reader schema and writer schema both required)
- Both unions: reader takes its first branch matching the writer's selected branch; "if none match, an error is signalled". Writer union, reader non-union: the selected writer branch must match, else error (Avro spec, Schema Resolution) [fetched; also quoted from the 1.11.1 spec in docs/research/read-only-record-signal-prior-art.md:176-178].
- Record, writer has an extra field: "the writer's value for that field is ignored". Reader field missing from the writer: reader's default if it has one; no default is an error [fetched].
- Enum, writer symbol absent in reader: reader's declared default if any, else error [fetched].
- Preservation: none. Extra fields are dropped by resolution [fetched].
### FlatBuffers
- Union evolution: appending a member is safe ("CodeV1 will not recognize the another_a"); inserting in the middle without an explicit discriminant misreads members; inserting with an explicit discriminant is safe (flatbuffers.dev/evolution/) [fetched].
- What an old reader sees for an unknown union type, and re-serialization preservation: [not found] on that page. Generated readers expose a type byte plus a union table pointer; an unknown type value falls outside the generated enum [background].
### Thrift
- Unknown fields and unknown union field ids: [not found] in thrift-rpc.md. Protocol readers skip unrecognized field ids and drop the data; an unknown union id leaves the union empty, and a union with no field set can fail validation [background].
### MessagePack-CSharp [Union]
- README Union section says only "Unique union keys are required"; it states no behaviour for an unknown key and no option for it [fetched, first 100k chars of the README].
- Missing keys: members initialize to default (README, Object Serialization) [fetched]. Extra array elements for [Key(int)] and unknown string keys: README silent; it shows a manual formatter that loops and skips extras [fetched].
- Generated and dynamic union formatters skip the payload and yield null when the key is absent from the union's key map [background]. The estate's own measurement agrees: unknown arm key decodes to null, no error (question document, "Measured in the C# reference... MessagePack-CSharp 3.1.7") [local, via eureka-state view]. The vendored copy at F:\Projects\CultPong\Assets\Scripts\MessagePack\Resolvers\DynamicUnionResolver.cs does a key-map TryGetValue at :347 (type-map TryGetValue at :206); the miss branch was not read.
- No configurable unknown-union behaviour found in the README; whether a newer version adds one [not found].
### serde
- enum-representations page says nothing on unknown variant names, #[serde(other)] or deny_unknown_fields [fetched]. serde_derive de.rs was fetched but the identifier/enum_ modules holding unknown_variant were not in the excerpt [not found].
- [background] default: an unknown variant is an error ("unknown variant `X`, expected one of ..."); #[serde(other)] on a unit variant makes the identifier decoder return it for any unknown name (units only, payload discarded); unknown struct fields are ignored unless #[serde(deny_unknown_fields)]; a missing field is an error unless #[serde(default)] or Option.
### Bincode / postcard
- [background] Both are non-self-describing: an enum is a variant index (bincode u32, postcard varint) followed by fields in order; an out-of-range index is a decode error; there are no field tags, so an extra or missing slot is detectable only by running out of or leaving bytes. Evolution is by schema versioning outside the format. Not fetched.
## 2. Preservation across an older reader, and the failure each answers
| System | Unknown data survives re-encode? | Failure answered |
|---|---|---|
| Protobuf | yes: unknown fields (hence unknown oneof cases) and unknown enum ints, in binary APIs only [fetched] | old intermediary re-serializing a newer message (purpose: background) |
| Cap'n Proto | page silent; reader sees a tag it may not know and must handle it [fetched] | in-place access; append-only evolution |
| Avro | no: extra field dropped, or error on no branch / no default [fetched] | writer schema travels with data, so resolution is exact and loud |
| Thrift | no (skipped) [background] | forward tolerance for old readers |
| MessagePack-CSharp | no: arm becomes null (measured), extra slot skipped | not documented |
| serde | no; error by default [background] | type safety; leniency is opt-in |
| Bincode/postcard | n/a [background] | compactness; no evolution in the format |
| Unity SerializeReference, DataContract ExtensionData | yes: bytes retained and recoverable (docs/research/read-only-record-signal-prior-art.md:182-205) [local] | data loss from missing types |
## 3. CultCache's own stance (F:\Projects\CultLib)
- docs/persisted-record-encoding.md:1-19: persisted records are verified by re-encoding what was decoded and comparing bytes; a record that reads but re-encodes differently is refused, on live data at startup or next read. Three Idunn guards cited at :10-14. [local] A tolerated extra trailing slot or a null-collapsed arm would not re-encode to the stored bytes; whether those guards would run on such records was not tested.
- No evolution/compatibility doc: `ls docs | grep persist|evol|compat|schema|version` returns only persisted-record-encoding.md. docs/document-variants-cut.md:327-333 covers union-slot reuse at registration ("late union-slot reuse passes registration"; "interface unions are never minted") [local, grep lines only].
- Reference union handling: src/GameCult.Caching/CultInspectorModel.cs:768-774 classifies an abstract/interface type by its [UnionAttribute]s ordered by key and refuses a type with none ("declares no [Union] subtypes", :772); also :120. This is the inspector's shape classifier; decode is MessagePack-CSharp's. No other use of UnionAttribute in src/GameCult.Caching. [local]
- Trailing-slot tolerance: packages/cultcache-ts/test/cult-cache.test.ts:1176-1195, "CultCache interop reader accepts missing compatible trailing slots and rejects mismatched slots": a legacy-payload store with fewer slots reads with `tags: []`; a mismatched-type payload rejects with /Expected string/u. Missing trailing slots are tested; extra trailing slots are not. [local]
- TS decode: packages/cultcache-ts/src/cult-cache.ts:679-692 wraps a formatter as `schema.parse(decode(payload))`, so TS strictness is whatever the zod schema says; no TS union codec (follow-up text). [local]
- Rust: packages/cultcache-rs/src/*.rs has no unknown-arm or extra-slot handling found by grep; "unknown" hits are store-header tests (:4889-4890 refuses an unknown header by name; :5009-5019 unknown-header.msgpack, variant-v2.msgpack). Rust enum arms persist as serde external tags (follow-up text). [local]
- Consumers persisting unions: Aetheria (F:\Projects\Aetheria) has `[Union]` abstract classes in Assets/Scripts/ServerShared: Provenance.cs:96-98 (keys 0-2), ItemInstance.cs:18-21 (keys 0-3) and :29-31, Environment.cs:73. The assembly registers CultCache documents (AssemblyInfo.cs:3-4; Galaxy.cs, ItemData.cs, AetheriaStores.cs reference CultCache); ItemInstance is held in entity item lists (Entity.cs:2003). An older build reading a store with a newer arm key would take the C# null-arm path, if these types sit inside persisted CultCache documents; that persistence path was not traced. Aetheria-legacy has more unions (Behaviors.cs:29-38, Item.cs:43-63, Contract.cs:10-11); commented-out keys 8, 9 at Behaviors.cs:37-38 show arms retired in place. [local]
- Ghostlight and Huginn: not searched for [Union] (the grep matched only Aetheria). Follow-up text: "No live record crosses runtimes as a union today" [local, view]. Same-runtime older-build-reads-newer-store: unverified.
- The question document records the C# measurement: extra trailing slot skipped and equal; missing trailing slot leaves member default; unknown arm null, for a Node in a list and for an Outcome; Rust and TS union decoders with explicit keys refuse by construction; Rust extra-slot behaviour unmeasured [local, view].
## 4. Operator's stated value, and how each option relates (inferred)
Verbatim, operator-rapport.md:50: "nobody should have to carry bytes they don't understand ... CultCache's whole promise is that a cache's contents are self-identifying via the upfront catalog." Context: pushing back on storing retired slots as metadata (:49). [local]
- Preserve-unknown (protobuf, Unity): the older reader carries bytes the catalog does not describe. Opposite to the stated value.
- Drop silently (Avro extra fields, MessagePack slot skipping, Thrift skip): nobody carries the bytes, but the loss is silent and a re-encode no longer equals the stored bytes (persisted-record-encoding.md:4-6).
- Null the arm (current C# union behaviour): the unknown arm becomes a value the catalog does not describe either; the record decodes as if valid.
- Refuse (Avro no-branch, serde default, Rust/TS explicit-key decoders, store-header refusal): the reader declares it cannot understand the bytes; consistent with self-identifying content because undescribed content is not accepted.
- Tolerate trailing slots but refuse unknown arms (fork option 1): the catalog-described slot list governs required members; slots beyond it are skipped, which is the drop-silently pattern for slots and the refuse pattern for arms.
Gathered by eyes-cycles-fma, session self-2026-10-14-eureka, 2026-10-14. Facts only; no recommendation.
Strength marks: [src] read from source text; [doc] vendor or standards documentation; [web] issue, forum or search-result summary; [abs] absence of evidence (searched, not found).
"Raw" Blender paths below are fetched as `https://projects.blender.org/api/v1/repos/blender/blender/raw/<path>?ref=<tag or branch>`. Local copies of the main-branch files are in `...\scratchpad\eyes-cycles-fma\dl\`.
## 0. The finding's own record (what is being tested)
- [src] The finding quotes the oracle probes: with the AVX2 kernel, 17 function rows differ from committed fixtures (floored_modulo 151/1024 cases ... color 55/1024), worst `mix_overlay` 493 ulp, `cycles_floored_modulo` 958152704 ulp (sign flip). Yggdrasil's /proc/cpuinfo shows avx2 and fma.
- [src] Oracle code: `kernel_check()` in `tools/cycles-oracle/oracle.py` bakes FLOORED_MODULO of 10000 by 0.1 and compares to numpy unfused `a - floor(a/b)*b`; exits 4 if the kernel fuses. Read with `git show e48f9488:packages/cultmath/tools/cycles-oracle/oracle.py` (lines ~528-551), README lines 20-30, `CyclesOracleTests.cs` lines 5-18 (those paths are not in the working tree at main; they exist at commit e48f9488). The oracle Blender is 5.2.2.
## 1. Cycles CPU kernels and flags
1. [src] The CMake file defines two x86 CPU kernel variants only: an SSE4.2 baseline and an AVX2 kernel (`WITH_KERNEL_AVX2`). No AVX-512 flag or kernel appears; `kernel.cpp` lists `cpu` and `cpu_avx2`. `intern/cycles/CMakeLists.txt` lines 76-130, 137-143 (main and v5.2.2); `intern/cycles/device/cpu/kernel.cpp` line 11 (v5.2.2): `KERNEL_NAME_EVAL(cpu, name), KERNEL_NAME_EVAL(cpu_avx2, name)`.
2. [src] GCC/Clang flags (CMakeLists.txt lines 97-100): SSE4.2 = `-msse -msse2 -msse3 -mssse3 -msse4.1 -msse4.2`; AVX2 = SSE4.2 flags + `-mavx -mavx2 -mfma -mlzcnt -mbmi -mbmi2 -mf16c`. The `-mfma` flag is what makes FMA instructions available only in the AVX2 kernel. MSVC: AVX2 kernel `/arch:AVX2`, SSE4.2 kernel empty on x64 (lines 82-89). Intel compilers use `/QxCORE-AVX2` or `-xcore-avx2`.
3. [src] Contraction flags, GCC/Clang block (CMakeLists.txt lines 160-180, main = v5.2.2): `-fno-trapping-math`, `-fno-math-errno`, `-fno-signed-zeros`, `-ffp-contract=on`, `-freciprocal-math`; `-fassociative-math` is commented out ("problems with bsdf_D for GGX on Linux (#130389)"); `-ffinite-math-only` deliberately not set. These are appended to the global `CMAKE_CXX_FLAGS`, so they apply to both kernels; only `-mfma` differs. GCC additionally gets `-fno-signaling-nans`, `-fno-rounding-math`, `-mfpmath=sse`.
4. [src] MSVC (non-clang) block (line 149): `/fp:fast`, with the comment "Unlike GCC/clang we still use fast math, because there is no fine grained control and the speedup we get here is too big to ignore." For clang-cl the GCC/Clang flag list is passed with a `/clang:` prefix (line 198).
5. [src] History of the contraction flag (Forgejo API commit messages):
- `f96f3c33af`, 2024-09-30, "Cycles: Add some more math optimizations": added `-ffp-contract=fast`, `-freciprocal-math`, `-fassociative-math`. Message: "These are used on Windows and HIP anyways, so our code can't expect exact IEEE semantics in any case." Measured "~1.5% speedup in my very quick test".
- `0fc58f0aba`, 2024-11-25, "Disable -fassociative-math on Linux to avoid GGX issues".
- `7b19e74cad`, 2025-12-14 (PR 150318): changed `fast` to `on`: "Disable FMA auto-vectorization across different statements in Cycles, because GCC 15.2.1 manages to ruin the viewport's scaling and offset" (GCC bug 106902). It also removed a `remove_cc_flag("-ffp-contract=off")` block ("Not needed to make cycles automated tests pass with -march=native. However Blender itself needs this flag").
- Before 2024-09-30 the CMake file carried no Cycles-specific contraction flag; the global Blender flag was `-ffp-contract=off` (stripped for Cycles by that `remove_cc_flag` block). [src, inferred from the removed lines; the earlier state was not read at an older tag.]
6. [doc] GCC definition: `-ffp-contract=on` "enables contraction within one expression, but not across different statements"; `fast` forms FMA "if the target has native support"; default `off` for ISO C, `fast` otherwise. Source: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html. (Which GCC release implemented `on` for C++ was not checked.)
7. [src] Cycles' floored modulo is one C++ expression: `return (b != 0.0f) ? a - floorf(a / b) * b : 0.0f;` in `intern/cycles/util/math_base.h` (v5.2.2 line 613-616; main line 646-649). `NODE_MATH_FLOORED_MODULO` calls it from `kernel/svm/math_util.h` line 164. `safe_modulo` (truncated) uses `fmodf`, not an expression. `-freciprocal-math` also permits `a/b` to become `a * (1/b)`.
8. [src] Env switch: `intern/cycles/util/debug.cpp` (v5.2.2) line 29 `CHECK_CPU_FLAGS(avx2, "CYCLES_CPU_NO_AVX2")`, line 23 `flag = (getenv(env) == nullptr)`; presence of the variable disables the AVX2 kernel. [web] added 2015-02 by Sergey Sharybin, commit 4b04a4e, described as developer-only: https://lists.blender.org/pipermail/bf-blender-cvs/2015-February/073396.html (via search summary).
9. [src] The AVX2 kernel is selected at runtime by CPU capability: `system.cpp``system_cpu_support_avx2()` requires avx2 and f16c ("F16C is considered part of AVX2 for our purpose"), so a default Blender on any AVX2 CPU uses it.
10. [src] Cycles' own render tests are image tests with a default `fail_threshold = 0.016`, `fail_percent = 1` (`tests/python/modules/render_report.py` lines 287-288), overridden per test (e.g. 0.032, 0.06, 0.048; `tests/python/cycles_render_tests.py` ~339-357). They compare images, not per-node bit patterns. A grep for AVX/NO_AVX in these two files found nothing, so the tests do not appear to pin a kernel. [src for what was found; absence is [abs]]
## 1b. GPU backends
11. [src] CUDA: `intern/cycles/device/cuda/device_impl.cpp` line ~225 passes `--use_fast_math` to nvcc (nvcc fuses FMA by default; `--fmad` default is documented NVIDIA behaviour, not fetched here). `kernel/device/cuda/compat.h` line 89 has the comment "Use fast math functions".
12. [src] HIP: `kernel/device/hip/compat.h` line 91 "Use fast math functions" (the HIP compile flags were not located; the commit message in item 5 states HIP already runs without exact IEEE semantics).
13. [src] Metal: `device/metal/device_impl.mm` line 570 `options.fastMathEnabled = YES;`.
14. [src] oneAPI: `kernel/device/oneapi/CMakeLists.txt` line 71 `-ffast-math`, line 83 `-cl-fast-relaxed-math` for spir64.
15. [src] OptiX: not separately examined (OptiX kernels are CUDA-compiled).
16. [src] The fp-contract/fast-math choices are thus explicit in all of: CPU (`on`, both kernels), MSVC CPU (`/fp:fast`), CUDA, HIP, Metal, oneAPI.
## 1c. Do Blender developers treat CPU/GPU numeric differences as bugs
17. [web] Issue #101114 (Power math node precision): a developer said they do not handle such precision issues as bugs since implementation is by the GPU driver and manufacturer. https://developer.blender.org/T101114 (search-result summary; page not read).
18. [web] Issue #68687 (Eevee modulo inconsistent): a developer said the imprecision "is likely to remain as it is an implementation behavior we cannot ensure"; reporters noted Cycles gave the expected result. https://developer.blender.org/T68687 (summary).
19. [web] Issue #53348 (Cycles CPU vs GPU in World nodes): a developer attributed the difference to "--fast-math we use for CUDA" and noted disabling it costs about 15% speed. https://projects.blender.org/blender/blender/issues/53348 (summary; page returned 403 to WebFetch).
20. [web] Issue #67448 "Shader node float precision issue cause different results on CPU and GPU" exists (archived); its content was not read (redirect not followed). https://developer.blender.org/T67448
21. [abs] No Blender issue specific to Floored Modulo and FMA/AVX2 was found; no Blender document stating a CPU/GPU numeric parity guarantee was found.
## 2. Shader languages
22. [doc] HLSL `precise`: separate operations stay separate ("where a mul and add operation might have been fused into a mad operation, precise forces the operations to remain separate"; fusion then only via the explicit `mad` intrinsic); order of operations preserved; IEEE NaN/INF honoured. It propagates only to operations that contribute to the `precise`-qualified variable; marking shader outputs is recommended. https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-variable-syntax
23. [doc] HLSL `mad`: may be fused or mul+add in hardware; for `precise` values the hardware must be consistent across all uses of that mad on that hardware. https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/mad (search summary). [web] The HLSL team blog says any operation may be refactored unless `precise`, and `/Gis` makes all computations precise: https://learn.microsoft.com/ko-kr/archive/blogs/marcelolr/precise-and-ieee-strictness-in-hlsl (summary).
24. [abs] DXC's default contraction of plain `a*b+c` was not confirmed from DXC docs or source; clang HLSL PR #119820 "Make fast math the default for HLSL" exists (https://lists.llvm.org/pipermail/cfe-commits/Week-of-Mon-20241209/654101.html), content not read.
25. [src] Vulkan SPIR-V environment (https://registry.khronos.org/vulkan/specs/1.3/html/chap37.html, fetched and grepped): "Implementations may rearrange floating-point operations using any of the mathematical properties governing the expressions in precise arithmetic ... may involve a different number of rounding steps"; "This rearrangement can be prevented for particular operations by using the NoContraction decoration." Without NoContraction the flags AllowContract/AllowReassoc/AllowRecip/AllowTransform are assumed. Same page: OpFMod "Inherited from x - y * floor(x/y)"; OpFRem inherited from `x - y * trunc(x/y)`; OpFDiv 2.5 ULP for |y| in [2^-126, 2^126]; and the page lists `OpFmaKHR`/`shaderFmaFloat32` (a core-spec FMA capability) as new.
26. [doc] GLSL: `precise` qualifier ("the qualifier precise will ensure that operations contributing to a variable's value are performed in the order and with the precision specified") "effectively prohibits the arbitrary use of fused multiply-add"; under `precise`, `fma()` is one operation and `a*b+c` is two. Sources: GL_ARB_gpu_shader5 / OES_gpu_shader5 text and https://registry.khronos.org/OpenGL-Refpages/gl4/html/fma.xhtml (search summary). `invariant` was not looked up.
27. [doc] SPIR-V `NoContraction` goes on the intermediate-result instructions, not the variable; GLSL `precise` lowers to it in glslang/ANGLE. [web] LLVM-based backends have contracted NoContraction multiplies into fma before (bug report in search results, wording in item summary only).
28. [web] Metal: `MTLCompileOptions.fastMathEnabled`/`mathMode` (fast vs safe); in safe mode MSL requires correctly rounded FP32 add, sub, mul, div, fma; `metal::precise` namespace gives precise library functions; explicit `fma()` is the advice for controlling fusion. https://developer.apple.com/documentation/metal/mtlcompileoptions/mathmode.md and https://developer.apple.com/forums/thread/90959 (summaries). Current Xcode default for fast math was not confirmed ([web] sources conflict). No Metal `fp_contract` pragma found [abs].
29. [abs] Unity's shader compiler (FXC/DXC via HLSLcc/SPIRV-Cross) contraction behaviour: no documentation found.
## 3. C# / .NET
30. [doc] `Math.FusedMultiplyAdd` (and `MathF`): "Returns (x * y) + z, rounded as one ternary operation"; infinite-precision multiply and add then one rounding. Available .NET Core 3.0+ and .NET 5-11. https://learn.microsoft.com/en-us/dotnet/api/system.math.fusedmultiplyadd
31. [web] dotnet/runtime issue #10137 "[Jit] Recognize FMA pattern and Emit Fused Multiply Add" closed 2024-02-05 as COMPLETED with the maintainer comment: "Closing this. We have explicit `FusedMultiplyAdd` APIs and will likely cover any future 'pick the better of MultiplyAdd or FusedMultiplyAdd' via an explicit `MultiplyAddEstimate` API." (read with `gh issue view 10137 -R dotnet/runtime`). Earlier comments there state the intent that `a*b+c` stay two IEEE roundings. PR #61037 adds an arm64 MADD fold for integers only (summary).
32. [web] Issue #98704 "Math.FusedMultiplyAdd returns wrong results on systems without FMA3 support", closed COMPLETED 2025-05-06: the software fallback had been an estimate (suspected ucrtbase `fma`), so correct rounding was guaranteed only with hardware FMA3; `COMPlus_EnableFMA=0` did not reproduce it on FMA3 hardware. Fix details were not read. Also [web] TA-Lib issue #340 reports the same misrounding concern.
33. [abs] A statement in .NET docs that RyuJIT never contracts floating-point `a*b+c` was not found; #10137's closure is the nearest evidence. The CultMath repo's own use of FusedMultiplyAdd/MathF.FusedMultiplyAdd was not searched.
## 4. Other reference implementations and tolerance policies
34. [src] OpenShadingLanguage testsuite (`testsuite/runtest.py` lines 82-87): image diff thresholds `failthresh = 0.004`, `hardfail = 0.01`, `failpercent = 0.02`, warn at 2x failthresh; "A test fails if more than failpercent of pixel values differ by more than failthresh ... or if even one pixel differs by more than hardfail." Raw URL: https://raw.githubusercontent.com/AcademySoftwareFoundation/OpenShadingLanguage/main/testsuite/runtest.py
36. [doc] Vulkan precision table lists per-operation ULP classes (item 25); functions defined as expressions inherit the budgets of their constituents (GLES3 CTS spec, https://fuchsia.googlesource.com/third_party/vulkan-cts/+/30e25b62d60b36b923d19a0c0759ef6cb7201f9e/doc/testspecs/GLES3/functional.shaders.builtin_functions.precision.txt, summary). Historical draft GL_ARB_shader_precision: `a*b+c` "correctly rounded single operation or sequence of two correctly rounded operations", fma same as a*b+c (https://developer.download.nvidia.com/opengl/specs/GL_ARB_shader_precision.txt, summary).
37. [abs] Not found/not examined: MaterialX reference-vs-generated shader tolerances (only structure of MaterialXTest located); glTF sample renderer conformance; any published "bit-exact vs ulp class" policy statement from Khronos, MaterialX, OSL or Blender; the Vulkan CTS source ULP constants for mix/smoothstep/fma.
## 5. Floored modulo specifics
38. [src] Definition in Cycles: `a - floorf(a / b) * b` (item 7). For a=10000, b=0.1f the quotient a/b rounds to 100000 (float) and floorf gives 100000, so the unfused product 100000*b rounds to 10000 and the result is 0, as the oracle records; the fused form computes the residual of the exact product (b is not exactly 0.1), yielding -1.49e-4 as the finding records. [abs] This arithmetic was not re-run here; it restates the finding's `kernel_check` docstring (oracle.py).
39. [abs] No Blender issue or commit mentioning Floored Modulo together with FMA, contraction or AVX2 was found. [web] Related reports concern modulo/wrap disagreements between Cycles and Eevee/GPU drivers (#101562, #68687, #88151).
40. [doc] Vulkan states OpFMod is "inherited from x - y * floor(x/y)", i.e. the same formula, so a GPU implementation of it follows its own contraction/precision rules (item 25).
## Coverage and gaps
- Strong [src]: Cycles flag set per ISA and compiler, the history of `-ffp-contract`, `safe_floored_modulo`, `CYCLES_CPU_NO_AVX2` mechanism, GPU backends' fast-math flags, Cycles/OSL image-test thresholds, Vulkan NoContraction and OpFMod text, HLSL `precise`.
- Gaps: which compiler/version builds official Blender 5.2.2 release binaries (matters for how `-ffp-contract=on` is honoured); the 4.x flags before/after 2024-09-30 at release tags; DXC/Unity default contraction; Metal contraction pragma; MaterialX, glTF and Vulkan CTS numeric tolerances; a direct statement that RyuJIT never contracts; Blender developer position on CPU/GPU parity beyond forum/issue summaries (several pages 403 to WebFetch).
- Files of record: `C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\0d5eef6e-e366-457f-bbc6-0151080ae1a8\scratchpad\eyes-cycles-fma\dl\` (CMakeLists.txt, math_base.h, math_util.h, vk.html).
Gathered 2026-10-12 by eyes-decode-stack at CultLib `67c79d5c`. No recommendation. Web facts come from WebFetch/WebSearch summaries of the pages named; items marked UNVERIFIED were not confirmed against primary source.
## A. The local door (what exists today)
- `packages/cultcache-rs/src/lib.rs:503``MAX_PAYLOAD_DEPTH = 256`; `check_payload_depth` (~526-540) is an iterative walk with a `[usize; 256]` count stack, scalars skipped by length.
- `lib.rs:598-604``decode_payload`: `check_payload_depth(payload)?; rmp_serde::from_slice(payload)`. Its doc comment calls rmp-serde's own counter (1024) "only a backstop".
- Derive: `packages/cultcache-rs/cultcache-rs-derive/src/lib.rs:294-330` generates `impl Deserialize` with a `Visitor::visit_seq` over slot-ordered arrays (`deserialize_tuple`); unknown trailing elements are consumed via `IgnoredAny`.
- rmp-serde resolved version: 1.3.1 (rmp 0.8.15; `packages/cultcache-rs/Cargo.lock`). Its depth limit is a `u16``depth: 1024`, decremented per array/map/ext (`~/.cargo/registry/src/*/rmp-serde-1.3.1/src/decode.rs` lines 181, 213, 294, 566, 583, 616), error `DepthLimitExceeded`, tunable with `Deserializer::set_max_depth` (clamped to u16::MAX, lines 307-310). CHANGELOG line 95: "Default stack protector depth is now 1024 instead of 1000."
- Upstream master rmp-serde (fetched summary, not checked line by line against 1.3.1): the `deserialize_newtype_struct` MSGPACK_EXT_STRUCT_NAME branch builds `ExtDeserializer` without `depth_count!`; the `any_inner` ext path does use it.
- Existing local tests that already use sized threads: `packages/cultcache-rs/tests/union_parity.rs` ~786-830: `stack_budget_debug_2mib` (comment: "A debug build on a 2 MiB thread carries 405 containers of this document; the bound keeps every accepted payload below that") on a slim fixture (`wrap_chain(126, ...)`), and `the_walk_does_not_recurse` (100000 nested arrays refused on a 256 KiB thread). No stacker, `stack_size`, or thread spawn in `src/` or `Cargo.toml`.
- The rmp-serde 1024 counter and the CultLib 256 bound both count containers; per-level stack cost depends on the target type (follow-up text: 15-19 KiB release, 87-95 KiB debug per level for contract-sized types).
## B. Rust serde ecosystem
### serde_json
- Default limit 128: `Deserializer::new` sets `remaining_depth: 128` (field `u8`); `check_recursion!` returns `ErrorCode::RecursionLimitExceeded` at 0; invoked in `deserialize_any/seq/map/struct/enum`. https://github.com/serde-rs/json/blob/master/src/de.rs
- `disable_recursion_limit()` exists only under Cargo feature `unbounded_depth`. Docs: "Parse arbitrarily deep JSON structures without any consideration for overflowing the stack"; tells the caller to "provide some other way to protect against stack overflows" and names `serde_stacker`. Docs also warn that Display, Debug and Drop on the result can overflow. https://docs.rs/serde_json/latest/serde_json/struct.Deserializer.html
- The limit is a nesting-level count; no per-type frame accounting (code read: counter decrement only). The serde_json README mentions no recursion limit.
### serde_stacker (dtolnay)
- Adapter wrapping a serde (de)serializer that grows the stack dynamically; README examples use `serde_json::Deserializer`, call `disable_recursion_limit()`, wrap with `serde_stacker::Deserializer::new`, nest 10,000 levels. README names Display/Debug/Drop as other overflow points and shows a manual iterative drop. README does not say it depends on `stacker` (UNVERIFIED). https://github.com/dtolnay/serde-stacker
- It is a serde-level `Deserializer` wrapper, so it hooks calls routed through the wrapped deserializer.
### rmp-serde / rmpv
- rmp-serde: depth counter 1024 default (section A). Search found no RUSTSEC advisory or issue for rmp / rmp-serde stack overflow (negative search, not proof). https://github.com/3Hren/msgpack-rust (rmp-serde/src/decode.rs).
- rmpv not inspected.
### Other Rust deserializers
- ciborium: `recurse: usize` default 256 in `from_reader`; `from_reader_with_recursion_limit`; doc: "Set a high recursion limit at your own risk (of stack exhaustion)!" https://github.com/enarx/ciborium (ciborium/src/de/mod.rs)
- serde_cbor: RUSTSEC-2019-0025, tags nested without limit, <1 kB input overflows the stack, fixed in 0.10.2 by limiting nested tags. https://rustsec.org/advisories/RUSTSEC-2019-0025
- rustc-serialize: RUSTSEC-2022-0004, JSON parser with no depth limit, "stack overflow, which aborts the process", unpatched, advises serde. https://rustsec.org/advisories/RUSTSEC-2022-0004
- yaml-rust: RUSTSEC-2018-0006, deep nesting aborts, fixed by depth check. https://rustsec.org/advisories/RUSTSEC-2018-0006
- serde-json-wasm: GHSA-j87p-gjr6-m4pv, overflow on deep untrusted JSON, fixed 1.0.1 by recursion depth check. https://advisories.gitlab.com/pkg/cargo/serde-json-wasm/GHSA-j87p-gjr6-m4pv/
- quick-xml PR 982: adds a depth cap to its serde deserializer, default 128 "matching serde_json". https://github.com/tafia/quick-xml/pull/982
- prost: `const RECURSION_LIMIT: u32 = 100`, comment "100 is the default recursion limit in the C++ implementation"; disabled by feature `no-recursion-limit`. https://github.com/tokio-rs/prost/blob/master/prost/src/lib.rs
- bincode, postcard, simd-json: searches returned nothing on depth handling; not established either way.
- Rust forum user report: serde overflowing the default stack beyond roughly 100 levels in some types, worked around with a 32 MiB thread. https://users.rust-lang.org/t/deserialize-large-nested-struct-without-stack-overflow/136335
## C. Stack-growth mechanism: stacker / psm / stacksafe
- `stacker::maybe_grow(red_zone, stack_size, f)`: checks whether the current stack is within `red_zone` bytes of its end; if so allocates a new stack of "at least `stack_size` bytes" and runs `f` there; else runs on the current stack. The closure runs on a stack with at least `red_zone` bytes available. https://docs.rs/stacker/latest/stacker/fn.maybe_grow.html
- Source (rust-lang/stacker `src/lib.rs`): remaining stack = stack pointer minus thread-local `STACK_LIMIT` (lazily from `guess_os_stack_limit`); if `remaining_stack()` is `None`, `maybe_grow` "immediately grow[s]"; panics are caught on the new stack and re-raised after the segment is freed (`catch_unwind` / `resume_unwind`); on unsupported platforms `_grow` is a no-op that just calls the callback.
- Platform: README says psm on most platforms, a Fibers-based implementation on Windows; "On all unsupported platforms this library is a noop... code will continue to hit the guard pages". https://github.com/rust-lang/stacker
- psm README table (rust-lang/stacker/psm/README.mkd): Windows x86_64 Available Yes / Tested Yes / Callstack "Yes, but disabled"; Windows i686, aarch64, arm: No/No/No; Linux x86_64, i686, aarch64, arm: Yes/Yes/Yes; wasm: No/No/No ("callstack is not even in an address space"). Text: stacks allocated the usual way are not valid on Windows; Fibers offered as an "unnecessarily slow and inflexible" alternative.
- CONFLICT, not reconciled: stacker's code selects a Windows backend (`backends::windows::_grow`, `cfg(all(windows, not(miri)))`) while the psm table marks Windows callstack "disabled"; stacker has a wasm32 branch (alloc-based restore guard) while psm lists wasm as No. An older docs.rs stacker 0.1.0 page lists a different verified-platform list.
- Not found in sources read: per-call cost numbers; Windows aarch64 behavior of stacker beyond the psm table.
- rustc usage (ensure_sufficient_stack, red zone / segment constants): two fetches returned 404; not recorded.
- `stacksafe` (fast/stacksafe): `#[stacksafe]` attribute checks remaining stack at function entry and allocates a new segment below a threshold; defaults 128 KiB threshold, 2 MiB segment; optional `serde` feature gives stack-safe (de)serialization for the `StackSafe<T>` wrapper; MSRV 1.88.0; credits Orson Peters' `recursive` crate; says it supports platforms stacker supports (dependency on stacker not confirmed). https://github.com/fast/stacksafe
## D. Dedicated thread with sized stack
- `std::thread`: default spawned-thread stack "currently 2 MiB on all Tier-1 platforms"; `Builder::stack_size`; `RUST_MIN_STACK` (overridden by `stack_size`, "may be ignored after program start"); "the stack size of the main thread is not determined by Rust". https://doc.rust-lang.org/std/thread/index.html
- Local use is in tests only (`union_parity.rs`, section A). No library was found in this pass that spawns its own decode thread for untrusted input; the Rust forum report (section B) and the protobuf.js advisory describe it as a caller-side or process-level workaround.
- protobuf.js CVE-2026-44289 workarounds: reject excessively nested messages at an outer protocol boundary, or isolate decoding "in a process that can be safely restarted". https://corgea.com/advisories/vulnerabilities/CVE-2026-44289
## E. Non-recursive decoders
- `deser` crate (mitsuhiko, experimental): "does not recurse on the call stack, so deep nesting needs no recursion limit"; nested sinks/emitters kept in an arena; not serde-compatible; calls itself "an experiment". https://github.com/mitsuhiko/deser/wiki
- `core-json`: no-std JSON parser with a fixed-size or caller-supplied state stack; typed layer is a separate crate, `core-json-traits`. https://docs.rs/core-json
- No serde-compatible explicit-stack derive deserializer found (search negative).
- msgpack-javascript: decoder iterative, `StackPool` array of states; DecoderOptions has no max depth, only length caps (`maxStrLength`, `maxBinLength`, `maxArrayLength`, `maxMapLength`, `maxExtLength`); pool memory grows with deepest nesting; array states allocate `new Array(size)` up front. https://github.com/msgpack/msgpack-javascript/blob/main/src/Decoder.ts
- msgpack-python C unpacker (`unpack_template.h`): explicit `unpack_stack stack[MSGPACK_EMBED_STACK_SIZE]`, `start_container` returns -3 at `top >= SIZE`. `unpack.h` defines the size as 1024 (`unpack_define.h` default is 32). Local probe (msgpack 1.2.2, CPython 3.13.15, `python -I`): 100, 256, 1000 nested arrays decode ok; 100000 raises `StackError`.
- CultLib's own `check_payload_depth` walkers are iterative (Rust lib.rs ~526; TS `packages/cultcache-ts/src/payload-depth.ts`; C# `CultPayloadDepth.cs`).
## F. Advisories and other-language limits
- MessagePack-CSharp CVE-2026-48506 / GHSA-vh6j-jc39-fggf (published 2026-06-25): `MessagePackReader.TrySkip()` recursed without counting depth, bypassing `MaximumObjectGraphDepth`; reachable through generated formatters' `reader.Skip()` on unknown keys; uncatchable StackOverflowException; affected <2.5.301 and 3.0.0 to <3.1.7; fix: iterative loop with a heap pending-count. https://securelayer7.net/lab/cve-2026-48506-messagepack-csharp-tryskip-uncontrolled-recursion-dos . Related: CVE-2026-48512 (`ConvertFromJson` recursion), CVE-2026-48513 (Union deserialization not consistently under depth enforcement).
- Default `MaximumObjectGraphDepth`: the sources read do not state it (a figure of 500 was not confirmed).
- protobuf: documented unmarshal depth limits Java 100, C++ 100, Go 10000 (plan to lower to 100); the page gives no rationale. https://protobuf.dev/programming-guides/proto-limits/ . CVE-2024-7254: protobuf-java nested SGROUP unknown fields/maps unbounded recursion, StackOverflow, CVSS 4.0 8.7, fixed 3.25.5 / 4.27.5 / 4.28.2. https://cveawg.mitre.org/api/cve/CVE-2024-7254 (CONFLICT: RHEL reports not affected; an OpenCVE page says no fix, against the CVE record's fixed versions).
- Go encoding/gob CVE-2022-30635: uncontrolled recursion on ignored fields; fix adds a depth limit of 10,000; fixed in 1.17.12 / 1.18.4. https://go.googlesource.com/go/+/6fa37e98ea4382bf881428ee0c150ce591500eb7 . Go encoding/json nesting constant: not confirmed.
- Python `json`: docs say the module "does not impose any such limits beyond those of the relevant Python datatypes themselves or the Python interpreter itself"; RecursionError for deep input not mentioned. https://docs.python.org/3/library/json.html . The pure-Python msgpack fallback Unpacker recurses (`# TODO should we eliminate the recursion?`), has no depth option, converts `RecursionError` to `StackError`. https://github.com/msgpack/msgpack-python/blob/main/msgpack/fallback.py
- msgpack5 (JS) CVE-2026-107298: no nesting limit, deep input exhausts the decoder stack (title seen only). https://vulert.com/vuln-db/CVE-2026-107298
## G. CultLib's other runtimes
- C# reference: `src/GameCult.Caching.MessagePack/CultMessagePackSecurity.cs`: `UntrustedData.WithMaximumObjectGraphDepth(256)` as backstop; the rule is `CultPayloadDepth.Check`. MessagePack package pinned 3.1.7 (`GameCult.Caching.MessagePack.csproj`), the TrySkip fix version. Other C# surfaces use bare `MessagePackSecurity.UntrustedData` (`src/GameCult.Mesh/CultMeshProviderSessionWire.cs:231`, `src/GameCult.Networking/CultNetSchemaMessageSerialization.cs:15`, `src/GameCult.Networking/MessageSerialization.cs:8`).
- TypeScript: `@msgpack/msgpack ^3.1.3` (`packages/cultcache-ts/package.json`); decoder iterative with no depth option (section E); encode passes `maxDepth: MAX_PAYLOAD_DEPTH * 2` (`src/document-formatter.ts:21`); `checkPayloadDepth` iterative. node_modules not present, so the installed version was not read.
- Python: `msgpack>=1.0` (`packages/cultcache-py/pyproject.toml`); `documents.py:135` and `stores.py:97,384` call `msgpack.unpackb(..., raw=False)`; the C extension has an explicit 1024 stack, so 256-bounded payloads fit; the pure-Python fallback is recursive.
- Whether a typed (class-mapped) TS/Py/C# decode layer above these raw decoders recurses per type was not inspected in this pass.
# Device capture runners: how established projects capture real-GPU results, and what a browser driver can silently get wrong
For `cultmath-tapes:question:device-run-driver` (campaign `cultmath-tapes`) and follow-up `cultmath-tapes:follow_up:glsl-browser-parity-run`.
Gathered by eyes-ag3-device-driver, session self-2026-10-15-ag3, 2026-10-16. Facts only; no recommendation.
Strength marks: [src] read from a primary source text this pass; [doc] vendor or standards documentation read this pass; [web] search-result or fetch-summary of a page (a small model summarised it; check before quoting); [abs] searched, not found.
Chromium doc pages were read at `main` or at an old pinned revision as noted; Chromium GPU bot details drift, so the dates matter.
## 0. What the question asks (the record)
- [src] Question `device-run-driver` (Imagination, 2026-10-16): a static page (`packages/cultmath/tools/device-runner`) compiles each family's call against `CultMath.glsl`, reads back through an RGBA32UI target, emits raw bits; C# judges. Options: `committed-captures` (page + stdlib Python server, any WebGL2 browser is a driver, captures committed with renderer, revision, shader sha, args hash, result bits, judged by `DeviceCaptureTests` on Linux), `playwright-live` (Microsoft.Playwright test dependency, Windows-only, ANGLE D3D11, must refuse SwiftShader), `both`.
- [src] Prior precedent named in the question: three Soul/Hands passes through the desktop app's built-in browser on Starfire, renderer `ANGLE (NVIDIA GeForce GTX 1070 Direct3D11)`.
- [src] Follow-up `glsl-browser-parity-run` records both GLSL files probed on a GTX 1070 (phacelle within 3.1e-6 of max(|v|,1), frustum check 0/256).
## 1. How established projects capture real-GPU results for CI
| Project | What runs where | What is stored, and where | What identifies the device | Source |
|---|---|---|---|---|
| Khronos WebGL conformance suite | Static HTML pages. Open `webgl-conformance-tests.html` in the browser under test (`?run=1` auto-starts), then "display text summary". Run by hand in any browser. | Formal results go as a pull request to the `KhronosGroup/WebGLConformanceSubmissions` repo. Older README copies said email `webgl_conformance_submissions@khronos.org`. Only WebGL implementors submit. | README instruction: "Verify that the User Agent and WebGL renderer strings identify your browser and target correctly." The summary text carries them. | [src] raw.githubusercontent.com/KhronosGroup/WebGL/main/sdk/tests/README.md (read 2026-10-16); [web] search summary of CONFORMANCE_RULES.txt |
| Khronos WebGL conformance rules | Results carry forward: "results submitted for an earlier version of the browser carry forward to later versions ... that do not cause any previously passing test to fail"; for OS versions likewise (a new OS version needs a new submission). | The carry-forward rule is the only staleness rule found. | | [web] search summary of CONFORMANCE_RULES.txt, not read in full |
| Vulkan CTS conformance submission | Run on the vendor's driver builds, every fraction. | Submission package must contain full `TestResults.qpa` logs per driver build and fraction, `git status` and `git log --first-parent <release tag>^..HEAD` output of a clean CTS checkout, `git format-patch` output of any CTS changes over the release tag, and a statement file with a `CONFORM_VERSION` line carrying the CTS git tag, plus PRODUCT, CPU, OS. `verify_submission.py` checks the package against the current mustpass list. | PRODUCT/CPU/OS lines in the statement. | [web] search summaries of `external/vulkancts/README.md` in Fuchsia/Android/Chromium mirrors (mirror copies differ by version) |
| Chromium GPU bots | Physical machines in Swarming; most tests via Telemetry launching the full browser; `webgl_conformance_tests` and pixel tests. `chromium.gpu.fyi` tests top-of-tree ANGLE. | Expected outcomes in per-test expectation files (`pixel_expectations.txt` under `content/test/gpu/gpu_tests/test_expectations/`, `Failure` entries). Pixel images are not in the repo (Skia Gold, below). Some pixel tests check only a few pixels with handwritten code so one check serves all vendors. | Swarming task dimensions: `gpu` = PCI vendor:device-driver version (example `10de:2184-440.100`), plus `os`, `cpu`, `pool`. The page does not describe a runtime renderer-string check. | [src] chromium.googlesource.com/chromium/src/+/main/docs/gpu/gpu_testing.md (read 2026-10-16) |
| Skia Gold (Chromium pixel tests) | goldctl client sends the produced image, the test name and hardware/software config metadata to a service. | Approved images live in a service (storage bucket + triage UI), not git. A test passes if the hash of the produced image is in that test's approved-hash list; default exact matching, multiple baselines per test. An unknown hash fails until a human triages it. Doc says local runs "differ slightly from bot images" and uploads need allow-listing; `--local-pixel-tests` disables upload. | Config metadata uploaded with each image. | [web] search summary of `docs/gpu/gpu_pixel_testing_with_gold.md` (pinned old revisions); [src] gpu_testing.md for the local-run flags |
| WebGPU CTS in Chromium | gpuweb/cts compiled to JS, run through WPT and Telemetry; Telemetry covers non-virtual devices with real GPUs. A pinned revision is auto-rolled. | Expectations per platform: Fail/Skip may be any CTS query; Slow/Crash/Timeout must list parameters in loader order. | | [web] search summaries of Chromium `web_tests/webgpu/README.md` and a 2022 W3C public-gpu post by Kai Ninomiya (not read in full) |
| Dawn node runner | `dawn.node` binding runs the CTS without a browser; `run-cts` can instead drive a Chrome instance. | `--output <path>` writes a results file, `--expect <path>` diffs a later run against it. Dawn keeps `webgpu-cts/expectations.txt`, pruned when tests start passing, with wide `Failure` wildcards for flaky ones. | | [web] search summaries of Dawn `src/dawn/node/README.md` and commit `f78edf56` ("Expand CTS expectations for Win11 QC CI bot") |
| three.js | `test/e2e/puppeteer.js` screenshots almost all examples and compares to stored references; helpers `deterministic-injection.js`, `clean-page.js`, `check-coverage.js` exist in an older release's `test/e2e`. | Reference screenshots in the repo. | A 2020 issue: screenshots were black on macOS with the GPU-path flags; a maintainer floated using SwiftShader everywhere "to avoid GPU-specific bugs". Current flags and CI setup not confirmed. | [web] search summaries of discourse.threejs.org and a lightrun mirror of the GitHub issue; [abs] current `puppeteer.js` flags not read |
| Babylon.js / Babylon Native | Visualization tests list scenes in `tests/test/visualization/config.json` (Babylon Native: `Apps\ValidationTests\Scripts\config.json`); each entry names a reference image, optional per-scene pixel-difference threshold. | Reference images in `ReferenceImages/` in the repo; `-u` regenerates. Babylon Native CI fails on too-different images and stores rendered/error images as an artifact. | | [web] search summaries of forum.babylonjs.com and a Babylon Native `ContinuousIntegration.md` fork; [abs] an `errorRatio` key was not found |
| glslang / SPIRV-Cross | No GPU. SPIRV-Cross round-trips `shaders/` through glslangValidator/spirv-as and SPIRV-Cross, diffs output text against committed `reference/` files; `test_shaders.py` exits non-zero on difference, `--update` regenerates references that then ride in the PR; says to use the right glslang and SPIRV-Tools versions. Only hardware option: `--malisc` static cycle counts. | Reference text in the repo. | | [src] raw.githubusercontent.com/KhronosGroup/SPIRV-Cross/main/README.md (read 2026-10-16); [abs] glslang's own test practice not read |
| dEQP GLES3 builtin precision tests | Random inputs through built-ins in a shader; outputs via transform feedback or framebuffer pixel; compared to a reference interval, not a single value. | Spec text of tolerances (section 4). | | [src] doc/testspecs/GLES3/functional.shaders.builtin_functions.precision.txt (Fuchsia mirror, read 2026-10-16) |
Observations across the rows (no ranking):
- [src/web] Where real-GPU outputs are exact or large, the approved results are stored outside a source repo (Gold, Swarming expectations) or as submission packages (WebGL, Vulkan CTS). Where outputs are text from a deterministic tool (SPIRV-Cross) the reference is committed beside the source and `--update` regenerates it.
- [web] Only the Vulkan CTS submission package was found to record the exact source revision (tag plus `git log`) inside the result bundle; WebGL's rule is the carry-forward rule above. No project was found that refuses a committed capture automatically because a shader source hash changed. [abs]
- [src] Chromium's GPU doc keys a bot's hardware by PCI id and driver version rather than by a renderer string read inside the page.
## 2. Headless browser drivers on real GPUs
### 2.1 Fallback to a software renderer
- [src] Chromium SwiftShader doc (read at `refs/heads/main`): flags `--use-gl=angle --use-angle=swiftshader` (SwANGLE as the ES driver); `--use-gl=angle --use-angle=swiftshader-webgl --enable-unsafe-swiftshader` (WebGL fallback). "Automatic fallback to SwiftShader-backed WebGL is deprecated ... WebGL context creation will soon fail instead of falling back." `--enable-unsafe-swiftshader` opts in and suppresses the console warning. The page does not describe how to verify the renderer.
- [web] Blink-dev "Intent to Remove: SwiftShader Fallback": deprecation period began Chrome 130; estimated desktop ship Chrome 137; a later reply from Rick Byers says API owners consider it high-risk and may phase it, so the milestone was unconfirmed in the thread; Finch feature `AllowSwiftShaderFallback`; headless SwiftShader use "will still be supported by opting in"; desktop only. Sources disagree on the shipped milestone (a Winaero Chrome 137 write-up says WARP replaces it on Windows, which the blink-dev page does not mention; Samsung's browser policy page says M139). Not resolved. [web]
- [src] Chrome developer blog "Supercharge Web AI model testing": "By default, Headless Chrome disables GPU." `chrome://gpu` showed "Software only, hardware acceleration unavailable" until drivers were fixed; "Some combinations of flags allow detection of the GPU, but don't allow you to actually use the GPU." Failing renderer string: `ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver-5.0.0)`; working: `ANGLE (NVIDIA Corporation, Tesla T4/PCIe/SSE2, OpenGL ES 3.2 NVIDIA 525.105.17)`. Linux flags there: `--headless=new --use-angle=vulkan --enable-features=Vulkan --disable-vulkan-surface --enable-unsafe-webgpu`. Setup was Colab, not CI.
- [web] A Playwright-on-GPU-runner write-up (createit.com) reported renderer ANGLE-on-Vulkan with SwiftShader; headed Chromium under a virtual display used the GPU; a Vulkan flag set later gave `llvmpipe`, still software.
- [src] Chromium GPU testing doc: example bot flags `--use-gl=angle --use-angle=gl --use-cmd-decoder=passthrough --force_high_performance_gpu`, passed via `--extra-browser-args`.
- [src] Playwright `chromiumSwitches.ts` at `main` (read 2026-10-16) contains no GPU, SwiftShader, ANGLE or headless switch. [abs] What the downloaded headless shell binary itself defaults to was not found.
- [abs] No source was found on `--use-angle=d3d11` combined with headless Playwright on Windows, and none on the renderer string a Windows GPU reports under headless.
- [web] `--disable-gpu` is reported by one source to force software rendering.
### 2.2 Detecting a fallback from inside the page
- [doc] `WEBGL_debug_renderer_info` exposes `UNMASKED_RENDERER_WEBGL` through `gl.getExtension(...)` then `gl.getParameter(...)`; MDN says it may be limited to privileged contexts depending on privacy settings, and Firefox disables it when `privacy.resistFingerprinting` is true, so `getExtension` can return null (MDN, via search result).
- [doc] `failIfMajorPerformanceCaveat: true` makes `getContext` return null if the browser would use a dramatically slow implementation (MDN, via search result). [web, unconfirmed here] whether Chrome treats SwiftShader as such a caveat was not found in a source this pass.
- [src] Strings observed in sources: SwiftShader `ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) ...))` and `Google SwiftShader`; hardware `ANGLE (NVIDIA ..., Direct3D11)` form as in the question's precedent.
- [src] Chromium's own bots do not read a string; they identify hardware by Swarming dimensions (section 1).
- [web] `WebGLShaderPrecisionFormat` from `getShaderPrecisionFormat(FRAGMENT_SHADER, HIGH_FLOAT)`: `precision` 23 means real highp float; it reports mantissa bits, nothing about subnormal handling; Safari mis-reported it as of April 2020 (WebKit bug via webglfundamentals).
- [web] A sample WebGL report line: `ANGLE (Intel(R) HD Graphics 4600 Direct3D11 vs_5_0 ps_5_0)`, highp/highp, precision 23.
### 2.3 Windows service and session 0
- [web] Microsoft XPDM-vs-WDDM table: on WDDM, Session 0 isolation means a Direct3D 9 HAL device is never available from a service (NULLREF, HAL, REF "No"; WARP10 "Yes").
- [web] Microsoft support article 978635: starting with Windows 8.1 / Server 2012 R2, Direct3D 11 supports use from a Windows service. WARP is recommended by Microsoft for service/server environments.
- [web] VTK issue 18657 (GitLab): swap-chain creation failed on Windows CI runners because the process ran in Session 0, which has no graphics hardware access; runners were moved from system services to scheduled tasks.
- [abs] No source found on headless Chrome or ANGLE behaviour specifically inside a service. The pattern the sources support is an interactive user session or a scheduled task that runs only when the user is logged on.
## 3. Committed-capture designs: versioning against source hashes and refusing stale captures
- [src] SPIRV-Cross: references are text regenerated by the tool; staleness is detected by re-running the tool and diffing, not by a stored hash. The tool versions (glslang, SPIRV-Tools) are named in the README as the ones to use.
- [web] Skia Gold: the approved set is keyed by test name plus image hash; an unknown hash fails until triaged. Config metadata is uploaded with the image. No source-revision binding was found.
- [web] Vulkan CTS: the result package carries the CTS tag, `git log` and patches; a checker script validates the package against the mustpass list for the version.
- [web] WebGL: carry-forward rule (earlier passing results hold for later browser versions that fail no previously passing test).
- [web] Babylon / three.js: reference image names live in a config list; regeneration is a flag (`-u`); a missing reference is an error (a 404 in a Karma run, Babylon forum).
- [src] Chromium expectation files: per-test `Failure` entries are removed once a test passes (gpu_testing.md); Dawn prunes `expectations.txt` the same way.
- [abs] No project was found whose committed device capture embeds a hash of the shader source it ran and is rejected by CI when that hash no longer matches the checked-in source. (Search was for the pattern, not an exhaustive survey of repositories.)
## 4. Float behaviour that bears on judging captures across ANGLE/D3D11, native GL and mobile GPUs
### 4.1 What the standards guarantee
- [doc] Qualifiers are minimums. highp float "is at least a 32 bit value", range about +-2^62 in the WebGL2Fundamentals table (exponents stripped in extraction, inferred); mediump at least 16 bit, range +-2^14, relative precision 2^-10; lowp at least 9 bit, range +-2 (webgl2fundamentals precision lesson; asawicki.info April 2015 for the 2^-10 and 2^-8 figures). On a desktop GPU mediump "will still most likely use 32bits internally"; the lesson's author's Pixel 2 XL used 16 bit for mediump and lowp.
- [doc] Highp in fragment shaders is optional in OpenGL ES 2.0 (a shader declaring it fails to compile where unsupported). [web] GL_ARB_shader_precision text: internal processing need not match IEEE 754; rounding mode cannot be set and is undefined; "any denormalized value input into a shader or potentially generated by an operation in a shader can be flushed to 0."
- [src] dEQP GLES3 precision spec tolerances (ULP unless noted): `+ - *` under 1 ULP; `/` 2.5 ULP for 2^-126 <= |y| <= 2^127-1; sin/cos 2^-11 abs on [-pi, pi]; asin/acos 4; atan2 6; atan 5; exp 3+2|x| highp; inversesqrt 2; log 3 ULP highp outside the central range; exact (0 ULP) abs, sign, floor, trunc, roundEven, ceil, min, max, clamp, step; derived functions (tan, pow, sqrt, mix, smoothstep, fract, mod ...) judged by their expansions. The page does not mention flush to zero. Lowp and mediump results on overflow "must produce an infinity or the max/min value".
- [web] Olli Etuaho on the WebGL list, 14 Nov 2014: proposes testing only at the spec minimums, with subnormals flushed to zero, since code that works there will likely work with more bits; concedes not simulating built-in internals is a gap.
### 4.2 Direct3D 11 (the ANGLE/D3D11 path)
- [doc] Microsoft Learn "Floating-point rules (Direct3D 11)" (updated 2025-04-15), 32-bit rules: "Denorms are flushed to sign-preserved zero on input and output of any floating-point mathematical operation" (exceptions: I/O or data movement that does not manipulate the data). Add/sub/mul within 0.5 ULP, and the page says hardware may truncate; sqrt and rcp 1 ULP; rcp and rsq have a separate relaxed requirement; "Fused operations (such as mad, dp3) produce results that are no less accurate than the worst possible serial ordering of evaluation of the unfused expansion", each unfused step allowed 1 ULP (D3D10 page: 1 ULP for mul/div). NaN bit patterns are not preserved. min/max flush denorms for comparison but the result "may or may not be denorm flushed". Doubles preserve denorms; 16-bit floats preserve denorms.
- [abs] No source found that states how ANGLE's D3D11 translation, fxc/dxc flags, or NVIDIA's D3D11 driver treat denormals or contraction beyond the D3D rules above.
- [web] ANGLE's `FeaturesD3D.h` workaround list contains HLSL-compiler-specific rewrites (for example expanding `pow` into multiplies on an affected compiler, per search summary); a Wine D3D11 trig test failed on one NVIDIA configuration with roughly 0.1% relative error, not on AMD (Wine bug thread, cause not identified).
- [web] ANGLE D3D11 vs D3D9 rendering differences are reported in Mozilla bug 1122912 (unresolved in the excerpt).
### 4.3 Mobile and other native GPUs
- [web] ARM blog "Benchmarking floating point precision in mobile GPUs, Part III": Vivante GC4000 and Imagination SGX 554 "do not support subnormal values" (below 2^-126 treated as zero); Mali-T604 supports subnormals, and the authors say Mali Midgard is the only mobile family they know to; the measuring shader divides by 2 per exponent step and multiplies back, drawing underflow pixels red. No ULP measurements in that part; Part II (not read) is said to find GPUs taking "the easy (round-to-zero) way out".
- [web] asawicki.info: `length`, `normalize`, `distance` and dot products in a `mediump` shader can overflow at (74,74,74) because 3*74^2 = 16428 exceeds 2^14; a GPU may pass because it uses more precision than required.
- [abs] No source read this pass on Adreno, Apple, current Mali or PowerVR denormal and contraction behaviour; the ARM post covers 2013-era parts.
- [abs] FMA contraction in GLSL: no WebGL or GLSL ES source was read here on whether `a*b+c` may be fused (see `docs/research/cycles-fma-contraction.md` for the Cycles CPU case and the D3D "mad" fused-op tolerance above).
## 5. Gaps left by this pass
- Current three.js `test/e2e/puppeteer.js` flags and Babylon `config.json` tolerance keys were not read from the repositories (search summaries only).
- Gold, WebGPU CTS and Dawn node pages are search summaries, not full reads.
- Whether Chrome's shipped (not 130-era) behaviour fails WebGL context creation when no GPU is usable, versus using WARP on Windows, is unresolved between the sources in section 2.1.
- Nothing here was measured on Starfire; no run of any driver was made.
# Prior art: a header flag one writer sets and content another writer cannot see
Serves: `variants:question:non-csharp-writers-mark-ids` (raised in
`variants:follow_up:foreign-ids-under-v1-header`, campaign `variants`). Gathered by
Imagination, session self-2026-10-14b-eureka, 2026-10-14. Facts with pointers; the
recommendation lives in the question, not here.
The CultLib case: the store header (`cultcache.store.v1` / `v3`) says whether any record
holds an element id, so a reader older than ids refuses the store instead of rewriting
elements without them. An element id is an ordinary keyed string in the payload, so a
runtime that does not know which member is the id (cultcache-py, -ts, -rs today) cannot
tell from content whether to mark. The persistence contract
(`src/GameCult.Caching/Contracts/cultcache-persistence-format.md:424-427` at 6afa8ed9)
says those runtimes keep a `v3` header they found and do not decide.
## How established formats split the job
| System | Who sets the flag | What an implementation that does not understand the feature does | Pointer |
|---|---|---|---|
| ext4 superblock | The code that first uses a feature sets its bit in `s_feature_compat`, `_ro_compat` or `_incompat`. | Unknown `incompat` bit: refuse to mount (fsck stops too). Unknown `ro_compat` bit: mount read-only, never write. Unknown `compat` bit: read and write freely; fsck should not modify. | https://www.kernel.org/doc/html/latest/filesystems/ext4/super.html (feature fields) |
| SQLite file header | The writer's journal mode sets the write/read version bytes (offsets 18, 19); the schema format number (offset 44) is set by the writer of the newer schema features. | Read version 1-2 with write version > 2: open read-only. Read version > 2: neither read nor write. | https://www.sqlite.org/fileformat.html section 1.3.3, 1.3.10 |
| git repository format | A repository declares `core.repositoryformatversion = 1` and the `extensions.*` it uses; the tool enabling a feature writes its extension key. | Any unimplemented `extensions.*` key in a v1 repo: the operation must not proceed (read or write). | https://git-scm.com/docs/repository-version |
| Protocol Buffers | No flag. New fields are self-delimited by tag. | Parsers keep unknown fields and write them back (proto2 always; proto3 again since 3.5), so an old reader does not drop a newer writer's data on rewrite. | https://protobuf.dev/programming-guides/proto3/#unknowns |
| Avro container files | The writer embeds its full schema in the file header. | Reader resolves writer schema against its own; a writer field the reader lacks is ignored, so it is lost if the reader rewrites. | https://avro.apache.org/docs/current/specification/ ("Schema Resolution") |
| Parquet | Writer stamps `created_by` (application and version) in FileMetaData. | Readers keep a list of known-bad writer versions and distrust their metadata (parquet-mr ignores min/max statistics from writers before 1.8.0, PARQUET-251). | https://github.com/apache/parquet-format/blob/master/src/main/thrift/parquet.thrift (`created_by`); https://issues.apache.org/jira/browse/PARQUET-251 |
## Patterns, as facts
- Flag-based formats (ext4, SQLite, git) put the duty to set the flag on the writer that
introduces the content. None of them has a writer that carries feature content while
not knowing it is that feature: an implementation that does not understand a feature
either does not write (ro_compat, SQLite read-only) or does not touch the store (git).
- Formats without a flag (protobuf, Avro) handle the older reader by preservation
(protobuf keeps unknown fields) or accept loss (Avro drops unknown writer fields on
rewrite). CultCache's C# 1.x readers skip unknown keyed slots and do not preserve
them, which is why the `v3` header exists.
- Parquet's `created_by` is a third option: readers judge content by who wrote it and
compensate for writers known to get a flag or statistic wrong.
- No system surveyed asks a reader to infer a feature flag from content it cannot
recognise; where content cannot be recognised, the burden is on writers.
## Mechanism facts from this pass (probes at 6afa8ed9 on Yggdrasil)
- A C# cache that pulls the Python-written `py-store/leaf-record.cc` (v1, element ids
`000000000021/22`) holds that record with `HoldsIds = true`, `IdsInMemoryOnly = false`;
a C# write that adds an unrelated record copies it forward and leaves the header `v1`.
The ids survive in C#; a pre-id reader with a type lacking the id slot would drop them.
- cultcache-py, -ts and -rs read `v3` and keep it on rewrite; none has a declaration of
Gathered by Eyes, session self-2026-10-14b-eureka, 2026-10-14. No recommendation. Committed by Self before the fork was put.
Method: primary PDFs were downloaded to the session scratchpad and read with pdftotext ("read"). "[secondary]" marks facts taken from a web-search summary, forum or blog, not read at the primary source. "[not verified]" marks things looked for and not reached.
## 1. How shipped volumetric clouds move, and what breaks
### 1.1 Horizon Zero Dawn, Schneider and Vos, SIGGRAPH 2015
- Density = low-frequency 3D Perlin-Worley base shape times height gradient and coverage, eroded by a high-frequency 3D Worley texture (slides ~34-37).
- A 128^2, 3-channel 2D curl noise texture "is non divergent and is used to fake fluid motion"; it distorts the cloud shapes and distorts the sample position of the second (detail) 3D noise "to fake the swirly distortions from atmospheric turbulence" (slides 33, 37). The curl texture is a static distortion of the detail lookup, not a time-integrated flow.
- Summary slide (48): noises are custom, "made from Perlin, Worley and Curl noise", and "all animated in a given wind direction". Wind direction varies over altitude (slide 21). High cirrus/alto layers are 2D textures "tiling and scrolling at different speeds and directions" (slide 87).
- Large-scale evolution is not noise motion: coverage, precipitation and cloud type come from a 2D weather map driven by a simulation, with a wind-speed control (slides 39-44).
- Reprojection (slides 92-93): quarter-res buffer updates 1 of 16 pixels per 4x4 block each frame; "We reproject the previous frame to ensure we have something persistent"; where reprojection fails (screen edges) the low-res result is substituted. Stated as making the shader "10x faster or more"; raw cost ~20 ms, target ~2 ms.
- Slide "Skies / Vertical Profile Model / Animating Density": the motion law for sky density is a sample-position offset, `noise_sample_position = sample_position - wind_direction * scroll_offset` (comment "Deform sample coordinates").
- Slide family "VFX / Animating Superstorm Density / The Mesocyclone": each ring samples noise at `GetRotatedPosition(sample_position, superstorm_center, time_offset * RingRotationSpeed[n])`, later with a per-ring `ring_skew[n]`: different rotation rates per layer about one center, no flow field.
- Slide "VFX / Animating Cloud Density For Skies" (code listing): the reprojection pass reads a stored per-pixel `cloud_distance` from a "CloudAttrWorkingBuffer", reconstructs `cloud_world_space = invView * (eyeRay * cloud_distance)`, applies the SAME motion law to that world position (rotate about the storm center blended by `superstorm_mask`; scroll `cloud_world_space + scroll_direction_2D * deltaTime`), then projects it with `inReprojectionMatrix` to get the previous-frame UV. The history lookup is displaced by the density's own motion, using one depth per pixel written by the march.
- The 2022 and 2023 decks show an "Aero Cloud Solver" frame (Apply Buoyant Force, Advect Density, Advect Velocity, Non-Divergence, Sourcing) among the history slides, as the offline VFX approach.
- A comparison table lists "Evolution: Yes" for the Vertical Profile method and "Pseudomotion Only" for the Envelope method, and "Pseudomotion Only" in later tables for the voxel methods.
- March is SDF-accelerated: "Hybrid SDF, Adaptive and Jittered samples"; jitter is `animated_hash` within 250 m of camera, `static_hash` beyond.
- No temporal reprojection slide found in the extracted text.
### 1.4 Frostbite, Hillaire, SIGGRAPH 2016
Source: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016_pbs_frostbite_sky_clouds.pdf (read; section 5.5.3 and the cloud intro)
- 5.5.3: clouds rendered in a single pass; each frame samples are randomly offset within their step range; current frame blended with previous "according to a constant blend factor" (EMA); "For the final frame to not look blurry when the camera is moving/rotating very fast, we re-project the previous result according to previous and current camera properties". Camera-only reprojection; no density-motion term stated.
- Text cites Guerette's visual-flow technique to give a panoramic cloud texture the illusion of motion along the wind direction, noting it has no variation of cloud shape, weather or lighting.
- Frostbite volumetric fog temporal blend, as reproduced by Flax Engine: Halton jitter, 7% current / rest history (https://flaxengine.com/?p=941) [secondary]. Godot docs: higher blend "makes ghosting much worse", moving fog volumes and lights leave a trail (https://docs.godotengine.org/en/stable/tutorials/3d/volumetric_fog.html) [secondary].
- Hillaire 2015 (Frostbite unified volumetrics) temporal section: [not verified] (158 MB PPTX; SlideShare copy cuts off before it).
- Calls the Horizon 1/16-pixel scheme "a fairly complex technique which may introduce visual artifacts as a result of the 4x4 pattern by which pixels are updated".
- Alternative: far fewer march steps + random per-pixel start offset + analytic integration per step + Playdead-style TAA (Pedersen, INSIDE, 2016) on a low-res buffer. Timings, 1080p GTX 1080 (Fig. 3): half res 128 steps 128.0 ms; half res 8 steps jitter+TAA 7.5 ms; quarter res 8 steps jitter+TAA 2.4 ms.
### 1.6 UE Volumetric Clouds
- Official component docs list View/Reflection/Shadow sample-count scales and a transmittance stop threshold; nothing on temporal behavior: https://dev.epicgames.com/documentation/unreal-engine/volumetric-cloud-component-properties-in-unreal-engine [secondary].
- Forum: Mode 2 traces 1 pixel per 4x4 block and reprojects 15, "lots of latency", cloud "would leak after reprojection over opaque meshes": https://forums.unrealengine.com/t/volumetriccloud/688516 [secondary].
- Forum hypothesis (unconfirmed): a VolumetricCloud writes neither depth nor velocity, so TSR has nothing to reject history on, causing trailing and banding inside a cloud: https://forums.unrealengine.com/t/tsr-ghosting-against-a-volumetriccloud-the-player-stands-inside-every-rejection-knob-measured-none-help/2837437 [secondary].
- Bug UE-198490 (fixed 5.4): horizontal flicker lines from the temporal component; workaround `r.VolumetricRenderTarget.Mode=1`: https://issues.unrealengine.com/issue/UE-198490 [secondary].
### 1.7 Red Dead Redemption 2 clouds (Bauer, SIGGRAPH 2019)
Talk exists ("Creating the Atmospheric World of Red Dead Redemption 2: A Complete and Integrated Solution", Advances in Real-Time Rendering in Games 2019). Slides [not verified]: not among the PDF/PPTX links on https://www.advances.realtimerendering.com/s2019/index.htm; a guessed URL returned 404. Nothing about its motion or reprojection was read.
### 1.8 Shadertoy-scale nebulae
[not verified]. No Shadertoy source was read in this pass.
### 1.9 What the sources say breaks
- Pulsing and repetition: Vlachos names these as the two problems of two-phase flow-map animation (3.1).
- Stretching: a texture advected by a non-uniform flow stretches without bound unless regenerated (Neyret, 3.3).
- 4x4 update-pattern artifacts (Toft et al., 1.5); latency and leaking over opaque geometry in 1/16 modes (UE forum, 1.6).
- Camera-only reprojection fails when content moves: https://www.wayline.io/blog/altos-1-1-better-temporal-reprojection-for-volumetric-clouds-in-unity ("When the player moves their camera, the history becomes useless") [secondary]. A hobby engine skips a velocity buffer "since the clouds move slowly", uses bilinear history sampling and EMA: https://handmade.network/p/75/monter/blog/p/7201-engine_optimization__fast_high-quality_cloudscape_through_noise_and_reprojection [secondary].
## 2. Temporal reprojection of raymarched volumes whose density moves
- A volume has no single surface depth. Nubis 2022 writes a per-pixel `cloud_distance` during the march, rebuilds a world point from it, applies the density motion law to that point, then projects with the previous view-projection (1.2). This is the only shipped density-motion-aware reprojection found at a primary source.
- Horizon 2015 and Frostbite 2016 reproject with camera properties only (1.1, 1.4).
- AtmoPlugin (hobby project, GitHub PR #15): raymarch writes a "contribution-weighted" reprojection depth into alpha, taken where opacity exceeds 5%, with fallbacks surface depth, scene depth, mid-atmosphere, infinity; temporal pass reprojects the pixel center with previous camera and planet transforms: https://github.com/Upa2sdo2l/AtmoPlugin/pull/15 [secondary].
- Two hybrid depth-of-field papers reproject semi-transparent regions by average world position of far-field hits, with a 3x3 neighborhood fallback when there is no hit, and lerp by latest hit ratio during motion to reduce ghosting: https://arxiv.org/pdf/2210.04981 and https://arxiv.org/pdf/2210.06158 [secondary].
- A search for a published "transmittance-weighted depth" reprojection for clouds returned nothing; it is not an established term in anything reached.
- Reported costs and artefacts of history: higher history weight = more ghosting for moving volumes (Godot, 1.4); EMA "stable in static scenes but performs poorly under dynamic volumetric lighting", and variance clipping plus low-discrepancy jitter tested on froxel data, implemented in Frostbite (a thesis at https://gupea.ub.gu.se/items/e05613c2-e1d4-48b0-b41e-c32f4b0ecbae, blocked by bot protection; only a search summary was seen) [secondary]; UE TSR trailing when no depth/velocity is written (1.6).
- Playdead's TAA (Pedersen, GDC 2016) is the history-reprojection reference cited by both Frostbite and Toft et al.
- Builds on Max and Becker 1995 "Flow visualization using moving textures": offset the UV by the 2D flow vector scaled by time; "distortion looks reasonable for the first 1/3 of uv space".
- "Blend the short animated segment in two layers. Each layer is offset half a phase so we can hide the restart for each layer."
- Two named problems: repetition ("the same normals will flow through the same point") and pulsing ("the surface appears to pulse in a repeating pattern"). Repetition solved by offsetting the second layer's sample; pulsing solved by a noise texture that offsets phase per pixel. Cost vs two scrolling normal maps: +2 texture fetches, +21 ALU instructions.
- Colors (Portal 2): distortion interval offset to -fraction..+fraction so the peak is at rest, favouring less distortion than for normals. Listed future work: dynamic per-frame flow vectors.
- Catlike Coding (https://catlikecoding.com/unity/tutorials/flow/texture-distortion/) [secondary]: p=frac(time), weight `1 - |1 - 2p|`, second pattern shifted half a period so weights sum to 1; Perlin noise in the flow map's A channel added to time staggers the pulse as a wave; per-phase "jump" vector times floor(time) shifts UVs only while weight is zero, limited to about +/-0.25, loop length is LCM of jump denominators; strong deformation makes compressed flow maps blocky; large-area flow maps have low effective resolution.
- Golus flow-map notes: [not verified].
### 3.2 Curl noise, Bridson, Hourihan, Nordenstam, TOG 26(3) 2007
- Velocity = curl of a noise-valued potential (2D scalar potential rotated 90 degrees; 3D three noise functions, "the same noise function evaluated at large offsets"). Divergence-free by construction: "No sources or sinks ('gutters')"; contrasted with plain noise as velocity, which "will generally contain many sinks" where particles accumulate.
- Derivatives by finite difference (displacement 10^-4 of the domain "works fine in single precision"). With noise at length scale L: vortices of diameter about L, speeds O(1/L). Several octaves with power-law amplitude falloff resemble Kolmogorov turbulence.
- Time variation: "achieved very simply by using time-varying noise"; pseudo-advection of Flow Noise mentioned as untried.
- Boundaries: potential multiplied by smooth ramp of distance to solids, `ramp(r) = 15/8 r - 10/8 r^3 + 3/8 r^5`.
- The paper names particle advection as a primary use. It says Perlin and Neyret's Flow Noise textures "cannot be used for moving particle systems".
- Perlin and Neyret, "Flow Noise" (SIGGRAPH 2001 sketch): rotating gradients over time plus pseudo-advection of small octaves by large ones; needs noise with analytic derivatives. https://evasion.inrialpes.fr/~Fabrice.Neyret/flownoise/index.gb.html ; Gustavson's simplex noise code exposes the derivative and a rotation parameter `t` for it (https://www.itn.liu.se/~stegu76/simplexnoise/DSOnoises.html) [secondary].
- A noise texture advected by a flow stretches; periodic regeneration prevents stretch from growing on steady flows, but the regeneration rate (latency) is hard to tune and blending regenerated layers causes ghosting and contrast loss.
- Method: accumulate local deformation (time integral of the strain-tensor norm) per layer since its last regeneration; blend layers of different latency by a criterion on that accumulated deformation against a user target (analogous to MIP-mapping); each layer blends three phase-shifted regenerated textures; proposes a procedural-texture blending scheme that avoids contrast loss.
- Needs a velocity field to compute deformation; the paper's flow came from a low-resolution fluid simulation.
### 3.4 Does a span / Lipschitz bound survive advection or a time-dependent warp
No source found states it. What was read:
- Seyb, Jacobson, Nowrouzezahrai, Jarosz, "Non-linear sphere tracing for rendering deformed signed distance fields", TOG 38(6) 2019 (PDF read): stepping in deformed space needs the Lipschitz constant of S o D^-1; "for affine transformations this is trivial [Stander and Hart 1994], but for more complicated deformations Lipschitz constants can be tedious or even impossible to compute analytically"; a global constant degrades step size where it is large; local constants "require a non-negligible computation whenever the deformation changes, i.e., every frame". Their remedy: trace in undeformed space as an ODE, avoiding the inverse deformation. https://par.nsf.gov/servlets/purl/10172295
- Galin et al., "Segment Tracing Using Local Lipschitz Bounds", CGF 39(2) 2020: local Lipschitz bounds per ray segment, bounds derived per operator and primitive (abstract/search summary only) https://diglib.eg.org/handle/10.1111/cgf13951 [secondary].
- Bridson 2007: speed of a noise-derived velocity is O(1/L) per octave (informal amplitude bound).
- Neyret 2003: the quantity that grows under advection is the time-integrated strain-tensor norm.
- A bound on the time derivative of a warped field, or a space-time Lipschitz constant: [not found].
## 4. Particle layers coherent with a volumetric field
- No shipped system found that drives volumetric density and a particle layer from one stated shared motion law.
- Niagara Curl Noise Force: reads a pre-baked, medium-resolution tiling curl-noise field by default, or computes a Perlin-derived curl at higher cost; docs do not state the divergence-free property: https://dev.epicgames.com/documentation/unreal-engine/particle-spawn-group-reference-for-niagara-effects-in-unreal-engine [secondary].
- Frostbite 2015 couples particles to fog by voxelizing particle extinction into the same froxel volume, so they are lit together (shared volume, not shared velocity): https://www.ea.com/news/physically-based-unified-volumetric-rendering-in-frostbite [secondary].
- Flax: volume particles write into the fog's 3D textures: https://docs.flaxengine.com/manual/graphics/fog-effects/volumetric-fog.html [secondary]. Godot demo of particle-faked fog animated with wind/attractors; "billboards make the fog appear to rotate with the camera": https://github.com/Bauxitedev/volumetric-fog-demo [secondary].
- Bridson's curl noise is built for particle advection and the divergence-free property is its stated reason (3.2).
- Nubis 2022 / Nubis3 slides read carry no particle layer sharing the density motion law.
## 5. Not reached
RDR2 (Bauer 2019) slides; Hillaire 2015 temporal section; Golus flow-map notes; any Shadertoy nebula source; UE internals beyond forum reports; any source bounding the time derivative of a warped noise field.
- The integration-native field places mass from the shipped field's noise-averaged
density.
- Mean window optical depth is 1.62 to 1.74 against the shipped 1.81.
- Mean T is 0.25 to 0.40 against 0.17. The difference comes from intermittency:
the image std is 0.29 against 0.05, at a near-equal mean.
- **msf is normalised by variance.** It measures spatial frequency, not contrast.
- **Single-scattering images, void.** The images are 256 x 128 with a point light at
the sun, and both are tonemapped with the same exposure. The shipped image's msf
is 0.0023, with 0.22% of power above 1/8. The integration-native image's msf is
0.101, with 16.9% of power above 1/8.
- **Wells.**
- Transmittance is a near-opaque silhouette for both fields: equal msf (0.011),
so it carries no signal.
- Single scattering at 6 levels (1.20x the march cost):
- msf is 0.073 against 0.069;
- power above 1/8 is 10.1% against 7.0%;
- the edge fraction is 16.7% against 9.5%.
- At 4 levels the cost is 0.77x the march.
**Exactness.** Against a converged march of the same field at 0.02 world:
- The relative τ error is a median 3e-5 and a maximum of 1.0e-4 on the void, and a
median 1e-5 and a maximum of 1.0e-4 on the wells.
- The binned gather against brute force over all roots differs by at most 1.3e-15.
- The shipped march against its own converged reference: |ΔT| is a median 0.0055
(p90 0.013) on the void and a median 0.0006 (max 0.83 on grazing rays) on the
wells.
**The detail is only reachable by integration.**
- Marching the same integration-native field at the shipped step gives a void |ΔT|
median of 0.14, a p90 of 0.48 and a maximum of 0.83.
- A field this detailed is beyond the march at its step. That is the operator's
premise, measured: the shipped fields' softness fits the regime that renders them.
## The primitive set
The primitive set has one design rule: nonlinearity lives in kernel placement, and
linearity lives in the integration. The density is a sum of nonnegative,
compact-support kernels. Everything nonlinear is evaluated once per kernel, at its
centre, and never per sample. That covers envelopes, clamps, `pow` profiles, the
flow warp and placement.
### Bases (closed-form line integral over a segment)
| Primitive | Why it integrates | Cost per span (measured unless marked) |
|---|---|---|
| Compact polynomial ellipsoid kernel, `(1 − u)^k` with `u = \|A⁻¹(x − μ)\|²` | On a line, u is quadratic in t, so the kernel is a polynomial of degree 2k between the two roots of `u = 1`, which are closed form. Same algebra as the simplex lattice integral in the first probe | k = 0: 5.5 to 7.4 ns; k = 1: 8 to 9.5 ns; k = 2: 11.7 ns. About 0.01 to 0.02 snoise3 (snoise3 is 610 to 650 ns in C#) |
| Truncated Gaussian ellipsoid, `exp(−κu)` on `u ≤ 1` | `exp` of a quadratic is a Gaussian in t, giving an erf difference. Truncation only clips the limits | 32 to 36 ns with A&S 7.1.26 erf (error ≤ 1.5e-7), about 0.05 snoise3 |
| k = 0 ellipsoid (constant density) | Chord length; this is EVER's primitive (Mai et al. 2024 [memory]) | 5.5 ns |
| Gabor kernel (Gaussian × cosine) | Complex erf, any affine transform (Gabor Fields 2026, eq. 9 [fetched in the prior-art file]) | Not probed. Up to 16 Taylor terms [paper] |
| Cosine sum (Balint 2019) | Integral is a sum of sines | Not probed. Two noise-sized evaluations per segment [paper]. Signed, so it needs an offset; low contrast |
| Simplex lattice (CultMath `snoise`) | First probe: degree 9 per lattice point | 0.16 snoise3 per point, 3.44 points per noise unit. Signed: only usable where density is affine in it |
### Warps
| Warp | Why it stays exact | Cost |
|---|---|---|
| Per-kernel affine warp: centre moved by `f(μ)·s`, shape by `(I + J·s)` | An affine image of an ellipsoid is an ellipsoid, so the ray stays straight and the kernel stays closed form. The warp *defines* the field, so there is no approximation error. It is evaluated once per root per frame (7 flow evaluations with the Jacobian) | Frame build 39 ms (void, 2540 roots) and 57 ms (wells, 16947 roots), or 19 and 27 ns per 1080p pixel amortised |
| Children inherit the parent's affine frame | The cascade has descendants that are exact affine images, so the warp is evaluated only at the roots | One 3x3 product per child, from a table |
| Not allowed | A per-point warp `x + w(x)` inside a span: the warped ray curves, which was the first probe's problem | n/a |
### Transfer and placement
| Rule | Status |
|---|---|
| Any function of the kernel centre: the shipped clamp, `pow`, the smoothstep fade, the noise-averaged density `E_n[ρ(x, n)]` from 16 empirical noise quantiles, through a 1D LUT over d (void) or s (wells) | Exact by construction, at one lookup per kernel |
| Affine transfer of the sum (`a + b Σ K_i`) | Exact |
| Any 1D function of an *affine* coordinate, `∫ f(a + bt) dt = ΔF / b` (pre-integration, Engel 2001) | Exact [derivation, not probed]: height fog `exp(−y/h)` and ramps in y |
| Polynomial or exp of a *quadric* coordinate; an indicator or clamp of a quadric (chord through roots) | Exact [derivation, not probed]: spherical shells and paraboloid bowls as envelopes |
| Product of the sum with a polynomial envelope in t | Exact, polynomial times polynomial [derivation, not probed] |
| Forbidden inside a span: `f(Σ K_i)` for nonlinear f, `max`/`min` of fields, a clamp of a signed sum, or noise displacing an envelope's argument | These are exactly what broke the first probe |
### Composition: the mass-conserving multiplicative cascade
- **Roots** sit on a jittered grid: spacing H0 = 30 world on the void and 40 on the
wells, one root per cell.
- Mass is `P(μ)·H0³`, so the mean density equals the shipped noise-averaged
density.
- The ellipsoid is elongated along the flow direction projected on the shell or
fog layer (aspect 1.3 to 2.4), with ±0.6 rad of jitter, and flattened along the
normal (×0.5 void, ×0.4 wells).
- There are two copies per root, at the shipped two-phase shifts with weights w0
and 1 − w0. Time animation is therefore the shipped crossfade, exact under
summation.
- **Children.** Each node keeps 25% of its mass and gives 75% to Nc = 5 children.
- Child weights are `u²·P(child)/P(parent)`, normalised. The `u²` makes the cascade
intermittent; the P ratio sharpens it toward the envelope.
- A child is scale 0.4, with aspect up to 1.6 and ±0.6 rad of rotation.
- Children sit along the parent's major axis: offset 0.6 along it, 0.21 across.
- All of this comes from a 256-entry table indexed by hash. A descendant bound of
1.77 times the ellipsoid holds the subtree, so the parent is a BVH node.
- **LOD.** A node is a leaf when its children would be smaller than 0.5 × the pixel
footprint at that depth. It then keeps its full mass. Because the cascade conserves
mass, truncation is a mean-preserving prefilter: the expectation step of the first
probe, exact here instead of approximate.
- **Gather.**
- Roots are binned by their subtree AABB into a hash grid of H0 cells.
- The ray walks the grid with Amanatides–Woo DDA and mailboxing, clipped to the
shipped active windows, so both fields integrate the same span.
- It exits early once Στ ≥ 7, which is valid because every term is nonnegative.
## "Juice" under these constraints
| Effect | Mechanism here | Measured or prior art |
|---|---|---|
| Filaments, wisps | Anisotropic children chained along the parent's major axis, which follows the flow | Visible in `void-T-IN-L6.png` as streaks along the flow. Gabor Fields uses anisotropic Gabor kernels for orientation-selective detail [fetched] |
| Sharp edges | Low-order kernels: k = 1 is the default, k = 0 gives hard ellipsoids | k = 0 (`L6-hard`) changes msf by −9% on the void. Edges come from kernel boundaries, not clamps |
| Voids | Regions with no kernel mass are exactly zero and are skipped by the gather at no cost | Structural |
| Turbulent intermittency | Random multiplicative cascade, the p-model of turbulent dissipation (Meneveau & Sreenivasan 1987 [memory]); clustered kernels, as in a Neyman–Scott process [memory] | Image std 0.29 against 0.05 at near-equal mean OD |
| Flow and animation | Per-kernel warp at the shipped flow and phases. Alternatives are Lagrangian advection of kernel centres with lifetimes (advected textures, Neyret 2003 [memory]) and Gabor phase for oscillatory detail (flow noise, Perlin & Neyret 2001 [memory]) | Only the two-phase crossfade was measured |
| Fine detail riding on structure | Children exist only inside parents, so detail appears only where matter is. This is Schneider's erosion idea (2015 [fetched]) without the per-sample clamp | Structural |
Wavelet turbulence (Kim et al. 2008 [memory]) and curl noise (Bridson 2007 [memory])
were not used. A per-point warp is ruled out above. Curl noise could orient kernels,
# Inverse procedural modelling: describing messy meshes as programs
Eyes pass, 2026-10-10. Operator: "What if the model had some trash geo and its goal is to describe it parametrically?" Context: Tripo hard-surface hulls; CultMath intervals and affine forms on main, tape/brush grammar parked (docs/cultmath-tape-target.md); the earlier GameCult/VibeGeometry attempt (2026-05). Facts only; tags mark FETCHED, SEARCH (search summary only) and KNOWLEDGE (unverified).
# Facts for `cultmath-tapes:question:lattice-site-weight-source`
Agent eyes-lattice-site-weight, session self-2026-10-14-eureka, 2026-10-14. Facts only, no recommendation.
Marks: [src] source code, [doc] repo doc, [web] web page, [abs] absence found, [probe] measured by the scripts beside this file.
The eureka-state view of the question id failed (`view` wants a PipelineRef, not the id string); the question text was taken from the brief.
## 1. What the fauna probe did (Aetheria 4354ef2c, read via `git show`)
- [src] `docs/research/2026-10-14-trophic-dag-probe/trophic_probe.py``lattice_web` (lines 95-119): per lattice cell `(cx,cz)` in a (2*WIN+1)^2 window (WIN=2): jittered site `(c+.5)*LAT` plus a drifting offset; weight `wt = (hash*.5+.5) * (1 + BOOST*prox)`, `prox = max over wells of exp(-(|site - well| / (2.2*.103*r))^2)`, BOOST 1.6; `d = hypot(...) * wt`; F1, F2 and a polynomial smooth-min `m` accumulated in loop order; output `max(soft^2, exp(-((f2-f1)/.10)^2))`. Wells orbit with zone time, so the site weight is a function of time.
- [src] The weight is read at the site's lattice position `(cx+.5)*LAT`, not at the query point and not at the drifted site.
- [doc] `docs/aetheria-fauna-map.md` B34: largest presence step for `lattice` .124 (8 steps over .1); "not discontinuities: a traced member's web value rises .03 per step for 5 steps, a thin weighted border sweeping a fixed candidate"; window 7x7 changes nothing; boost .6 removes the steps (.047); reading the lattice drift at the member's cycle start removes them at full boost (.011, `cyc.log`).
- [doc] Map r4 gap (1): "a 2D weighted lattice cellular with the smooth-min blend (her node) on `pcg3d` cells and zone time"; "The search window follows the weight range ... The CultMath node states the weight range it supports" (map lines 438, 444-446).
- [doc] Prior art doc `docs/research/2026-10-14-trophic-dag-filaments-prior-art.md` 1.1.7 [M]: power diagram evaluated pointwise by the same neighbour search "only if the search window is widened with the weight range"; marked "Neither derivation checked against a source".
## 2. Literature and implementations
- [web] Wikipedia, Weighted Voronoi diagram (https://en.wikipedia.org/wiki/Weighted_Voronoi_diagram): multiplicative = distance divided by positive site weight (circular Dirichlet tessellation, edges circular arcs and line segments); additive = weight subtracted from distance (hyperbolic Dirichlet tessellation, hyperbola arcs and segments); power diagram = weight subtracted from squared distance. A multiplicatively weighted cell may be non-convex, disconnected or hold holes. The page says nothing on continuity.
- [web] Sangil Lee, "Understanding the Variations of Cellular Noise" (https://sangillee.com/_posts/2025-04-18-Cellular-noises/): weighted Voronoi as effective distance `(1/w)*|x - seed|` with `w = 0.2 + 0.8 * random3(cell).z`, i.e. the per-site weight is a component of the hash of the lattice cell id, computed inside the noise function; the weighted example loops 5x5 instead of 3x3 (no reason given).
- [web] Inigo Quilez, Voronoise (https://iquilezles.org/articles/voronoise/): `hash3(cell id)` yields jitter in `.xy` and a per-cell value in `.z`; 5x5 window; notes `min()` is discontinuous and the smooth blend fixes that. No weighted variant.
- [web] Worley noise, Wikipedia and The Book of Shaders (https://thebookofshaders.com/12/): the standard is current cell plus neighbours (3x3), with a wider pass for exact borders; Worley's original used a variable number of points per cell. Nothing found on per-point weight attributes beyond the Lee article; the continuity guidance in the search results was general-knowledge reasoning, not sourced.
- [abs] No source found stating a continuity theorem for weighted Worley; no source found for a node that takes its per-site weight from an external field function. The only per-site weight mechanism found in shader writing is the hash of the cell id.
- [probe] Continuity as observed (section 4): F1 stays continuous as long as the window contains the true winner; failure occurs when it does not.
## 3. CultMath today
- [src] `F:\Projects\CultLib\packages\cultmath\src\CultMath\math.cs:909``public static CultCellular cellular(float3 p)`: one argument, 3D, unweighted; sites `neighbor + jitter` with jitter from `pcg3d(int3(neighbor))` through `cellular_unit`; fixed 5x5x5 window with a per-offset lower-bound prune (`max(0,|d|-1)`), whose exactness proof (comment above the function) relies on the jitter being in [0,1) and on equal weights; returns `CultCellular { nearest = (grad F1, F1), edge = (grad(F2-F1), F2-F1), id = pcg4d of the winning cell mapped to [0,1) }` (`CultCellular.cs`). Hash is of the integer cell, never the float bits.
- [src] `math.cs:831-847``smin_grad(float4 a, float4 b, float k)`: quadratic smooth-min with analytic gradient (value-and-gradient form).
- [src] `Voronoi.cs` (`Voronoi.SampleTones`, native `cultmath_apollonian_voronoi_tones`) is a UI-tone colour sampler over a hash-jittered field with a hard-coded 5x5 loop; takes x, y, tone, span; not a weighted-cell function.
- [abs] No function in CultMath takes a weight, a callback, or a site span; no weighted cellular, no per-site hash output exposed besides `id`. (Map B28 states the same.) HLSL twin at `shaders/CultMath.hlsl:440-503`.
- [doc] `packages/cultmath/docs/design.md` ~265-340: `cellular` section, search-radius proof and pruned/unpruned test pin.
- Fact for composition: `CultCellular.id` is already a per-cell hash scalar available to the caller after the call, but only for the winning cell, after F1/F2 are decided.
## 4. Probe: where does the weight come from, 2D, 4 orbiting wells
Scripts: `siteweight_probe.py`, `siteweight_probe2.py`; raw numbers `probe_results.json`, `probe_results2.json`. numpy, scratch; hash is a lowbias32-style integer hash (stand-in for pcg3d). Units: lattice cell = 1; domain [1,7]^2, 500x500 grid (600 for shape rows); wells with radii .6-1.2 cells orbiting a centre; weight `wt = base(.5..1) * (1 + B*prox)`, B = 1.6, WIN = 2, the fauna probe's smooth-min and border output.
Correction (2026-10-14, from the Aetheria Self's body-Voronoi probe, Aetheria 05a44541): the fauna probe's `trophic_probe.py` weights at the cell centre `(cx+.5)*LAT` (section 1 above), which is the `cellc` variant below, not `site`. An earlier label here called `site` the fauna probe's method.
Variants: `base` (no field); `site` (field at the jittered site position); `qpt` (field at the query point, same factor on every candidate); `cellc` (node calls the field at the integer cell centre, `cell + .5`, no jitter); `site_t0` (as `site` but field read at t=0 while wells have moved); `warp` (extra: query scaled `q*(1+B*prox(q))`).
Mean absolute difference of the border output vs `site`: base .169/.186, qpt .168/.180, cellc .164/.156, site_t0 0/.234, warp .310/.280. Number of distinct cells on the grid: 53 for base, site and qpt; 54 cellc; 143/128 warp.
[probe] `qpt` leaves every winner unchanged because the factor is common to all candidates; it changes F1, F2 and the smooth-min scale, which moves the border line (border pixel fraction near wells .164 vs .300 for base, far .250 vs .281).
### 4b. Cell size and border near wells (pixels per cell, mean; "near" = within 1 well radius), t=0
| | near | far | border fraction near / far |
|---|---|---|---|
| base | 10951 | 10515 | .300 / .281 |
| site | 9672 | 11439 | .270 / .253 |
| qpt | 10951 | 10515 | .164 / .250 |
| cellc | 11562 | 12274 | .274 / .262 |
| warp | 3447 | 8566 | .291 / .302 |
### 4c. Pop measure, dense spatial line (6001 points over 6 cells, step 1e-3 cell), max |step| of F1 and of output, window sweep vs window 6 reference
- With the true winner inside the window, every variant's max F1 step is small (site .0010-.0033, qpt .0019-.0079, cellc .0009-.0041, warp .0054-.022, base .0007-.0013 per 1e-3).
- WIN=1 (3x3): `site` F1 error vs window 6 up to .174 (diagonal line, B=1.6; max output step .95); `cellc` .155-.194; `base` .0065; `qpt` .0092-.0113; `warp` none seen. At WIN=2 and B=1.6 all variants' F1 error is 0.
- Weight range: `site` at B=6 (weight ratio about 12:1), t=8, horizontal line: WIN=2 error .0169 (output step .371); WIN=3 error 0.
- Note: even `base` (weights .5-1, ratio 2) shows F1 error .0065 at WIN=1.
### 4d. Pop measure, time at fixed query points (wells move), step refinement (worst point from a dt=.01 scan, refined)
| variant | dt .01 max output step | dt 1e-3 | dt 1e-4 | dt 1e-5 | max F1 step at dt 1e-5 |
[probe] All three steps scale linearly with dt: no discontinuity in time for any variant when the window holds the winner. Winner-id changes over t in [0,20] at 4 query points (dt .01): base 0, qpt 0, site 17, cellc 15, site_t0 0.
### 4e. Caveats of the probe
Static smooth proximity field, 4 wells, 2D, one seed of the hash; 3 boost values in the pop table only B=1.6 and 6.0; no gradient-bearing outputs measured; `cellc` field is sampled at the cell centre, so it differs from `site` by the jitter distance; no visual images rendered, only the numeric measures above. The `warp` variant is an addition outside the three asked and is reported for completeness.
print(f"W{W}{fold} B{boost} S{S}: cell-boundary jump out max {j.max():.4f} (>.01: {np.mean(j>.01):.4f}), soft max {js.max():.4f}, F1 max {jf.max():.2e}")
# Shader runner probes: the HLSL-to-GLSL ES toolchain, lavapipe on Yggdrasil, native ANGLE on Starfire
For the cultmath-tapes questions that replace `device-run-driver`: `glsl-es-translation-owner`,
`conforming-shader-run` and `real-device-shader-run`. These are body facts measured by
imagination-ag4-shader-runner (session self-2026-10-16-ag4) on 2026-10-16, between about 14:10 and
15:20 UTC (16:10 to 17:20 CEST). Every probe read CultLib at f697e03f (origin/main). Companion to
`docs/research/native-shader-testing.md` (f697e03f) and `docs/research/device-capture-runners.md` (c9d3e672).
The probe scripts are in this directory under `p/`: `probe.sh` and `lib.sh` (Yggdrasil jobs), `fam.py`
(the per-family shader generator and bit judge), `vkrun.c` (an 87-line Vulkan compute host) and
`angle_native.py` (a 96-line native ANGLE host). They are throwaway probe code, not a proposed
implementation. The logs are `q12-run3.log` (lavapipe) and `lib-run4.log` (library route). The earlier
runs are superseded: run1 used a raw-ulp metric, run2 died on CRLF, and lib-run1 to lib-run3 failed on
the capability and grep problems recorded below.
## Metric
Each fixture family has 256 cases, and the fixture records the C# result bits. "Exact" means all 256
cases are bit-identical to the C# fixture. Otherwise the worst error is given in units of
2^-23 * max(|expected|, 1) (written "u1" below), the scale the fixture's ulp-bounded checks use. NaN and
Inf cases are compared only as NaN-versus-not-NaN. These figures describe the data. They are not the
fixture's own judge, which is C# and per family: GlslMirrorTests tolerances, and the `check` text on 10
families.
## The fixture surface (glsl-parity.json at f697e03f)
- 109 families, each with 256 cases, encoded `args -> result` as hex 32-bit words. There are at most 32
words per case.
- 105 families use only float, int, vecN, ivecN and mat3 (mat3 appears as a return only). The other 4
take or return structs: `CultCellular cultmath_cellular(vec3)`, `CultPhasor cultmath_phacelle(vec3,
vec3, float, float)`, and `SpanNoise cultmath_span_fbm/ridged(SpanPath, SpanStage, int, float, float)`.
Any runner needs per-struct flatteners in field order for these 4.
- Tolerance classes: 89 are `ulp-bounded`, 2 are `exact`, 7 are `platform` (exp, pow, log, asin, acos,
atan, and phacelle's sin and cos), and 10 carry a `check` (enclosure).
- What a harness needs: generate one shader per family that reads the argument words, bit-casts them to
the signature's types, calls the function and writes the result words. The probe generator does this
for both HLSL and GLSL in 112 lines of Python, judge included.
## Q1. Can dxc emit a SPIR-V library that SPIRV-Cross lowers to GLSL ES for CultMath's real surface?
Yggdrasil, ubuntu:24.04. Tools: dxc v1.9.2607 Linux (`linux_dxc_2026_07_29`, the version get-dxc.ps1
pins on Windows), and SPIRV-Cross built from HEAD 300681bb (2026-10-14).
| Step | Result |
|---|---|
| `dxc -T lib_6_6 -spirv -fspv-target-env=vulkan1.2 CultMath.hlsl`, no change to the source | fails: "No OpEntryPoint ... only allowed if the Linkage ... capability is being used" |
| `-default-linkage external` (lib_6_3, universal1.5), no `export` | fails the same way: no exports in SPIR-V |
| `export` added to every top-level `cultmath_` definition, vulkan1.2 | fails: "Capability Linkage is not allowed by Vulkan 1.2" |
| same, `-fspv-target-env=universal1.5` | compiles, but declares 14 capabilities, ray query, interlock, quad control and partitioned subgroups among them |
| SPIRV-Cross `--es --version 310` on that module | fails: "RayQuery requires Vulkan GLSL 460" |
| ray query stripped | fails: "Cannot find mapping for combined sampler parameter". The Texture2D/SamplerState functions were exported |
| `export` only on the 173 non-texture definitions, all capabilities but Shader and Linkage stripped, every OpExtension stripped, then `spirv-as` | assembles; SPIRV-Cross `--es --version 310` exits 0 |
| SPIRV-Cross `--es --version 300` | fails: "At least ESSL 3.10 required for compute shaders" (a library gets a GLCompute placeholder entry) |
| ES 3.10 output, `#version` guarded by `#ifdef SPIRV_CROSS_LIBRARY_HEADER`, placeholder main removed, compiled by glslang as a `#version 300 es` fragment shader | compiles |
Shape of the output against the committed lowering:
| overloads | kept | **46 dropped**: one definition per name survives (for example `cultmath_snoise(vec2)` and `cultmath_iv_snoise_ball(vec2, float)` are gone) |
| SSA temporaries `_NNN` | 0 | 5632 |
| min and max | as written | NaN-guarded `mix(mix(min(a,b),b,isnan(a)),a,isnan(b))` |
In `cultmath_snoise(vec3)` as emitted, the body is one temporary per SSA value (`vec3 _1157 = floor(...)`)
and constants are spelt to 20 digits.
**Answer:** yes, but only with three owned workarounds: an `export` annotation on 173 definitions,
which FXC (Unity's D3D11 compiler) does not accept, so it would have to be a macro; a capability and
extension strip pass; and unique names for 46 overloads, which breaks the public API for both the HLSL
and the GLSL consumers. The output is generated text, not a readable library.
## Q2. Does a conforming Vulkan implementation run the families in a Linux container on Yggdrasil?
'[numthreads(64, 1, 1)] void main(uint3 id : SV_DispatchThreadID) {\n uint i = id.x; if (i >= %du) return; uint b = i * %du; uint o = i * %du;\n%s\n}\n'
url=$(curl -s https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/tags/v1.9.2607 | python3 -c "import json,sys;print([a['browser_download_url'] for a in json.load(sys.stdin)['assets'] if 'linux' in a['name'] and a['name'].endswith('.tar.gz')][0])")
echo"$url"; curl -sL --fail -o dxc.tgz "$url"&& mkdir -p tools/dxc && tar xzf dxc.tgz -C tools/dxc;DXC=$(find tools/dxc -name dxc -type f | head -1);echo"DXC=$DXC";$DXC --version 2>&1| head -2
okl=0;for b in fam/*.body.glsl;dot=${b%.body.glsl};tag=$(basename $t);n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
run $tag g all.glsl;for v in $VS;do[ -s $v.body ]&& run $tag$v$v.body;done;done
export VS; python3 - <<'PY'
import os,filecmp
d='/tmp/w/fam';tags=sorted(f[:-10]for f in os.listdir(d)if f.endswith('.body.glsl'))
def same(a,b): return[t for t in tags if os.path.exists(f'{d}/{t}.{a}.out') and os.path.exists(f'{d}/{t}.{b}.out') and filecmp.cmp(f'{d}/{t}.{a}.out',f'{d}/{t}.{b}.out',shallow=False)]
print('families',len(tags),'hlsl ran',len([t for t in tags if os.path.exists(f'{d}/{t}.h.out')]),'committed glsl ran',len([t for t in tags if os.path.exists(f'{d}/{t}.g.out')]))
for v in os.environ['VS'].split():
ran=[t for t in tags if os.path.exists(f'{d}/{t}.{v}.out')]
sg=same(v,'g'); print(v,'ran',len(ran),'bit-identical to committed glsl',len(sg),'to hlsl',len(same(v,'h')),'| differs from g:',[t for t in ran if t not in sg][:8])
print('committed vs hlsl',len(same('g','h')),'of',len(tags))
PY
for v in v0 b2;do{printf'#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat $v.body;printf'out highp vec4 c;\nvoid main() { vec3 p = gl_FragCoord.xyz; c = vec4(cultmath_lerp(p, p.yzx, vec3(0.5)), cultmath_lerp(1.0, 2.0, p.x)) + cultmath_saturate(vec4(p, 1.0)) + vec4(cultmath_snoise(p)); }\n';} > $v.es300.frag; glslangValidator $v.es300.frag >$v.es300.txt 2>&1;echo"$v ES 3.00 glslang exit $?$(grep -m2 ERROR $v.es300.txt | tr '\n'' '| cut -c1-200)";done
for t in g $VS;doecho"--- cmp vs C# fixture: $t";CMP_TOP=3 python3 $P/fam.py cmp $FX fam $t 2>&1| grep -v "^ *$"| head -8;done
grep -h -m1 ERROR fam/*.r.log | sed -E 's/[0-9]+:/N:/g'| sort | uniq -c | sort -rn | head -5
python3 - <<'PY'
import os,filecmp
d='/tmp/w/fam';tags=sorted(f[:-10]for f in os.listdir(d)if f.endswith('.body.glsl'))
def same(a,b): return[t for t in tags if os.path.exists(f'{d}/{t}.{a}.out') and os.path.exists(f'{d}/{t}.{b}.out') and filecmp.cmp(f'{d}/{t}.{a}.out',f'{d}/{t}.{b}.out',shallow=False)]
print('functions %d, wrappers found %d of %d, missing %d, inlined (no call) %d, not-one-call %d'%(len(fs),len(res)-len(missing),len(res),len(missing),len(inl),len(multi)))
print('distinct callees %d, callee shared by >1 wrapper %d'%(len(callee),sum(1forcincallee.values()ifc>1)))
cmp -s e.drop.glsl e.export.glsl &&echo"drop and export outputs identical"||echo"drop and export outputs DIFFER"
for m in drop export;doG=e.$m.glsl;[ -s $G]||continue; python3 - $G shw/wrappers.tsv <<'PY'
import re,sys,collections
g=open(sys.argv[1]).read();rows=[l.split('\t')for l in open(sys.argv[2]).read().splitlines()if l]
src=collections.Counter(r[1]for r in rows)
defs=re.findall(r'(?m)^(?:\w+ )*?(\w+) (\w+)\(([^)]*)\)\n\{',g);out=collections.Counter(d[1]for d in defs)
print(sys.argv[1],'defs',len(defs),'cultmath_ defs',sum(v for k,v in out.items()if k.startswith('cultmath_')),'source defs',sum(src.values()))
print(' names whose def count differs from source:',[(k,src[k],out.get(k,0))for k in src if src[k]!=out.get(k,0)][:8])
print(' non-source names emitted:',[k for k in out if k not in src][:10],' cmw in text:',g.count('cmw'),' suffixed _N cultmath names:',len(re.findall(r'\bcultmath_\w+_\d+\(',g)))
print(' prologue:',' | '.join(l for l in g.split('\n')[:6]if l.startswith('#'))[:200], ' main:', g.count('void main'))
PY
done
G=e.export.glsl;echo"--- cultmath_lerp and cultmath_saturate as emitted:"; grep -E '^\w+ cultmath_(lerp|saturate)\('$G
step "3b glslang ES 3.00 fragment, call sites resolve per overload"
url=$(curl -s https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/tags/v1.9.2607 | python3 -c "import json,sys;print([a['browser_download_url'] for a in json.load(sys.stdin)['assets'] if 'linux' in a['name'] and a['name'].endswith('.tar.gz')][0])")
echo"$url"; curl -sL --fail -o dxc.tgz "$url"&& mkdir -p tools/dxc && tar xzf dxc.tgz -C tools/dxc;DXC=$(find tools/dxc -name dxc -type f | head -1);echo"DXC=$DXC";$DXC --version 2>&1| head -2
echo"dxc-compute Fma total: $(for s in fam/*.h.spv;do spirv-dis $s| grep -c ' Fma ';done| python3 -c "import sys;print(sum(int(l) for l in sys.stdin))")"
for b in fam/*.body.glsl;dot=${b%.body.glsl};tag=$(basename $t);n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
for d in plain ncod gis;do cmp -s $R/$d/lavapipe-hlsl/$tag.out $R/$d/lavapipe-glsl/$tag.out ||echo"EQ-FAIL $d$tag";done
done <$W/manifest.txt
exportCULTMATH_DEVICE_WORK=$WCULTMATH_DEVICE_NAME=$(sort -u $R/device.txt | head -1 | sed 's/^device=//')CULTMATH_DEVICE_IDENTITY=x CULTMATH_WRITE_DEVICE_BOUNDS=1
for v in plain ncod gis;do awk -F'\t' -v v=$v'{k=$1; u[k]+=$5; if($5+0>m[k]+0)m[k]=$5; c[k]++} END{for(k in c) print "BOUNDS",v,k,"families",c[k],"maxulp",m[k],"sumulp",u[k]}' /tmp/bounds.$v;done
for v in plain ncod;do awk -F'\t''{print $1"/"$2"\t"$5}' /tmp/bounds.$v| sort > /tmp/b.$v;done
join -t$'\t' /tmp/b.plain /tmp/b.ncod | awk -F'\t''$2!=$3{print "ULPDIFF",$1,"plain",$2,"ncod",$3}'| head -60
# Native shader testing: how established projects check shader numerics without a browser, and what each path proves
For `cultmath-tapes:question:device-run-driver` (campaign `cultmath-tapes`). Companion to `docs/research/device-capture-runners.md` (CultLib c9d3e672), which covered browser drivers only; not repeated here.
Also serves the operator's question of 2026-10-16 on why CultMath lowers HLSL to GLSL with its own text lowering (section on HLSL to GLSL ES / SPIR-V toolchains).
Gathered by an Eyes pass for Eureka Self, session self-2026-10-16-ag4, 2026-10-16. Facts only; no recommendation.
Strength marks: [src] read from a primary source text this pass (raw file fetched and read or grepped locally, or a spec PDF converted to text); [doc] vendor or project documentation page read this pass (via a fetch summariser, so check before quoting); [web] search-result summary only, not read in full; [abs] searched, not found.
Nothing was built or run. Dates matter: several repos below are moving; read at HEAD on 2026-10-16.
## 0. The record
- [src] Operator, 2026-10-16, verbatim: "There has to be a better way to test shader code than getting a browser involved". An observation that sets research direction, not a design ruling.
- [src] Question `device-run-driver` (viewed in the mind): options committed-captures / playwright-live / both; the runner is a static page that compiles each family's call against `CultMath.glsl`, reads back via RGBA32UI, emits raw bits; C# judges.
- [src] CultMath shader files: `packages/cultmath/shaders/` has `CultMath.hlsl` (673 lines), `CultMath.Affine.hlsl`, `.Cycles.hlsl`, `.Interval.hlsl`, `.Phacelle.hlsl`, and `CultMath.glsl` (900), `.Cycles.glsl`, `.Phacelle.glsl`. HLSL intrinsics seen by grep across the `.hlsl` files: `asuint` (13), `asfloat` (6), `saturate` (2); no `mad`, `fma`, `frexp`, `ldexp`, `precise`, `sincos`, `rcp`, `groupshared`, `#include` hits in the grep (the `GlslLowering.cs` doc comment says the HLSL has "includes" that are inlined).
- [src] The `.glsl` files contain no `precision` statement and no `#version` line (grep found none); a consumer supplies them.
- [src] `GlslLowering.cs` header: the GLSL ES 3.00 files are a regex text transformation of the HLSL; a dialect gap is fixed in the HLSL "in the subset both languages share".
## 1. Google Amber
- [doc] README (github.com/google/amber): runs on Vulkan (fully supported); Dawn listed "WIP". A command-line `amber` tool runs script files, reports pass/fail by exit code, can write output buffers and images to files. Says "not an officially supported Google product". AmberScript is the format getting new features; VkScript (the VkRunner syntax) is kept for history, no new features. Build bots check that Amber compiles on several targets; "integration tests don't run on the bots because of bot limitations, so only unit tests run in CI".
- [src] Maintenance: GitHub API `google/amber`: not archived, created 2018-11-13, pushed 2026-09-24, 76 open issues. Last five commits 2026-05-28 to 2026-09-24 (ray query support, acceleration structures, alpha-to-coverage, barrier fixes).
- [doc] AmberScript (docs/amber_script.md): starts `#!amber`. `SHADER compute <name> <format>` where format is GLSL, HLSL, SPIRV-ASM, SPIRV-HEX, SPIRV-BIN or OPENCL-C, inline, `FILE`, or `VIRTUAL_FILE`; optional `TARGET_ENV`. `PIPELINE compute <name> ... END`. `BUFFER <name> DATA_TYPE <type> [STD140|STD430] DATA ... END | FILL v | SERIES_FROM s INC_BY i`, types include int8..int64, uint8..uint64, float16, float, double, vectors, matrices. `COPY a TO b`. `RUN <pipeline> x y z`.
- [doc] Expectations: `EXPECT <buf> IDX <n> EQ|NE|LT|LE|GT|GE <values>`; with tolerance `EXPECT <buf> IDX <n> TOLERANCE <t1..t4> EQ <values>` (1 to 4 floats, a trailing `%` makes one relative; components without one use a default); whole-buffer `EQ_BUFFER`, `RMSE_BUFFER ... TOLERANCE <x>`, `EQ_HISTOGRAM_EMD_BUFFER ... TOLERANCE <x>`. Pixel forms `EQ_RGB`, `EQ_RGBA` use 0-255. Comparison is on typed values, not raw bit patterns; a uint32 buffer compared with `EQ` is an exact integer compare. [abs] A ULP-based tolerance form was not found in the AmberScript doc.
- [doc] VkScript (docs/vk_script.md): `probe ssbo` has a `~=` fuzzy-equal comparator and a `tolerance t0 [t1 t2 t3]` command.
- [web] Amber with SwiftShader: a build option `AMBER_ENABLE_SWIFTSHADER`/`AMBER_USE_SWIFTSHADER` builds SwiftShader as a Vulkan ICD; run with `VK_ICD_FILENAMES` pointing at its manifest; `amber -d` style device listing confirms it. [abs] Amber's own CI workflow files were not found at the paths tried (404); whether Amber CI uses lavapipe or SwiftShader was not established.
- [web] Amber lineage: shader test files mirror VkRunner's format; Amber is shipped inside Android's `external/deqp-deps` and Vulkan CTS.
## 2. Software Vulkan: Mesa lavapipe and SwiftShader
### 2.1 Conformance and float behaviour
- [web] Lavapipe announced formally Vulkan 1.2 conformant (Phoronix, May 2022) and Vulkan 1.3 conformant (July 2022; Khronos submission 696, AMD Ryzen 7 1800X, Mesa 22.1). A Mesa issue (#5472, older) complained lavapipe reported a conformance version while not yet conformant; resolution not checked.
- [web] Mesa MR 11739 "WIP: lavapipe: float_controls" (author's words as summarised): passes about 97% of CTS, fails `float_controls...denorm_*_preserve` cases, "llvmpipe/gallivm have no concept of setting a specific denorm mode"; the author said they probably would not continue. Current merged state of float controls in lavapipe: [abs] (the Mesa GitLab pages and `lvp_device.c` were behind bot protection or not at the tried path).
- [web] A separate llvmpipe MR disables denorms for compute shaders to align with fragment shaders, because OpenCL CTS tests "don't expect working denorms".
- [web] SwiftShader commit "Implement VK_KHR_shader_float_controls": at that time SwiftShader exposed no preserve, denorm or rounding controls and set all denorm-preserve and flush-to-zero flags false. Current values: [abs].
- [doc] Vulkan float controls (core in 1.2): a flush-to-zero flag set means denormals may be flushed, not that they are; preservation needs `shaderDenormPreserveFloat32` and a `DenormPreserve` execution mode on the entry point. Rounding-mode controls do not apply to float to int conversions (always toward zero).
- [web] Mesa lavapipe docs page (docs.mesa3d.org/drivers/lavapipe.html) 404s at the tried URL; the `llvmpipe.html` page was read and says nothing of lavapipe floats.
- [src] D3D11 floating-point rules (learn.microsoft.com, doc dated 2025-02/04): 32-bit ops are within 1 ULP (add, subtract, multiply: 0.5 ULP "applies only to addition, subtraction, and multiplication", hardware may truncate); "Denorms are flushed to sign-preserved zero on input and output of any floating-point mathematical operation" (data movement exempt); min/max flush for comparison but the result may or may not be flushed; fused operations (mad, dp3) are no less accurate than the worst serial ordering with each step allowed 1 ULP; sqrt and rcp 1 ULP, rcp/rsq have their own relaxed requirement; x*1.0, x/1.0, x+-0.0 return x "except denorm flushed"; NaN payload bits not preserved; no exceptions or traps; double precision preserves denorms and follows IEEE 754R; float16 preserves denorms.
- [src] GLSL ES 3.00 spec section 4.5.1 (Khronos PDF, converted locally): highp float is "defined by the IEEE 754 standard for 32-bit" in storage; "Any subnormal (denormalized) value input into a shader or potentially generated by any operation in a shader can be flushed to 0. The rounding mode cannot be set and is undefined." Required precisions: `a+b, a-b, a*b` correctly rounded; `a/b` and `1.0/b` 2.5 ULP for b in [2^-126, 2^126]; `a*b+c` "correctly rounded single operation or sequence of two correctly rounded operations"; `exp`, `exp2` (3 + 2|x|) ULP; `log`, `log2` 3 ULP outside [0.5, 2.0] and absolute error < 2^-21 inside; `inversesqrt` 2 ULP; `sqrt` inherits from `1.0/inversesqrt`; trig and `determinant` have undefined precision; explicit conversions correctly rounded; "rounding mode ... must not affect the result by more than 1 ULP". NaNs and Infs "are not required to be generated".
- [src] GLSL ES 3.10 spec (NVIDIA-hosted copy of the Khronos PDF) repeats the same flush-to-zero sentence (line 3061 of the converted text).
- [src] Table of required ranges in ES 3.00: highp relative precision 2^-24 (as IEEE); mediump minimum range (-2^14, 2^14), relative 2^-10; lowp minimum range (-2, 2), absolute 2^-8. Integers: highp 32-bit; mediump 16 to 32 bits; lowp 9 to 32 bits. "Mediump and lowp ... have maximum range and precision as defined by IEEE 754", so an implementation may run them at 32 bits.
### 2.2 Who uses software Vulkan in CI
- [src] wgpu `.github/workflows/ci.yml` (trunk, read via fetch): job `gpu-test` on `windows-2022`, `macos-14`, `ubuntu-24.04`. Linux installs the Vulkan SDK and Mesa (driver named only via the composite action, not visible), sets `LVP_POISON_MEMORY: true`; Windows installs DXC and WARP (`install-warp` action), Agility SDK, Mesa, Vulkan SDK, sets `WGPU_DX12_COMPILER: dxc`. Tests run with `cargo xtask test` (nextest).
- [doc] wgpu `docs/testing.md`: names `LVP_POISON_MEMORY` ("fills all newly initialized memory with a non-zero value"); describes a parameter system that skips or expects failure per adapter; says nothing on float or denorm differences.
- [web] onnxruntime commit replaced Mesa lavapipe with SwiftShader in headless CI because an old lavapipe returned `VK_ERROR_INCOMPATIBLE_DRIVER` when Dawn asked for a Vulkan 1.3 instance; the SwiftShader ICD is built from source there and used by Dawn.
- [web] Dawn/Chromium use the SwiftShader Vulkan ICD for headless runs; ANGLE end2end has a `VULKAN_SWIFTSHADER` config and a `--deqp-egl-display-type=angle-swiftshader` mode (section 4).
- [web] Wine/vkd3d: tests marked broken on WARP; a November 2025 commit disabled 64-bit wave bitwise ops on WARP "known to be buggy"; a merge request says WARP "doesn't emulate faithfully a hardware device".
- [abs] No project was found that commits raw float bit patterns from lavapipe or SwiftShader as a reference. Projects found compare with tolerance, by pixel, or by exact integer.
## 3. GLSL/HLSL to SPIR-V
### 3.1 glslang
- [src] glslang README (read at main): ESSL is a supported front end; compute shaders use `.comp`; stage is inferred from the file extension; C++ and C interfaces exist; JS/WASM build via `-DENABLE_GLSLANG_JS=ON`. Tests: gtest under `gtests/` plus a `runtests` script, expected outputs in `Test/baseResults/`.
- [src] The HLSL front end of glslang is deprecated: README states deprecation, slated for removal in the next major version with at least 18 months' notice, no bug reports accepted. Commit "Deprecate the HLSL front-end." dated 2026-04-16 (GitHub API). This does not touch `dxc -spirv`, a separate compiler.
- [web] Compute shaders need desktop GLSL 4.30 or GLSL ES 3.10 (Mesa error text "Compute shaders require GLSL 4.30 or GLSL ES 3.10"). In ES 3.10 only the last member of a shader storage block may be a runtime-sized array; desktop is looser.
- [src] SPIRV-Cross (section 3.3) throws "At least ESSL 3.10 required for compute shaders." for ES versions under 310. So a `#version 300 es` compute shader does not exist: a GLSL ES 3.00 fragment function ported to compute becomes a 3.10 shader.
- [src] ES 3.10 section 4.7.4: vertex and compute languages have predeclared `precision highp float; precision highp int;`; the fragment language has "no default precision qualifier for floating point types" and `mediump int`. So an unqualified float in a compute shader is highp, while a fragment shader must declare a float precision.
- [src] ES 3.10 and 3.00 share the flush-to-zero sentence and the ULP table for highp (the 3.10 table was not diffed line by line; the denorm sentence was).
- [abs] A published comparison of ES compute against desktop 4.30 numerics (same function, fragment vs compute) was not found. The two specs' highp rules, as read, are the same sentence; what a given driver does per stage is not stated by either spec.
### 3.2 dxc -spirv
- [src] DXC `docs/SPIR-V.rst` (235 KB, read by grep): default target environment `vulkan1.0` via `-fspv-target-env`; `-fspv-entrypoint-name=<name>` exists; `min16float`, `min10float`, `min16int`, `min12int`, `min16uint` map to 32-bit types with `RelaxedPrecision` unless `-enable-16bit-types`, which gives real 16-bit types and the `Float16` capability. `lib_6_x` appears only in the raytracing section ("must be compiled as library using lib_6_3/lib_6_4"). [abs] No statement in the doc that a non-raytracing `lib_6_x` library compiles to SPIR-V with exported functions; no statement on denormal execution modes.
- [src] `SPV_KHR_float_controls` is in the supported-extension list; no description of when dxc emits denorm modes (not found by grep of the doc).
- [src] HLSL exposes `asuint`/`asfloat` as bitcasts; in SPIR-V these are `OpBitcast`. (CultMath's use is section 0.)
### 3.3 SPIRV-Cross (SPIR-V to GLSL ES/HLSL/MSL)
- [src] README (main): ES example `spirv-cross --version 310 --es test.spv`; sections on HLSL-derived SPIR-V; names glslangValidator's `-e` entry-point flag in an error hint. Active: commits 2026-09-18, 2026-09-22, 2026-10-14.
- [src] `spirv_glsl.cpp`: `#version` is written as `#version <n> es` when `options.es && version > 100`; ES is allowed from 100 up (the `300 es` form is the emitted string); buffer blocks need ES >= 300, `binding` needs >= 310.
- [src] Precision: `backend.allow_precision_qualifiers = options.vulkan_semantics || options.es;``flags_to_qualifiers_glsl` emits `mediump ` for ids with `RelaxedPrecision`, else `highp `, and omits either when it equals the fragment default; structs and doubles get none. `Options::Fragment` defaults are `default_float_precision = Mediump`, `default_int_precision = Highp` (`spirv_glsl.hpp` lines 196-197). `emit_header()` writes `precision <p> float;` and `precision <p> int;` only for fragment shaders when `options.es`. So a function-only library run through `--es` with default options would, in a fragment context, assume mediump float defaults and tag non-relaxed values `highp`.
- [src] Library modules: `spirv_parser.cpp` (main) handles a module with no `OpEntryPoint` but with `OpDecorate ... LinkageAttributes ... Export`: marks `is_library_module`, uses the first exported function as a placeholder entry point with execution model GLCompute, and `spirv_glsl.cpp` emits "each exported function as a normal free function" plus the callees. A module with neither throws "There is no entry point in the SPIR-V module." The code paths exist at HEAD; which release first had them, and whether the output is valid for ES, were not checked.
- [web] A Unity issue reports min16float hints (`min16f as def32` modifier in FXC bytecode) not honoured in SPIR-V/ESSL output (mediump produced where highp was expected). One blog summary says SPIRV-Cross may pick a low default precision unless told otherwise (not authoritative).
- [src] Readability: [abs] no source characterising readability of the emitted GLSL was found; SPIRV-Cross output is generated text with temporaries named `_<id>` unless debug names survive (names come from OpName; not verified for dxc output).
## 4. Native ANGLE without a browser
- [doc] ANGLE README: implements EGL 1.5 and GLES 2.0/3.0/3.1 over Vulkan, desktop GL, GLES, D3D9, D3D11, Metal; "Chrome uses ANGLE for all graphics rendering on Windows" and it is the default WebGL backend for Chrome and Firefox on Windows; its shader compiler can emit Desktop GLSL, Vulkan GLSL, HLSL, ESSL.
- [doc] ANGLE `doc/DevSetup.md`: build standalone with `fetch angle`, `gn gen out/Debug`, `autoninja -C out/Debug`; tests, libraries and samples land in `out/Debug`; on Windows default backend is D3D11 (D3D9 otherwise); backend chosen at runtime through `EGL_ANGLE_platform_angle` via `eglGetPlatformDisplayEXT` (`util/EGLWindow.cpp` as example); backends removable via GN args `angle_enable_d3d11` etc. [abs] prebuilt binaries not covered in the doc.
- [src] ANGLE `doc/dEQP.md`: targets `angle_deqp_gles2_tests`, `angle_deqp_gles3_tests`, `angle_deqp_gles31_tests`, `angle_deqp_egl_tests`; renderer chosen with `--deqp-egl-display-type=angle-d3d11 | angle-d3d9 | angle-d3d11-fl93 | angle-gl | angle-gles | angle-metal | angle-swiftshader | angle-vulkan` (same as `--use-angle=X`); default is Vulkan except Apple (OpenGL); test lists come from `third_party/VK-GL-CTS/.../gles3-main.txt`; GoogleTest builds of the same targets run on the bots with expectation files in `src/tests/deqp_support`.
- [src] ANGLE `doc/TestingAndProcesses.md`: angle_end2end_tests expectations live in `src/tests/angle_end2end_tests_expectations.txt` (`SKIP`, `TIMEOUT` only); the WebGL conformance expectations live in the Chromium tree, not the ANGLE tree.
- [web] `angle_end2end_tests` configs include `ES2_D3D11_WARP`, `ES2_D3D11_REFERENCE`, feature-level variants and `VULKAN_SWIFTSHADER`; end2end tests subsume the older angle_tests and standalone tests.
- [doc] ANGLE `EGL_ANGLE_create_context_webgl_compatibility` / `ANGLE_webgl_compatibility` (spec text, Draft): `eglCreateContext` attribute `EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE` adds WebGL validation; shaders with `#version 100` get WebGL 1 rules and `#version 300 es` get WebGL 2 rules.
- [src] Path in D3D11 (`src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp`, main): ANGLE translator emits HLSL, then `D3DCompile` with profile `vs_5_0`/`ps_5_0` style (`getMajorShaderModel`), flags `D3DCOMPILE_OPTIMIZATION_LEVEL2`, plus `D3DCOMPILE_IEEE_STRICTNESS` only `if (workarounds.enableIEEEStrictness)`; on failure retries with `SKIP_VALIDATION`, then `SKIP_OPTIMIZATION`. The HLSL is generated at link time (`DynamicShaderCompilation.md`) because signatures between stages must be known.
- [src] ANGLE `doc/CompilingTranslatorWithEmscripten.md`: the translator alone can be compiled for use from a page (preliminary notes).
- [abs] The `enableIEEEStrictness` workaround's default value in ANGLE and in Chrome was not found. [abs] Whether Chrome's WebGL2 context and a standalone ANGLE EGL context share all translator options and workaround flags was not established (same repository; Chrome applies its own feature flags and the WebGL-compatibility context).
- [abs] No source found for a project that uses a standalone ANGLE `libEGL`/`libGLESv2` as a compute-readback harness; ANGLE's own tests use it as the system under test.
- [src] Context: GLES 3.0 cannot run compute (3.1 adds it); ANGLE advertises 3.1 (README). A WebGL2 fragment readback and an ES 3.1 compute readback are different ANGLE shader paths (pixel-shader HLSL vs compute-shader HLSL).
## 5. D3D11/D3D12 compute harnesses for HLSL
- [src] DXC repo `tools/clang/unittests/HLSLExec/` files: `ExecutionTest.cpp`, `HlslExecTestUtils.cpp`, `LongVectors.cpp`, `ShaderOpArithTable.xml`, `ShaderOpTest.cpp` and others; the test project uses TAEF (WEX) test methods.
- [src] `HlslExecTestUtils.cpp`: `useWarpByDefault()` returns true unless `_HLK_CONF` is defined; when the `UseWARP` parameter is set it creates a device from `EnumWarpAdapter` at feature level 11_0, with an optional `WARP_DLL` path to load a specific WARP build.
- [src] `ExecutionTest.cpp`: `DenormBinaryFloatOpTest`/`DenormTertiaryFloatOpTest` read tables from `ShaderOpArithTable.xml#DenormBinaryFloatOpTable`/`DenormTertiaryFloatOpTable` (rows like `FDivDenormFTZ`; columns `ShaderOp.Target`, `ShaderOp.Arguments`, `Validation.Type`, `Validation.Tolerance`, inputs and expected values); a helper `ifdenorm_flushf_eq` accepts either flushed or unflushed results; derivative tests use `CompareFloatULP(..., 1)` with alternative acceptable values.
- [src] `LongVectors.cpp`: validation config `Epsilon(tol)` or `Ulp(tol)`; default is ULP 1.0 for floating-point types, exact for others; failures print the validation type and tolerance.
- [src] DXC's `HLSLExec` tests thus run on WARP by default and on hardware via a flag; tolerances are per-row in data tables.
- [web] Shader Test Framework (KStocky, D3D12, Catch2, HLSL 202x): README says "very experimental"; tests are HLSL functions run through a `ShaderTestFixture`; a device can be hardware or WARP (README read by fetch did not state WARP; a search summary and the Skyrim Community Shaders DeepWiki page did); README does not state its float comparison. Used by Skyrim Community Shaders per a DeepWiki page (AI-generated, unverified).
- [web] An LLVM 2025 dev meeting talk lists "accuracy tolerances" and "NaN/INF/denorm handling" among hard areas for HLSL test writing.
- [web] WARP is Microsoft's recommended D3D11 device for service/server environments; Session 0 services cannot use hardware D3D9 HAL on WDDM (also in the earlier file).
- [abs] WARP's own float conformance statement (denorm flushing, mad fusion) was not found in a primary document; the D3D11 rules in 2.1 are the contract WARP implements.
- [web] vkd3d-proton and Wine's `d3d12` tests mark WARP-specific broken cases (see 2.2).
## 6. Other paths
### 6.1 Slang
- [doc] Targets page (shader-slang.org/slang/user-guide/targets.html): D3D11 (DXBC), D3D12 (DXIL), Vulkan (SPIR-V), OpenGL (GLSL source, SPIR-V, vendor binaries; "only limited support for compiling code for OpenGL"), Metal, CUDA and OptiX, CPU compute ("compute only", "functional, but not feature- or performance-complete"), WebGPU (WGSL, "work in progress"). GLSL ES: not mentioned.
- [doc] `a2-04-glsl-target-specific.md`: version selected by `-profile glsl_<version>` (example `glsl_450`); the GLSL target "is currently less mature than the SPIR-V target and has several known limitations"; known issue with 3-element vector constant-buffer packing. GLSL ES / `300 es`: not mentioned. [abs] A search for Slang emitting `#version 300 es` found nothing.
- [doc] `docs/cpu-target.md`: outputs C++ (`-target cpp`) or executables, shared libraries, host-callable code, object code; in-process execution through `getTargetHostCallable`; default host-callable backend is `slang-llvm` (no file system, no C compiler needed, shader style only), alternatives are system Clang/GCC/Visual Studio via temp files; limitations: no barriers, no user atomics (except compiler-generated 32/64-bit int add), entry points cannot be named `main`, `float16_t` unsupported, limited bounds-check handling; "no content on floating-point semantics".
- [web] Slang can compile to SPIR-V directly or via GLSL (`-emit-spirv-via-glsl`, glslang downstream).
### 6.2 wgpu / naga
- [src] naga README table: inputs SPIR-V (primary), WGSL (primary, "fully validated"), GLSL (secondary, "GLSL 440+ and Vulkan semantics only"); outputs SPIR-V, Metal, HLSL ("Shader Model 5.0+"), WGSL (secondary), GLSL (secondary, "GLSL 330+ and GLSL ES 300+"). HLSL is not an input language. naga is a different translator from ANGLE's; wgpu's DX12 path uses naga HLSL output compiled with DXC or FXC (`WGPU_DX12_COMPILER`, ci.yml).
- [src] wgpu CI deletes `naga/tests/out` snapshots before test runs (ci.yml), i.e. naga keeps snapshot-compared output files.
### 6.3 Tint (Dawn's WGSL compiler)
- [web] Search snapshots (2020-2021, a CMake-only Dawn fork): Tint reads WGSL (default on) and SPIR-V (`TINT_BUILD_SPV_READER`, off by default); writers for SPIR-V, WGSL, HLSL, MSL, GLSL (GLSL writer added October 2021, validated with glslang). [abs] The current upstream README was not retrievable (404 at tried paths), so current readers and writers are unconfirmed.
### 6.4 Shader-function unit-test frameworks and GPU-free paths
- [web] `shadertest` (Kupoman, Python): load a GLSL file, call its functions from pytest; supports bool, int, uint, float and vectors; implementation (GPU context) not read.
- [web] `shdr` (cdave1): experimental CPU emulator for GLSL fragment shaders, translates GLSL to JavaScript, "does not quite match the glsl". `rpu` (Rust crate): a GLSL-like language compiled to WebAssembly for CPU rendering, not full GLSL. HlslUnit (.NET): an interpreter that runs HLSL on the CPU (search summary).
- [web] Blender added infrastructure to run GLSL shader unit tests and shares C++/GLSL source in the GPU module, compiled through the real GPU backend.
- [web] Khronos forum "GLSL Unit" (Google MapsGL team) unit-testing framework for shaders, old, likely dormant.
- [doc] Vulkan Validation Layers `debug_printf`: shaders print through `debugPrintfEXT`; output goes to the validation layer message stream; [web] each a runtime Vulkan layer, not a numeric assertion framework.
- [abs] No GPU-free GLSL ES interpreter with IEEE-faithful float semantics, maintained, and used by a notable project was found.
## 7. Cross-cutting: tools that produce SPIR-V for sections 1-3 (item 8)
| Tool | Input | Output | Library with no entry point | Maintenance |
|---|---|---|---|---|
| glslang | GLSL, ESSL, HLSL (deprecated 2026-04-16) | SPIR-V | not covered in README | active (commits 2026-04) |
| dxc `-spirv` | HLSL | SPIR-V | `lib_6_x` only documented for raytracing; `-E` entry otherwise [abs] export behaviour | active repo, wiki and docs |
| SPIRV-Cross | SPIR-V | GLSL (incl. `300 es`, `310 es`), HLSL, MSL | HEAD has library-module path keyed on `LinkageAttributes Export` | active (2026-10-14) |
- [src] Which can also feed Amber/Vulkan runs: glslang and dxc produce SPIR-V from GLSL/HLSL; Amber itself accepts HLSL and GLSL source in `SHADER` blocks directly (section 1), choosing compilers internally [web: Amber builds against glslang/shaderc; which is used for HLSL not read].
- [src] Which can emit GLSL ES 3.00 for the web from one source: SPIRV-Cross (`--es --version 300`, with the fragment precision options above), naga (GLSL ES 300+), HLSLcc (GLSL ES from DXBC; `ES 3.0+` per README), Slang (not documented).
- [web] WGSL for the web: naga and Tint write WGSL (naga: "secondary"); Slang WGSL target "work in progress".
## 8. What each path exercises (stated as facts about the scope of each check)
| Path | Executes | Compiler/translator in the chain | Float rules in force | What a pass establishes | What it does not |
|---|---|---|---|---|---|
| Amber on lavapipe | SPIR-V on Mesa llvmpipe JIT (CPU) | glslang or dxc/other to SPIR-V, then Mesa NIR/LLVM | Vulkan spec; GLSL-derived SPIR-V denorm handling per Mesa; FMA contraction per Mesa/LLVM | The shader source, as compiled to SPIR-V, computes within the stated tolerance on a Vulkan 1.3 conformant implementation | Behaviour of ANGLE's HLSL, fxc/D3DCompile, NVIDIA D3D11 |
| Amber on SwiftShader | SPIR-V on SwiftShader's JIT (Reactor/Subzero) | same front end | Vulkan spec; float controls reported off (older commit) | same, on a second conforming implementation | same |
| Amber on a real Vulkan GPU | SPIR-V on the driver | same | vendor | The SPIR-V on that driver | WebGL path |
| glslang/SPIRV-Cross text snapshot tests | nothing executed | translators only | none | translation is stable between versions | numerics |
| ANGLE `angle_end2end`/dEQP on D3D11 | GLSL ES via ANGLE translator, HLSL, `D3DCompile` (fxc), NVIDIA D3D11 driver, as in Chrome on Windows | ANGLE as built in the harness (the same codebase Chrome vendors) | D3D11 rules (flush to zero, 1 ULP) over ESSL rules | The ES shader behaviour of that ANGLE build on that driver | Chrome's own flags/feature overrides unless reproduced |
| ANGLE on SwiftShader (`angle-swiftshader`) | GLSL ES via ANGLE to SPIR-V, SwiftShader | ANGLE Vulkan backend | Vulkan | ANGLE's Vulkan translation path, software | ANGLE's D3D11 path |
| D3D11/12 compute on WARP | HLSL via dxc/fxc, WARP CPU rasterizer | Microsoft compiler | D3D rules as implemented by WARP | The HLSL source on a D3D conformant software device | NVIDIA hardware or the WebGL route |
| D3D11/12 compute on GTX 1070 | HLSL via dxc/fxc, NVIDIA driver | | D3D rules, vendor | The HLSL source on that GPU | other vendors |
| wgpu/naga on lavapipe or WARP | WGSL (or SPIR-V in) via naga | naga | WGSL/Vulkan or D3D12 | naga's translation of that source on that device | ANGLE or glslang/dxc paths |
| Slang CPU | Slang to C++ or LLVM, run on the host CPU | Slang | host CPU IEEE with the host compiler's flags (page silent) | Slang source semantics on a CPU | HLSL or GLSL as consumers compile them |
| GLSL-to-CPU emulators/interpreters | the interpreter | the interpreter | the interpreter's host floats | the interpreted source | any real driver |
| Browser WebGL2 (earlier file) | GLSL ES via browser's ANGLE | | | the exact consumer path on that browser/GPU | other browsers/drivers |
- [src] Spec positions that bound what a conforming-implementation pass can claim: GLSL ES 3.00/3.10 allow flush of subnormals to zero at any time, undefined rounding, `a*b+c` either fused or two roundings, and undefined trig precision; D3D11 requires flushing subnormals on math input/output and allows fused ops up to the unfused tolerance; Vulkan lets an implementation flush unless a preserve mode is requested and supported.
- [src] Where the same GLSL ES source goes through ANGLE→HLSL→`D3DCompile`→driver, the compiler flags in `Renderer11.cpp` (OPTIMIZATION_LEVEL2, retry configurations, conditional IEEE_STRICTNESS) are part of the path; a SPIR-V route (glslang or dxc, then a Vulkan implementation) has none of those stages.
- [abs] No source found that quantifies disagreement between ANGLE→D3D11 and a Vulkan software implementation for the same GLSL ES function.
## 9. Gaps (searched, not resolved)
- Lavapipe and SwiftShader current `VkPhysicalDeviceFloatControlsProperties` values; FMA contraction policy in either; the lavapipe docs page.
- Amber's own CI jobs and which compilers it uses for HLSL and GLSL.
- Whether `dxc -spirv` can emit an exported-function library that SPIRV-Cross's library path accepts; whether `dxc` flush/preserve execution modes are emitted by default.
- Tint's current readers and writers.
- Slang GLSL ES output; Slang CPU float flags.
- ANGLE: default of `enableIEEEStrictness`; prebuilt standalone `libEGL`/`libGLESv2` availability for Windows outside Chrome/Electron; Chrome-specific translator options.
- WARP numerics statement; DXC test tolerance values per intrinsic row (the XML table structure was read, not every row).
| MessagePack-CSharp (UntrustedData and TrustedData) | 500 (`MaximumObjectGraphDepth`) | each `DepthStep` call: formatter-driven, one step per container a formatter enters; check is `Depth >= max` before increment | read only; skipped content not counted unless a formatter calls DepthStep (see 2.1) | MessagePackSecurity.cs:31,41,92,266-273 |
| rmp-serde | 1024 (`depth: u16`, `set_max_depth`) | containers (array, map, ext newtype) via `depth_count!`; counter decremented, error at 0 | read: yes; write: error variant exists but is never constructed | decode.rs:51-60,213,294,309-310,566,583,616; encode.rs:37,49,64 |
| serde_json | 128 (`remaining_depth`, `disable_recursion_limit`) | each `[` and `{` entered by `deserialize_any`-style paths; error when the counter reaches 0 (deepest enterable container is 127, inferred from the decrement-then-test-zero code) | read only; serializer has no depth code; skipped values (`ignore_value`) are iterative and contain no `check_recursion!` call | de.rs:63,1357-1387,1434,1445,1102-1215; ser.rs (no match) |
| msgpack-python (C unpacker) | 1024 (`MSGPACK_EMBED_STACK_SIZE`) | each container pushed on a fixed stack, before the zero-length check; raises `StackError` | read: yes (including `skip()`); write: `Packer``DEFAULT_RECURSE_LIMIT = 1024` (`ValueError`) | unpack.h:19; unpack_template.h:117-127; _unpacker.pyx:208-211,524; _packer.pyx:42,259-262 |
| msgpack-python (pure-Python fallback) | Python's recursion limit; `RecursionError` is mapped to `StackError` | recursion frames | read; write via `DEFAULT_RECURSE_LIMIT = 1024` | fallback.py:66,586-606 |
| @msgpack/msgpack | decode: no depth limit found (heap-allocated `StackPool`, iterative); encode `maxDepth` 100 | encode: depth argument to `doEncode` | read: unbounded; write: yes (`Too deep objects in depth N`) | Decoder.ts:111-190,640-643; Encoder.ts:9,97,173; README.md:119 |
| msgpackr | no depth option found | recursive `readObject` (inferred from the single recursion comment at unpack.js:506) | neither | unpack.js, pack.js (grep for depth/maxDepth: no hit) |
| System.Text.Json | 64 (reader and serializer); writer default 1000 | `JsonReaderOptions.MaxDepth`; `JsonSerializerOptions.MaxDepth` default is the minimum of reader and writer defaults | read: yes; write: yes (writer 1000, serializer 64) | JsonReaderOptions.cs:13,43; JsonWriterOptions.cs:18; JsonSerializerOptions.cs:27-28,580-593 |
| Newtonsoft.Json | 64 (`JsonReader` ctor; `MaxDepth` nullable) | reader `Depth + 1 > _maxDepth` | read; `JsonWriter.cs` has no MaxDepth match | JsonReader.cs:332,356-359 |
| protobuf C++ | 100 (`default_recursion_limit_`) | recursion budget decremented per nested message/group | read | coded_stream.h:630,1588-1602,1637 |
| protobuf upb | 100 (`kUpb_WireFormat_DefaultDepthLimit`) | `d->depth` per decode; max configurable up to a uint16 | read | upb/wire/internal/constants.h:11; decode.c:105,140,192,619,1306-1312 |
| ciborium | 256 (`recurse` counter) | `recurse()` wrapper around array/map/tag decode | read only (cib_ser.rs has no depth/recurs match) | ciborium/src/de/mod.rs:54-74,812,854 |
| cbor2 (Python) | 400 `max_depth` since 5.9.0 (CVE-2026-26209 fix); 6.0.0 (2026-04-28) rewrote the decoder in Rust as iterative, "limited only by the available memory" | containers | read | docs/versionhistory.rst:120-142,172-173 |
| BSON (MongoDB) | 100 levels | "Each object or array adds a level" | the doc page does not say read vs write | mongodb.com/docs/manual/reference/limits/ |
| Go encoding/json | 10000 (`maxNestingDepth`) | scanner `parseState` stack length; the scanner validates the whole input before decoding (`checkValid`) | read; encoder has no depth limit, only pointer-cycle detection after 1000 pointer levels (`startDetectingCyclesAfter`) | scanner.go:146-186; encode.go:298-305,764 |
Section 1.5 left this untested. Python 3.13 and cultcache-py at main 2037dd44 wrote four single-file stores of
`cultcache.interop-note`. The C# reference at 2037dd44 read them through
`tests/GameCult.Caching.InteropPeer read --file` (dotnet sdk 10.0 on Yggdrasil, ygg-verify job1). The scripts and
log are in the Imagination scratchpad `imagination-cl-py-slot-types/` (`write_stores.py`, `cmd.txt`, `job1.log`).
| Store | Python definition | Payload | C# result |
|---|---|---|---|
| a | `interop.py` (`define_document_type`, catalog members hand-typed `System.String`, `System.String[]`) | correct | opens, record read |
| b | `define_database_entry_type`, same schema name and version as C# | correct | refused: `Persisted schema 'cultcache.interop-note' ... is incompatible ...: slot 0 changed type 'System.Object' -> 'System.String'; slot 1 ...` (every slot) |
| c | as b | `title = 12` | refused at the catalog, as b |
| d | as a (typed catalog) | `title = 12`, accepted by Python on write | refused at decode: `Record 'note:python-d' ... cannot be decoded ... Unexpected msgpack code 12 (positive fixint)` |
Mechanism (source read): the C# schema id is a SHA-256 of the semantic fingerprint (`CultCache.cs:656-658`), while
Python's defaults to the type name. A Python-written entry therefore never matches by id. It resolves by schema name
(`CultCache.cs:518-521`), and the content hashes differ, so `CompareSchemaShapes` runs and refuses any TypeName mismatch
(`:874-877`). The refusal is thrown from the pull, so the C# cache does not open the store at all.
Consequences:
- `define_database_entry_type` has no `members` or per-slot type parameter (`documents.py:214-229`). An ordinary
Python consumer cannot write a store that a C# peer with the same schema name will open. The only way to get one is
to hand-build `define_document_type` with typed members, as `interop.py` does. This is the TS finding
`cut-union-codec-ts.s1.codec-catalog-refused-by-reference` again, this time in Python.
- Store d: a wrong scalar that Python accepts on write makes the store unreadable to the C# reference.
- A C# `object` member is also cataloged `System.Object`, and MessagePack-CSharp's object formatter accepts any
value. Python's current default is therefore true of a C# `object` slot, and false for every typed C# slot.
Real consumer definitions for scale (source read): `cultnet_py/cultmesh_contracts.py:626-698`
(`CultMeshSimulationFact`, a frozen dataclass annotated `str`, `int`, `float`, `str | None`, passed as `cls=`), and
`repixelizer/src/repixelizer/verse_state.py:77-118` (dict values with no `cls`; `("value", 0)` and `("surface", 4)`
hold whole nested structures in one slot, which is a genuine "any" slot). Python annotations do not carry the C# widths:
`int` cannot say Int32 or Int64, and `float` cannot say Single or Double.
## 2. Prior art (dynamic host, declared field types)
| System | Types come from | Write | Read | Evidence |
|---|---|---|---|---|
| protobuf python | .proto IDL, generated classes | type-checked on field assignment (`TypeError`; `ValueError` out of range; bool rejected for int) | wire-type mismatch handling not stated in docs | protobuf.dev/reference/python/python-generated ("setting `foo` to a value of the wrong type will raise a `TypeError`"); `type_checkers.py` (IntValueChecker rejects bool `TypeError`, out of range `ValueError`; UnicodeValueChecker non-UTF-8 `ValueError`) https://raw.githubusercontent.com/protocolbuffers/protobuf/main/python/google/protobuf/internal/type_checkers.py ; proto3 guide: wire format "doesn't provide a way to detect fields encoded using one definition and decoded using another" https://protobuf.dev/programming-guides/proto3/ |
| Apache Avro python | Avro schema (JSON), not host code | `DatumWriter.write` calls `validate(..., raise_on_error=True)` -> `AvroTypeException`; union branch picked by non-raising validate | reader does not validate values; checks schema resolution (`SchemaResolutionException`) and decodes by writer schema | https://raw.githubusercontent.com/apache/avro/main/lang/py/avro/io.py ; fastavro writer: `validator` parameter "validation will be done on the records" (opt-in) https://fastavro.readthedocs.io/en/latest/writer.html |
| Thrift python | .thrift IDL, generated code | generated `write` calls `validate()` which checks required-field presence only (not scalar types); `writeFieldBegin` writes the given type unchecked | generated read emits `if ftype == TType.X:` ... `else: iprot.skip(ftype)`: a wrong wire type is silently skipped, field left unset | t_py_generator.cc `generate_py_struct_reader`, `generate_py_struct_required_validator` https://raw.githubusercontent.com/apache/thrift/master/compiler/cpp/src/thrift/generate/t_py_generator.cc ; TBinaryProtocol.py `writeFieldBegin`/`readFieldBegin` have no type check |
| pydantic v2 | Python annotations in host code | validates input at construction / `validate_python` / `validate_json`; lax coercion default, strict opt-in per call, field or config | same entry points | https://pydantic.dev/docs/validation/latest/concepts/strict_mode/ ("Lax mode (default) coerces"; page does not say assignment or serialization validates) |
| msgspec | Python annotations (Struct) in host code | docs (not retrieved; search snippet only): encode serializes values without enforcing annotations | decode validates against the `type=` and raises `ValidationError`; `array_like=True` encodes Struct as positional array | search result only, unverified: https://jcristharif.com/msgspec/usage.html (fetch returned 404 for usage/structs/supported-types pages) |
| Cap'n Proto (pycapnp) | .capnp schema loaded at runtime (`capnp.load`) | assignment type-error behavior not documented on the fetched page | n/a (pointer-based, no parse step) | https://capnproto.github.io/pycapnp/capnp.html (schema loading: "much like loading a Python module") |
| FlatBuffers python | .fbs schema, `flatc --python` generated typed accessors/builders | no runtime type validation stated on the fetched page | typed accessors read at fixed offsets; no value validation stated | https://flatbuffers.dev/languages/python/ |
| MessagePack (msgpack-python) | none | none | none | no IDL; the format is schemaless (msgpack-python itself not fetched) |
Observed pattern in the fetched sources, stated as fact only:
- Type source is an IDL/schema in protobuf, Avro, Thrift, Cap'n Proto, FlatBuffers; host-code annotations in pydantic and
msgspec.
- Write-side type checking: protobuf (assignment), Avro (default on), pydantic/msgspec (on construction/decode of input);
# Prior art: refuse or walk a container the identity walk does not own
For `variants:question:c2a-unowned-holders` (campaign `variants`, cut `c2a-element-id-holes` r5).
These are from Imagination's reading. No web fetch was made in this pass, so check the
pointers before quoting them outside the mind.
| System | Unknown or unsupported type in a payload | Extension point | Pointer |
|---|---|---|---|
| Protocol Buffers | Not expressible. A field is a scalar, a message, `repeated`, or `map<scalar, V>`. No other container exists. | None. You model the data as messages. | protobuf.dev, Language Guide (proto3), "Repeated fields" and "Maps" |
| Microsoft Bond | Fixed container set: `list`, `vector`, `set`, `map`, `nullable`, `blob`, `bonded`. Any other type is a codegen error. | Type aliases with explicit converters, declared per type. | microsoft.github.io/bond/manual/bond_cs.html, "Type aliases" and "Custom type mappings" |
| Orleans 7+ serializer | Throws `CodecNotFoundException` when it serializes a type with no codec (not `[GenerateSerializer]`, not registered). | Surrogates via `[RegisterConverter]`, or codecs registered per type. Built-in codecs cover the BCL immutable collections. | learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization |
| MessagePack-CSharp 3.1.7 | `DynamicObjectResolver` refuses an unannotated type (formatter null, the serializer throws) unless the caller opts into contractless. | Resolvers, `[MessagePackFormatter]`. | Probed: `scratchpad/imag-r5/probe1.log` (NkHold: std=null, contractless builds one) |
| Fixed-size array parameter plus count | compiles, `lib_6_3` and `cs_6_0` [probe P1, P3] | see P2 | needs one transform (`T x[N]` parameter to `T[] x`) | see section 4 |
| Caller accessor macro (`CULTMATH_BODY(i)`) | the library build defines no accessor, so the function is absent [probe P4] | absent from the built GLSL: SPIRV-Cross output carries no consumer macro | the mirror would need the macro too | refused by the route |
| Bodies packed into library-owned constants | a library cbuffer or uniform | a uniform block in a function-only library; a binding the consumer does not own | a static field | refused (P5, by construction, not run): a library may not own a consumer's binding |