Compare commits

..
Author SHA1 Message Date
250d616f8f Merge pull request 'cultmesh-ts: write authority for the RUDP document server' (#37) from libby/cultmesh-ts-write-authority into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Failing after 5m14s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Successful in 26s
2026-11-07 20:21:27 +00:00
Libby
3f4139549e cultmesh-ts: CHANGELOG states the refusal size bound
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 20:21:26 +00:00
Libby
ada30be84e cultmesh-ts: gate before shard refusal, bound the refusal, harden tests
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m2s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 36s
A put with wrong-typed shard fields is judged with null shard fields first, then
refused as malformed once granted. The refusal's record key and reason are
clipped so it fits maxPayloadBytes. The allocation test watches transient
allocations; catch-path and delete tests assert ids, operation, reply count,
onError and handlers. README/CHANGELOG no longer claim the schema is resolved
after the gate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 20:13:49 +00:00
Libby
1689b62597 cultmesh-ts: gate unreadable writes, malformed refusals, payload tests
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m22s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 1m5s
A frame that fails to decode or parse is judged as the write it looks like, so
an ungranted peer gets write_not_authorized instead of silence; a granted peer
gets onError and a malformed_document_put/delete refusal. Adds raw-frame tests
(int, 2**70, oversized claimed length, non-decoding, non-map document) and
the timing residual note.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:57:42 +00:00
Libby
1bc05d7b06 cultmesh-ts: document server asks a write authority before applying a peer put or delete
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Deny by default with a grant-all opt-in; the gate runs before the put is sized,
resolved or decoded; refusal is the typed write_not_authorized. Adds
onDocumentDeleteRaw, refuses wrong-typed shard fields before the gate, grants
all writes in the existing server tests, and ports the cultmesh-py
remote-write-authority tests (no forwarding).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:52:42 +00:00
Libby
474e27fcb3 WIP B 2026-11-07 19:36:40 +00:00
f0731c6b91 Merge pull request 'cultnet-ts: typed write_not_authorized factory and canonical schema id' (#36) from libby/cultnet-ts-write-refusal into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Failing after 6m33s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Successful in 36s
2026-11-07 19:36:36 +00:00
Libby
2016ad7457 cultnet-ts: write_not_authorized blank-reason check is C# IsNullOrWhiteSpace
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m11s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 36s
Review: trim() differs from string.IsNullOrWhiteSpace on U+0085 and U+FEFF; use the
package's isNullOrWhiteSpaceCSharp and pin both in the test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:36:32 +00:00
Libby
d0c0156874 cultnet-ts: typed write_not_authorized factory and canonical schema id
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
cultNetWriteNotAuthorizedError(recordKey, reason?) writes the C# ForRemoteWriteRefused
bytes (pinned to error-vectors.json). CultNetDocumentRegistry.canonicalSchemaId resolves
the id a put or delete writes under, from the id alone, for a server's write authority.
No behaviour change for existing callers; the registry already had a delete apply.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:35:21 +00:00
10c1f4dd00 Merge pull request 'cultmesh-rs: a sink's refusal of a put is a typed write_not_authorized' (#35) from libby/cultmesh-rs-typed-refusal into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 19:31:48 +00:00
Libby
74ccfaf67a cultmesh-rs: a sink's refusal of a put is a typed write_not_authorized
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m48s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 55s
The RUDP document server sent a host sink's refusal as an untyped Error (code None).
It now sends write_not_authorized with details naming the record, as C#, TS and
Python do; the text stays the fixed variant text, never the sink's. The rejection
carries the put's record_key. Other rejections stay untyped. Unit test pins the
bytes to the error-vectors.json write_not_authorized vector; integration tests
check code/details on the wire and that a refused put reaches no sink.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:30:54 +00:00
608ddb94f7 Merge pull request 'cultmesh-py: bind the TCP+UDP pair for port 0 without the race' (#34) from libby/cultmesh-py-port-race into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 19:28:51 +00:00
Libby
cff68d5528 cultmesh-py: bind test proves only the UDP number was taken
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:28:50 +00:00
Libby
e0f5e13a1b cultmesh-py: bind the TCP+UDP pair for port 0 without the race
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 4m46s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 46s
CultMeshLocalServer.start bound TCP to port 0 and then bound UDP to whatever number
the kernel picked, failing with EADDRINUSE whenever another process held that
number for UDP (this host has hundreds of UDP listeners), and left the accept
thread running on failure. Port 0 now retries with a fresh pair (bounded); both
sockets bind before any thread starts; an explicit port still fails honestly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:25:38 +00:00
e2dc04e724 Merge pull request 'cultmesh-py: peer writes need a write authority (default-deny)' (#33) from libby/cultmesh-py-remote-write-authority into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
2026-11-07 19:19:27 +00:00
Libby
87c4ac366d cultmesh-py: refuse wrong-typed shard fields before the write gate
A non-string shardId or non-int shardEpoch (bool is not int) on a put or delete
is a malformed_document_put/delete before the authority is asked, so a
shard-scoped authority cannot be bypassed by a shard the store would file under
str(shardId). The authority receives the fields as sent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:19:26 +00:00
Libby
f074c3d45f cultmesh-py: document why schema resolution precedes the write gate
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m39s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 30s
Comment at the gate and a README residual; no behaviour change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:17:30 +00:00
Libby
61a1d872b3 cultmesh-py: gate runs before an ungranted peer's payload is touched
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
raw_put_target resolves only a put of the wire shape (string key, bytes payload),
so an int payload is never converted (bytes(10**9)) before the authority speaks.
Shard fields of the wrong type reach the authority as None. Tests: huge payload
refused without allocating, RUDP peer path (endpoint and transport), shard types.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:14:12 +00:00
Libby
2947e15088 cultmesh-py: peer writes need a write authority (default-deny)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m36s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 20s
Finishes the remote-write gate: exports, daemon --allow-peer-writes, the gate is
total (an ungranted peer learns nothing from malformed/unowned-put replies),
existing server tests grant explicitly, 12 tests port RemoteWriteAuthorityTests
(forwarding has no Python counterpart), refusal bytes pinned to error-vectors.json.
README documents the breaking change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 19:07:32 +00:00
Libby
7a721204ae WIP cultmesh-py: remote write authority gate (not finished, not for merge)
Adds write_authority.py, node.raw_put_target/raw_delete_target, the gate in
_handle_raw_put/_handle_raw_delete with the peer passed in, and serve_node/serve params.
Missing: __init__ exports and imports in facade.py/session.py, daemon --allow-peer-writes,
tests, README and Breaking note. Untested.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 16:59:47 +00:00
d00222761c Merge pull request 'Docs: cultcache-ts publishing is off; tag runs use the tagged workflow' (#32) from libby/publish-docs-cleanup into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Failing after 5m5s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Successful in 36s
2026-11-07 16:53:04 +00:00
Libby
bf5a09bd04 publish-packages: say that cultcache-ts publishing is off; keep the tag pin readable
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m5s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 25s
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>
2026-11-07 16:53:03 +00:00
e75285c067 Merge pull request 'publish-packages: cultcache-ts-v* tags no longer trigger publish' (#31) from libby/disable-cultcache-ts-tag-publish into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
2026-11-07 16:38:12 +00:00
Libby
8569825630 publish-packages: stop cultcache-ts-v* tags from triggering the npm publish
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m38s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 26s
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>
2026-11-07 16:38:11 +00:00
7d29208539 Merge pull request 'cultcache-ts 0.15.0: fold 0.14.1, 0.14.2 and Unreleased; raise consumer ranges' (#30) from libby/cultcache-ts-0.15.0 into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
Publish packages / cultcache-py to PyPI (push) Has been skipped
Publish packages / cultmesh-py to PyPI (push) Has been skipped
Publish packages / cultnet-py to PyPI (push) Has been skipped
Publish packages / @gamecult/cultcache-ts to npm (push) Failing after 9m25s
2026-11-07 16:34:31 +00:00
Libby
7ac9e60bdc cultcache-ts 0.15.0: fold the untagged 0.14.1/0.14.2 and Unreleased into one entry
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m57s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 26s
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>
2026-11-07 16:31:04 +00:00
da515b0f5e Merge pull request 'Fix build:ts: one text-only error factory in cultnet-ts (6 TS2345 in cultmesh-ts)' (#29) from libby/cultmesh-ts-error-message-sites into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 16:27:31 +00:00
Libby
d0fcd3e36e cultnet-ts, cultmesh-ts: one factory for the text-only error message
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m15s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 26s
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>
2026-11-07 16:27:24 +00:00
8477d8dc74 Merge pull request 'npm: scope cultnet-ts, cultmesh-ts, cultmesh-browser under @gamecult' (#28) from libby/scope-npm-packages into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 16:25:15 +00:00
Libby
286a9af389 npm: scope cultnet-ts, cultmesh-ts and cultmesh-browser under @gamecult
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m49s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 36s
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>
2026-11-07 16:25:10 +00:00
655cb2c909 Merge pull request 'cultcache-rs: pin the one-global rule on load_soa (PR #15 gap)' (#27) from libby/cultcache-rs-load-soa-global-test into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
2026-11-07 16:18:28 +00:00
Libby
08715ac55c cultcache-rs: pin the one-global rule on load_soa
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 4m50s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 26s
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>
2026-11-07 16:17:05 +00:00
ec7a009b02 Merge pull request 'cultnet: add unsupported_schema_version to the closed cultnet.error.v0 code set' (#26) from libby/error-code-unsupported-schema-version into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
2026-11-07 16:16:17 +00:00
Libby
d18def46b3 Merge remote-tracking branch 'origin/main' into t26
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m33s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 25s
2026-11-07 12:27:39 +00:00
f1cdbfdfd7 Merge pull request 'cultnet-ts selection/v1 carriers + error code/details' (#20) from libby/cultnet-ts-selection-v1 into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Failing after 6m39s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Successful in 41s
2026-11-07 12:27:32 +00:00
Libby
fc184b0888 Merge remote-tracking branch 'origin/main' into t20
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m31s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 21s
# Conflicts:
#	packages/cultnet-ts/src/contracts.ts
#	packages/cultnet-ts/src/index.ts
2026-11-07 12:26:32 +00:00
1718a50642 Merge pull request 'cultnet-py multi-peer RUDP socket server' (#19) from libby/cultnet-py-multipeer-server into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:26:06 +00:00
Libby
95a4680dfe Merge remote-tracking branch 'origin/main' into t19
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m43s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 46s
2026-11-07 12:25:23 +00:00
33cadb25fa Merge pull request 'cultmesh-py/rs authority route verification' (#18) from libby/cultmesh-route-verification into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:25:15 +00:00
Libby
0c23338204 Merge remote-tracking branch 'origin/main' into t18
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 7m16s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 32s
2026-11-07 12:23:57 +00:00
Libby
0bffcf0f9f cultnet: add unsupported_schema_version to the closed cultnet.error.v0 code set
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-07 12:23:39 +00:00
0fd9d25527 Merge pull request 'cultnet-py: schema catalog describes cultnet.error.v0 as the contract does' (#17) from libby/cultnet-py-error-catalog into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:13:32 +00:00
530acdac20 Merge pull request 'cultcache-py: single-file store lock, fsync and unique staging; msgpack hard; slot-nil reader' (#16) from libby/cultcache-py-store-lock into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:13:21 +00:00
cf0d93d091 Merge pull request 'cultcache-rs: name, index and global lookups (C# parity)' (#15) from libby/cultcache-rs-lookups into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:12:04 +00:00
d76b4033bf Merge pull request 'cultnet error message parity: TS type + Python builder/decoder, shared C#/Rust vectors' (#14) from libby/cultnet-error-parity into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:11:38 +00:00
778a4032c4 Merge pull request 'docs(start): tested CultNet first-call snippets (C#, TypeScript, Rust, Python)' (#11) from libby/start-cultnet-cells into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:10:56 +00:00
Libby
00345b424b Merge remote-tracking branch 'origin/main' into HEAD
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m11s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 26s
# Conflicts:
#	docs/start/README.md
2026-11-07 12:09:33 +00:00
375d1c5ca1 Merge pull request 'docs/start: two-client .io game recipe (CultCache + CultMesh, TS)' (#10) from libby/recipe-io-game into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:09:16 +00:00
5966b4df91 Merge pull request 'ci: drop the Eve getting-started job (GameCult/Eve no longer resolves)' (#25) from libby/ci-drop-eve-checkout into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:07:13 +00:00
a06798fdec Merge pull request 'cultcache-ts: browser-safe entry that excludes the node-only single-file store' (#24) from libby/cultcache-ts-browser-entry into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 12:06:31 +00:00
Libby
4a407f9464 ci: drop the Eve getting-started job; Eve is deprecated and its repo no longer resolves
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m30s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 22s
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>
2026-11-07 11:44:55 +00:00
51803afd02 Merge pull request 'docs: mark samples/eve-browser-network as needing a sibling Eve checkout' (#23) from libby/docs-eve-sample-sibling into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 11:40:50 +00:00
Libby
23279b8e50 cultcache-ts: browser-safe entry that excludes the node-only single-file store
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Failing after 1m19s
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m36s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 36s
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>
2026-11-07 11:37:03 +00:00
Libby
3d7126d036 docs: mark samples/eve-browser-network as needing a sibling Eve checkout
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Failing after 56s
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m31s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 37s
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>
2026-11-07 11:33:51 +00:00
c17b330d70 Merge pull request 'sln: drop EveBrowserNetworkSample so dotnet build CultLib.sln succeeds' (#22) from libby/drop-eve-sample into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 11:33:13 +00:00
Libby
7d654cc277 sln: drop EveBrowserNetworkSample so dotnet build CultLib.sln succeeds
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Failing after 1m1s
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m45s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 41s
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>
2026-11-07 11:32:01 +00:00
824a1545fd Merge pull request 'ci: Windows legs run on workflow_dispatch only; Ubuntu gates push and PR' (#21) from libby/ci-ubuntu-gate into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 11:31:09 +00:00
Libby
2897b4bad8 ci: Windows legs run on workflow_dispatch only; Ubuntu gates push and PR
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Failing after 58s
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 6m37s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Successful in 1m56s
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>
2026-11-07 11:30:22 +00:00
4f4c65d847 Merge pull request 'cultnet-ts: drop unset optional keys from createDocumentPutMessage' (#13) from libby/cultnet-ts-put-nil-keys into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
2026-11-07 11:26:57 +00:00
7e38bf2bf0 Merge pull request 'heimdall-consent: typed records for the answer collector' (#12) from libby/consent-records into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-07 08:28:31 +00:00
Libby
638e71f427 heimdall-consent: re-pin docs to Heimdall PR #6 aaeb175, fix doc line numbers, mark notice state event and registration-checkbox gap as not modelled
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
No behaviour or vector rows changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 04:10:44 +00:00
Libby
420eb9e840 heimdall-consent: pin the one version rule in the golden vectors (4294967295 ok; 4294967296 and 01 refused)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-07 02:40:57 +00:00
Libby
11933ab65b cultnet-ts: selection/v1 carriers (snapshot request, subscribe, raw response), judged against the reference's bytes
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 01:39:57 +00:00
Libby
51941fa61b Carrier vectors: drop the derived hasHop member from the reference's expected JSON
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 01:39:57 +00:00
Libby
f5c2771192 cultnet-py: multi-peer RUDP socket server, with C#-captured transcript parity
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 01:36:55 +00:00
Libby
1d21a93a2d cultnet-ts: cultnet.error.v0 keeps routingHint, code and details, judged against the reference's bytes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 01:36:25 +00:00
Libby
497ff201aa cultmesh-py: pin high-S signature acceptance to the .NET reference
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 01:36:05 +00:00
Libby
280ec74c9a cultmesh-rs: port CultMesh authority route verification
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>
2026-11-07 01:36:05 +00:00
Libby
7b62cf4ddf Capture the reference's selection/v1 carrier and error code/details bytes as interop vectors
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 01:35:54 +00:00
Libby
a36a60b106 cultmesh-py: port CultMesh authority route verification
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>
2026-11-07 01:34:22 +00:00
Libby
af0b1b6817 heimdall-consent: one version rule for every version field (positive, no leading zeros, <= 4294967295)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-07 00:32:09 +00:00
Libby
27d7e48cd6 heimdall-consent: pin key(), MemberTerms read-back, acceptance member/experiment, scope rule; refuse leading-zero versions
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-06 20:53:13 +00:00
Libby
81aeb60c57 cultnet-py: schema catalog describes cultnet.error.v0 as the contract does
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-06 20:51:14 +00:00
Libby
00ecdcf9e6 cultcache-py: read the slot-nil vector; changelog for the store lock and msgpack
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-06 20:50:10 +00:00
Libby
b2434fc7fc cultcache-py: single-file store takes the cross-process lock, stages uniquely and fsyncs
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>
2026-11-06 20:49:32 +00:00
Libby
9548d51a71 cultcache-py: msgpack is a hard dependency; drop the optional-import fallbacks
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>
2026-11-06 20:47:07 +00:00
Libby
95d8be34a7 cultcache-rs: lookups cross-type test, changelog and README entries
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 19:45:56 +00:00
Libby
3250ba49d3 cultnet-py: error_message builder and closed-code/full-key validation in parse_message
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 19:45:52 +00:00
Libby
03b31f1f68 cultnet-ts: CultNetErrorMessage carries routingHint, code (closed union) and details; decode tests on C#/Rust bytes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 19:44:57 +00:00
Libby
2528fbc090 cultcache-rs: lookups parity tests ported from the C# suite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 19:44:36 +00:00
Libby
c4fae30677 cultcache-rs: name, index and global lookups from the C# reference
#[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>
2026-11-06 19:44:36 +00:00
Libby
19f77ae3c2 heimdall-consent: README maps doc lines at Heimdall commit 4e99bfa
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 19:42:44 +00:00
Libby
b6f61a3869 cultnet error vectors: shared hex file for TS/Py, Rust pins it to the C# captures
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 19:42:38 +00:00
Libby
5ab6a6181f heimdall-consent: live answers need text; ties on received_at break by bytewise event_id
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>
2026-11-06 19:42:24 +00:00
Libby
a9a2d51c6a heimdall-consent: match Heimdall's rulings at doc commit 2364979
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-06 18:27:06 +00:00
Libby
3a98eef12f cultnet-ts: drop unset optional keys from createDocumentPutMessage
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Failing after 41s
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 5m58s
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
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>
2026-11-06 18:10:00 +00:00
Libby
309c8c1ba3 heimdall-consent: typed records for the answer collector
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-06 15:35:25 +00:00
Libby
be0502c897 docs(start): tested CultNet first-call snippets for C#, TypeScript, Rust, Python
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Failing after 1m18s
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 56s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Failing after 43s
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
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>
2026-11-06 12:30:20 +00:00
Libby
8777ce4766 docs/start: two-client .io game recipe over CultCache + CultMesh (TS)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Failing after 37s
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Failing after 31s
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Failing after 36s
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
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>
2026-11-06 12:20:06 +00:00
b88beddecb Merge pull request 'heimdall-stake: stake function and splitting-gain report (Rust, zero deps)' (#9) from libby/heimdall-stake into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-06 04:34:33 +00:00
Libby
84f932ec50 heimdall-stake: sybil note and golden vectors
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-06 02:30:58 +00:00
Libby
ae33ba4842 heimdall-stake: stake function and splitting-gain report (Rust, zero deps)
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>
2026-11-06 02:30:58 +00:00
cafffbf6d5 Merge pull request 'docs/start: verbatim drift check for C# and TS first-call blocks' (#7) from libby/start-drift-checks into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-05 12:43:13 +00:00
51445cb2af Merge pull request 'CultMath: Phacelle rows are bit-identical forward, backward and shuffled' (#5) from libby/phacelle-row-order into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-05 11:02:47 +00:00
6ba990d734 Merge pull request 'heimdall-delegation: delegation resolver (Rust, zero deps)' (#6) from libby/heimdall-delegation into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-05 11:02:24 +00:00
Libby
bb25bf04cb docs/start: verbatim drift check for the C# and TypeScript first-call blocks
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-05 09:43:55 +00:00
Libby
5cb5f35dcc heimdall-delegation: README caveat that the cap does not bind under ballot copying
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 09:41:42 +00:00
Libby
e2a39d8aef heimdall-delegation: pro-rata cap, revocation time on receipts, prior art
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-05 05:26:37 +00:00
Libby
30318c18fc heimdall-delegation: delegation resolver with property tests
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-05 00:09:07 +00:00
Libby
bbee623e29 CultMath: Phacelle rows are order-independent (forward, backward, shuffled)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-04 21:35:41 +00:00
66c9f50993 Merge pull request 'docs/start: tested Rust and Python CultCache first calls' (#3) from libby/docs-start-rust-py into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-04 21:34:14 +00:00
83321353e6 Merge pull request 'docs: docs/start onboarding with tested CultCache first calls (C#, TS)' (#2) from libby/docs-start into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-04 20:33:56 +00:00
6d126d4b94 Merge pull request 'CultMath: regenerate lavapipe device bounds at 85ce65fa' (#4) from eureka/fbm-lavapipe-bounds into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-04 20:31:23 +00:00
883271f62c CultMath: regenerate lavapipe device bounds at 85ce65fa
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-04 20:26:13 +00:00
Libby
533b492af2 docs/start: tested Rust and Python CultCache first calls
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-04 20:20:33 +00:00
85ce65fad8 Merge pull request 'CultMath: fbm, and the measured Lipschitz bound of phacelle' (#1) from libby/cultmath-fbm-lipschitz into main
Some checks failed
CultMesh QUIC native / Build win32-x64 (push) Has been skipped
CultMesh QUIC native / Build linux-x64 (push) Has been skipped
CultMesh QUIC native / Release native binaries (push) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (push) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (push) Has been cancelled
2026-11-04 19:19:08 +00:00
Libby
5d8e032d7f cultmath: glsl-parity header names the recording image and lavapipe/Mesa; asin/acos/atan2 oracle tolerance capped at 1 ulp
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-04 17:22:13 +00:00
Libby
52dd5e8acb docs: docs/start onboarding page with tested CultCache first calls in C# and TypeScript
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
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>
2026-11-04 16:11:05 +00:00
Libby
b57501b33e cultmath: phacelle-lipschitz prints usage and exits 2 on missing or malformed arguments
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
Without arguments the tool threw IndexOutOfRange; a malformed number threw FormatException.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This commit builds; docs follow in the next commit.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:22:05 +02:00
Meta Vens
ba05fb7351 Caching tests: the ref-list vector names the test type as the runtime does
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:21:34 +02:00
Meta Vens
972e7b2ed4 Caching: a reference gained on an otherwise identical slot is compatible drift (1.0.x ref-list stores open); 2.0.1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:16:14 +02:00
Meta Vens
eabb50e68c Caching tests: the persisted descriptor carries its own content hash
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:14:21 +02:00
Meta Vens
a88b5930ec Caching tests: ref-list drift test compiles
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:11:28 +02:00
Meta Vens
3e65477f22 Caching tests: a 1.0.x store with a ref-list slot opens at 2.0 (red); reference gain is the only drift excused
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:01:34 +02:00
Meta Vens
68f70ba490 content-vectors.ts-written.json regenerated by the script: five-slot chunk references
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:00:41 +02:00
Meta Vens
2c9043357e cultnet-ts: chunk references carry the element id (slot 4), as the reference reads and writes it; changelogs for the flush-over-damage refusal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:55:15 +02:00
Meta Vens
a70c9844e5 Content acceptance vectors regenerated from the reference: slot 4 is the id, extras sit past it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:55:07 +02:00
Meta Vens
e324ef48fd Content vectors tests: chunk-ref extras sit past the id slot; add id-slot cases (chunk_id_str, chunk_id_int, chunk_id_str8, chunk_6)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:33:49 +02:00
Meta Vens
7964d81acc org.gamecult.cultlib CHANGELOG: flush over damage refuses; stamps judged before the body (Unreleased, major)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:11:21 +02:00
Meta Vens
87957b2d1a Land fixes: ported conditional-commit pins state C2a's identity, not main's (storedAt, message text)
A staged write mints a later storedAt, so the 'rewrite kept storedAt' premise is gone; the pin is that a record rewritten under another schema fails Expect. The variant refusal names the renamed base through ChangedKeys, as C2a's typed exception carries it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 21:06:10 +02:00
Meta Vens
f2cdb7897d Land fixes: directory flush records the schema id it wrote; ported drift and variant tests meet C2a's write set and typed refusal
Directory store now sets StoredSchemaId with StoredVersion after a page write (found by main's OwnPushOfALoadedDriftedEntryKeepsExpectHolding). RecordRewrittenUnderAnotherSchema stages the record, since a flush writes only the write set. Variant conflict test expects CultWriteConflictException.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:59:54 +02:00
Meta Vens
c14b2c65d1 Land fixes: stamp judged before the body, deep stores on the grown stack, ported tests refuse by their typed errors
Stamp rule runs on slot 0 before the body is scanned (JudgeStamp). DescribeHeader names a v<N> header that carries a suffix. Rust store_header walks on the grown stack. Drift tests declare Wide's id compatible (ownership by declaration). Mesh write-guard tests expect the typed refusals.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:49:43 +02:00
Meta Vens
f2b3569822 Merge main at cultlib-unity-v2.0.0 (42dce361) into C2a: one owner per collision
Identity: C2a's StoredSchemaId/StoredVersion owns; main's PersistedSchemaId, PersistedIdentity and both MarkWritten are deleted. Stamp rule: ReadStore judges the stamp before the body. Flush over damage refuses (merge-r1-precedence). Mesh single-file writes route through SingleFileMessagePackBackingStore. TS: store-file.ts commits, C2a's header handling kept. Rust: C2a's readable_store_format in front of the grown decoder. Build not yet verified at this commit.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:41:53 +02:00
Meta Vens
3a320d144d Caching tests: a store's own rewrite of a drifted entry marks it, including a consumer's store; a merge is judged on another writer's change to a held record
Red until stores mark through a protected base member: the directory store's
Push+PushAll leaves the entry's persisted schema id stale, and a store outside
GameCult.Caching has no way to mark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:40:08 +02:00
Meta Vens
e88bf11920 Caching tests: compare the persisted schema id with the writer's descriptor, not a version label
Schema ids are content hashes.

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:05:23 +02:00
Meta Vens
4ecea25c49 Caching tests: Expect and ExpectUnchanged on a record loaded under compatible schema drift
Red at b549d731: an unchanged drifted record fails Expect on both stores, and another writer's normalising flush is not seen. Pins the cases of cultlib-gaps:cut_spec:cut-expect-persisted-schema.r1.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Builds and passes.

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

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

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

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

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

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

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

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

Builds unaffected (docs and a comment).

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

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

Builds and passes.

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

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

Builds. Co-Authored-By below.

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

Builds. Co-Authored-By below.

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

Builds. Co-Authored-By below.

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

Builds. Co-Authored-By below.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:05:27 +02:00
Meta Vens
defe2b80de Document CultMath.Expr: design.md Expressions section, resolver README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:05:19 +02:00
Meta Vens
dc302a96a7 CultMath.Expr: scalar SSA expression graph, builder with substitution, one validator, evaluator, MessagePack shape
ExprGraph (immutable class, one validator in Create), ExprBuilder (hash-consing, dead-node removal, Substitute), ExprOps (the one op table), ExprEvaluator, ExprBindings, ExprGraphException; CultMathExprResolver appended to the default chain after CultMathResolver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:05:18 +02:00
Meta Vens
b136ad9bd4 Merge hands/cultmath-cycles-converters: Cycles shader-node semantics in CultMath, exact to unfused Cycles
Cut cultmath-tapes cycles-converters (spec r2, attempts h1-h3), held by Soul
s3 (verdict cultmath-tapes:verdict:cut-cycles-converters.s3) under ruling
cycles-kernel-reference-ruled. Main carried only docs since the branch base
b3bd8dd5, so the merged code is the Soul-verified head 53f4c213.

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

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

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

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

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

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

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

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

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

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

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

Not yet built or run at this commit.

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

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

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

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

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

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

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

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

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

Builds.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Builds: yes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

All commits build.

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

All commits build.

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

All commits build.

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

All commits build.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:34:30 +02:00
Meta Vens
abf624ab04 C# reference: one payload nesting bound (256) on read and write, nil arm slots refused, bounds vectors
CultPayloadDepth.Check walks MessagePack headers iteratively and refuses the first
container past CultMessagePackSecurity.MaximumPayloadDepth, read or skipped, called by
SerializeUntyped (on the bytes it produced) and DeserializeUntyped (before decoding).
UntrustedData's 500 is replaced by the same value as a backstop. CultUnionGuard refuses
[k, nil] arms. New vectors in tests/vectors/union-parity, with bounds-outcomes.msgpack
as their data; the gate now runs every Check-calling test body and fingerprints bytes
and write times. All commits build.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:34:56 +02:00
Meta Vens
6254da0c88 CultCache: refuse an unknown union arm on read and an unnamed subtype on write
A guard ahead of MessagePack-CSharp's union formatter decides both; the store
decode site names the record and schema.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:35:56 +02:00
Meta Vens
f17ef3d825 Tests: a record a type reads but no type declares is read-only on every cache write path
The relabel tests become refusals: ARecordReadByNameIsReadOnly (name branch)
and AnOlderVersionCannotWriteANewerRecordItReadsThroughTheNewerEntry (a
persisted entry's list). New: every write path refuses before staging,
declaring the id claims the record, a type registered after load claims it,
MintElementIds is refused by a read-only record, the undeclared dropped-slot
case, and a variant of a read-only base lands. The copy test also checks the
write beside the read-only record landed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:34:58 +02:00
Meta Vens
639782c5d6 Research: prior art for signalling a read-only record before the write is refused
Eyes pass for variants❓read-only-record-signal. ORMs, embedded
stores, serializers, file systems and object stores; signal shape and
time-of-check pitfalls for each, with evidence pointers.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 10:47:31 +02:00
Meta Vens
2a3773a463 CultCache: the store owns a file's bytes, not the record object
Delete RawBytes from CultPersistedRecord and CultSchemaCatalogEntry, with every
drop-on-assign hook and the catalog's content guard. A decoded record or snapshot
carries no stored bytes, so an in-place edit of its payload or overrides can never
be shadowed by cached bytes, and the public snapshot and record serializers encode
every record from its fields.

SingleFileBackingStore now reads through ReadStore, which returns the snapshot and,
parallel to it, the exact slice the file holds for each catalog entry and record. Its
durable index keeps, by record key, that slice and its SHA-256 (hashed when a
comparison asks), which is the record's version. A write re-encodes every record
the writer staged and copies every other record, and every catalog entry it keeps,
as the slice the file holds. The directory store keeps its manifest's catalog slices
the same way. SerializeSnapshot/DeserializeSnapshot hooks become
SerializeStore/ReadStore plus SerializeCatalogEntry.

A staged but unchanged non-canonical record is now re-encoded canonically and gets
a new version; an untouched one keeps its bytes and version.

Tests: in-place edits through the snapshot API (payload, override) and through a
held document (collection, nested object) are written as edited; an untouched
non-canonical record survives a neighbour's write byte for byte; a staged one is
re-encoded. The cost test counts record hashes.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 07:04:48 +02:00
Meta Vens
bd71385d5c C# write set tests: pin the re-mint and staleness branches, the foreign refusal's cause, and Expect on the stored delta
Ported from Soul's s2 probes: P5 skewed clock (equal storedAt re-mint, mint from the durable one),
D (directory store re-mint, as a store-kind parameter on the later-storedAt test), E (a disappeared
base refuses a variant write), P3 (foreign refusal whose reload fails carries the cause). New: a
foreign record a refusal reloaded is not seen as moved, and a writer that finds the file gone has
read nothing. ExpectOnAVariant is restored to pin that Expect tests only the stored delta: a stale
base reaches the write conflict, not a mismatch. Per-commit build not verified.

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:55:12 +02:00
Meta Vens
56743c61a3 C# write set tests: the second writer in the Expect-on-a-variant test reads what the first wrote
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:18:06 +02:00
Meta Vens
2649dd1f6b C# write set tests: a readable vector holding a variant refuses a write the cache never read, and Expect on a variant is retried after the reload
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:15:42 +02:00
Meta Vens
45d6866d31 C# write set: qualify MintStoredAt, and pin the variant write into a plain store another writer changed
ebd66e6f and cc8604ae do not build: the re-mint named MintStoredAt without its class.
Two tests pin the staleness rule for a write that would add a variant to a plain store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:12:38 +02:00
Meta Vens
cc8604ae94 C# write set: tests and contract text for the refusal that reloads and the header-only staleness rule
The merge tests become the refuse, reload, retry rule; new tests pin copied records that
cannot be decoded, the later storedAt over a replaced record, a batch that removes a staged
key, and a Mesh write into a variant store. The persistence and composition contracts state
the rule for the parity cuts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:11:17 +02:00
Meta Vens
ebd66e6f5b C# write set: a record written over another is stored later than the one it replaces
ApplyWriteSet re-mints an upsert's storedAt when the durable record it replaces is not
earlier, so A-B-A on the same wall clock cannot read as unchanged. Two stale whole-view
comments are corrected.

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

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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 05:02:50 +02:00
Meta Vens
2774a7fe8b C# write set tests: the real-store check, and tests for the five mutants that survived
CULTLIB_REAL_STORE names a copy of a real store: one staged write leaves every other record and
catalog entry byte for byte. New: a copied record changed after the load keeps a marked header; what
this cache wrote counts as known id-free; a batch wins over a staged write of its key; a write is
judged on the staged edits plus the variants another writer left in the file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 03:38:29 +02:00
Meta Vens
88934bcb76 C# write set tests: fix the five tests the first run named, and the Mesh test's catalog comparison
The unconditional-commit last-writer-wins test now keeps another writer's record; the load-minted ids
stay for MintElementIds after an unconditional write; a detached store marks the variant it writes, not
the one it copies; a variant write supplies the indexed override; the conditional/unconditional
comparison uses one batch (a conditional commit refuses staged writes by design).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 03:20:17 +02:00
Meta Vens
a7114a1f9f C# write set: tests for copy-forward, raw catalog carry, header rule and the inverted lay-back rules
New WriteSetTests (single-file and directory stores, flush and commit): another writer's write and
removal survive, a store that appeared after open is kept, an unstaged in-place change is not
written, a copied record keeps the catalog entry the store holds now, an entry's unknown trailing
fields survive byte for byte, conditional and unconditional commits write the same bytes, the
header drops only when every copied record is known id-free, v2 exactly with a variant. Mesh: a
write copies neighbours forward and replaces only its key. The lay-back and whole-view tests and
the flush-restamp tests are rewritten or removed with the rules they pinned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 03:16:49 +02:00
Meta Vens
aa186513a5 C# stores: every flush, commit and CultMesh single-file write applies a write set to the durable store
SingleFileBackingStore.ApplyWriteSet replaces WholeView and ReplaceDurable: under the lock, from the
durable snapshot read there, it removes staged removals, writes staged upserts and copies every other
record and the catalog entries they reference unchanged. PushAll, CommitBatchCore (conditional and
unconditional, one apply) and CultMesh's write call only it. Catalog entries read from a store keep
their exact bytes and are written verbatim while their decoded fields are unchanged. Derive no longer
lets a registered entry publish an id nothing staged sits under. HeaderForWrite is the one header rule
for the single-file and directory stores. A write is judged against variants another writer left in the
file. Tests of the deleted rules are rewritten in the next commit.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Not yet compiled or run at this commit.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:46:39 +02:00
Meta Vens
bad511f5ac Merge CultCache R1 (f789c8d1) into C2a: one reader per runtime, R1's failure taxonomy
R1's rules win where both sides decided the same question: a zero-byte file is
not a store (refused as unreadable in every runtime), only nothing at the path is
empty, a dangling link is an I/O error, refusals never echo stored values and echo
a header only as cultcache.store.v<digits>, writes report Rejected, NotReplaced
and ReplacedNotDurable. C2a's rules win on what it added: the v3 element-id
header, variants, whole-file proof, typed unreadable errors in C#, TypeScript and
Python. Rust keeps R1's CultCacheStoreUnreadable{path, kind}; C2a's
StoreUnreadableError is deleted. The readability manifest's zero-byte row is
refuses x4.

Not yet built or tested: verification follows in the next commits.

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 19:16:19 +02:00
Meta Vens
32f5700b2a Semver check tests: double trailing slash, and a process started outside the target repo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:03:00 +02:00
Meta Vens
15ccdbb953 Test: a staged write a commit landed is no longer staged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:52:42 +02:00
Meta Vens
c49c76ee50 Research: prior art on records a writer does not own (formats, embedded stores, literature)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:51:58 +02:00
Meta Vens
a9741dbf57 Tests: pin the staged-key rule, the multi-key refusal and the carried record's stamp; a pull no longer clears staged keys
A pull runs only on a clean store, and a store is clean exactly when nothing is staged, so the pull's clear was redundant and is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:47:47 +02:00
Meta Vens
84a34b8735 Semver check tests: pin the broken-manifest failure text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:35:27 +02:00
Meta Vens
86eea40bce One owner for the foreign-record refusal: it reaches the cache, names every key, and refuses only what the cache wrote
CacheBackingStore.RefuseForeign now decides, for both store kinds, which written keys a foreign record refuses, drops the
store's staged change and the cache's copy of each through the Loaded path a pull uses, and throws one conflict naming every
refused key. The single-file store tracks the keys staged since its last load or write, as the directory store already did, so
a clean copy of a record another writer made foreign is carried, not refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:34:09 +02:00
Meta Vens
f40069dd88 Semver check tests: scope the cleanup assertion to the checker's own temp dirs, cover a broken manifest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:34:05 +02:00
Meta Vens
6a3dae8954 Semver check: run tests from outside the repo, pin cleanup, usage and restore failure
The checker now runs with the target repo as its working directory in tests,
so the tool manifest is found from the script's own location as it is in the
release scripts. Adds tests for usage errors, a missing changelog, backslash
and trailing-slash baseline paths, temp-file cleanup, a restore failure and a
missing dotnet failing closed, the five-line failure excerpt and the
five-diagnostic cap. Drops two unreachable defaults and an unreachable
spawn-error branch from the checker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:32:38 +02:00
Meta Vens
ed31a97689 Semver check tests: cache built fixture assemblies by content
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:01:37 +02:00
Meta Vens
bc15dba81b Test: the owner's-id case of the moved-record lay-back publishes the owner's catalog entry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:00:52 +02:00
Meta Vens
0a98004bc4 Tests: pin the Stryker survivors in the lay-back and the directory refusal
The file's record moves when only its storedAt or only its schema id differs from the loaded one; a
staged removal is refused and dropped with the rest; a refusal leaves staged what it did not refuse.

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:54:41 +02:00
Meta Vens
b0f7d59c96 Directory store: a refused foreign overwrite leaves the pending set with its failed write
WriteGeneration left the refused key in the dirty and deleted sets, so every later generation
refused it. The refusal is now one check before any staging, shared by flush and commit, that
removes the refused keys from the pending sets. Tests cover both store kinds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:51:54 +02:00
Meta Vens
c246e0c5ae Single-file store: lay back an unowned record only while the file still holds it as loaded
A whole-view write laid back the loaded bytes of an unclaimed record over whatever the file held,
reverting its owner's newer write and moving storedAt backwards. It now lays back only while the
file holds the loaded (schemaId, storedAt); a newer record is carried as the file has it and a
removed one is not written back. The v3 header is decided by records that are written as the cache
holds them, so a laid-back id-less record no longer marks the store. A document changed in place is
written as a write of the record, not dropped. A refusal drops the store's copy of the refused
records, so it no longer refuses every later write.

Does not build with the directory store untouched? It builds; the directory twin follows.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:59:52 +02:00
Meta Vens
904c00ac81 Test: pin the Stryker survivors in the foreign-record and lay-back cut
- The lay-back fixture gains an older entry listing the id ahead of the
  one that owns it, and a test where another writer re-describes the id
  after the load: a whole view (flush and commit) lays back the entry it
  loaded. Kills the survivors in Publisher's own-id preference, the
  laid-back entry's precedence in the written catalog, and the commit's
  choice of that catalog.
- A store whose file is gone lists no foreign records.
- The registry's public resolution still refuses an unclaimed id, naming
  the schema and the id (was NoCoverage).
- A directory load retried after a writer's commit lists each foreign
  record once.
- Deleted LaidBack.ExceptWith(replaced): equivalent, since an upserted key's
  new entry already carries what the cache would be told.

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:20:11 +02:00
Meta Vens
a11d30fce2 C#: a whole-view write lays back a record under an id its type does not own
C2a F2, widened by Self: records and catalog entries a runtime does not own
survive its writes byte for byte. The schema-identity probe showed a C#
rewrite restamping every other runtime's record to its sha256 id and
dropping their catalog entries.

- The single-file store remembers, per key, a record it loaded under an id
  its type neither owns nor declares compatible (resolved through the
  catalog's schema name or an entry pointing at a local id), with the entry
  that published it. While the store still holds the entry it loaded, a
  whole-view write (flush or unconditional commit) lays that record back
  with its id, storedAt, bytes and catalog entry. A write of the record
  itself still stores it under the type's own id.
- A record published by an entry under the type's own id is this schema's
  and is re-encoded as before (the registered entry wins its own id).
- A laid-back record's load-minted element ids stay in memory only, so
  MintElementIds still rewrites it.
- The rolling-deploy tests lose their premise: an older version's whole
  view no longer sheds a newer record's member. They now pin that it lays
  the record back and the newer cache's condition holds; the A-B-A test
  keeps F6's defence through an explicit older write.
- Contract: the stamp and the lay-back rule in the persistence format.

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:05:45 +02:00
Meta Vens
3d285f4923 Variants map: cross-runtime schema identity fork (C1 recommended); F-ID.1-3 open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:04:55 +02:00
Meta Vens
7dd19baac3 Test: a store holding a schema the cache does not register opens and carries it
The corrupt-store test pinned the old refusal of an unknown schema at open.
Under C2a F2 that record is foreign: the store opens, lists it, and opening
still rewrites nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:52:08 +02:00
Meta Vens
2ee707d05e cultmesh-rs: no permanent send failure is dropped when a rejection ends the session
A reply sent earlier in the same poll could fail permanently and then be
dropped when a rejection returned first; and a refusal fragment that can
never be sent must end the session naming its error. Both now reach
end_session as the unsendable end. The test makes the refusal itself
unsendable: the peer's goodbye names the error, nothing is counted as a
lost datagram, and the other peer is served.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:50:43 +02:00
Meta Vens
05221dae74 C#: a record no registered type claims is carried untouched, and a write over it is refused
C2a F2, operator: "CultCache never destroys or relabels a record it doesn't
understand; keeps it byte-for-byte, refuses writes that would overwrite it."

- The registry's claim decision (Claimant) is split out of resolution. It
  never decodes or compares shapes, so a store classifies a record from its
  header. The last branch, "No local CultCache schema matches", now means
  foreign instead of a refused load.
- Both store kinds keep a foreign record out of the cache and list it in
  CacheBackingStore.ForeignRecords (key, schema id, catalog name, storedAt).
- Single-file store: a whole-view write lays back every foreign record the
  file holds now, with its catalog entry, and keeps a marked header while it
  carries one (it cannot see their ids). A staged write, a commit (onto the
  file or whole view) and a whole view over a key the file holds as foreign
  refuse with CultSchemaConflictException naming the key and id. A merge
  judgement never decodes a foreign record, and one that replaced a held
  record takes it away.
- Directory store: a foreign record's page is never read; a generation that
  would replace or remove one refuses before any page is written.
- Contracts: hard rejection no longer lists "no compatible local schema
  candidate"; the persistence format says what a foreign record is.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:37:40 +02:00
Meta Vens
c996ae7892 Caching: a single-file refusal echoes a header only in its known shape
RequireSingleFileFormat echoed any header string. It now names one only in
the shape cultcache.store.v<digits> and otherwise gives its byte length.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:30 +02:00
Meta Vens
2d7a3c78eb Caching tests: an empty store file is refused; a missing file or [0x90] is empty
Pins the C# side of the cross-runtime rule the Rust, Python and TS runtimes
now share. C# already refused a zero-byte file (the snapshot reader runs out
of bytes at the array header); this test keeps it that way. Its exception is
still EndOfStreamException, an IOException: that overlap belongs to R3's C#
work.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:17 +02:00
Meta Vens
9adea36680 CultNet TS: a second type for one schema is refused, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." CultNetDocumentRegistry.register silently replaced
the binding for a schema id, so two types could claim one schema and the
last one registered won.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:17:45 +02:00
Meta Vens
98999997ce CultCache: a second type for one schema is refused, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." C# now holds one type per schema, as TypeScript,
Python and Rust already do.

- Delete IsExactWireAlias and the branch that registered an identical second
  type by CLR type only. A second type with the same schema name and version
  is refused with CultSchemaConflictException naming both types and the
  claimed id, whether it is identical, declares other compatible ids, or has
  other members. The untyped DuplicateSchemaRegistration refusal and the
  separate SchemaIdClaimedTwice path collapse into that one refusal.
- Owner-beats-lister (batch 10) is unchanged: one type owns an id, others may
  list it as compatible.
- The per-name version index stays; it serves v1/v2 of one schema name, not
  aliasing.
- Tests: the alias-registration tests are replaced by one refusal test over
  identical, re-declared and re-shaped second types, in both registration
  orders.
- cultcache-store-composition.md drops the AsSchemaAlias stale-protection note.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Odin store-recovery map, cut R1.

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

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

Odin store-recovery map, cut R1.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:36:10 +02:00
Meta Vens
5b86bc9faf Test: a flush storing a record under a new id gives it a later storedAt when the file no longer holds it
Defends the id half of the whole-view restamp rule on its own: the record C# loaded
under v1's id is gone from the file when a v2 cache flushes it back, so there are
no stored bytes to compare, and only the changed id says it is a new store.

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:28:55 +02:00
Meta Vens
9629aa119b C#: a type listing an id registers beside the type that owns it; the owner wins
Batch 9 refused any overlap between two types' ids, which forbade the migration
CompatibleSchemaIds exists for: v2 declares v1's id while v1's class stays
registered (Soul F8). The rule is now owner beats lister. A type owns its own
schema id; any number of types may list an id as compatible beside its owner,
registering in either order. A record under an id resolves to its owner, else to
the one type listing it. An id no type owns and several list names no single
type, so a record under it is refused, typed (CultSchemaConflictException naming
the id and the listers), where it resolves: refusing at registration would
depend on whether the owner had registered yet.

A second owner of one id is still refused at registration, typed. A schema id
hashes the schema's name and version, so the only second owner is a type of the
same name and version, which the name-and-version check already meets; the
separate own-id lookup could never fire and is gone.

Tests: an owner and a lister register in both orders, the owner's id resolves to
the owner, the lister's own id to the lister, and without the owner to the
lister; two listers register in both orders, a record under their id is refused
typed with no owner and resolves to the owner with one, registered before or
after them; a cache with v1 beside v2 reads each version's records as its own
type, in both registration orders. The emitted-fixture note says why a fixture
is emitted now that v1/v2 pairs no longer need hiding from the shared registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:26:06 +02:00
Meta Vens
188d4ad88d C#: a whole-view write that stores a record differently mints it a later storedAt
A condition names a record by (stored schema id, storedAt), and that name must
say which bytes are stored. A single-file whole-view write (plain flush, or an
unconditional commit) re-encoded every record under its own type's id with the
storedAt it loaded, so the pair could name two byte strings: v1 shed a member
keeping storedAt, a second v2 cache restored v2's id keeping it again, and a v2
cache holding the original bytes committed its condition (Soul F6, A-B-A).

Now such a write stores a record under a later storedAt whenever it stores it
differently: under another id than it was loaded under, or with other bytes than
the file holds at that storedAt (a record written with the ids it minted at
load, for one). A record stored exactly as the file holds it keeps its storedAt.
The store's entries and the cache's take the id and storedAt the write gave them.

Tests: the A-B-A through commits and through plain flushes, with Expect and
ExpectUnchanged; a flush that re-encodes a record restamps the cache that
flushed (a condition holds straight after, SM12); a record stored unchanged keeps
its storedAt through commits and flushes; persisting load-minted ids gives a
later storedAt. The rolling-deploy test asserted v1's rewrite kept storedAt; it
now asserts a later one, since that rewrite is a new store of the record.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:26:37 +02:00
Meta Vens
4b668002aa cultmesh-ts: pin each served-bound limit with a fixture that can tell the rules apart
The queue-bound puts carry a source runtime id, which the registry does not
store, so sizing the record as received instead of as served is caught.
The test states the served record's shape itself rather than asking the
registry, so a registry that served a different record is caught too. A
two-document snapshot over maxPayloadBytes proves a reply that cannot be
sent reaches onError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:15:49 +02:00
Meta Vens
0eb7434ab1 cultmesh-ts: the RUDP document server fragments its replies and refuses unservable puts
The server reassembled fragmented puts but sent every reply as one UDP
datagram, ignoring its own maxFragmentBytes and maxPayloadBytes, and a
datagram that failed to leave vanished. An 80 KB note put in 1 KiB
fragments was admitted and its snapshot never arrived, with no error.

Replies now fragment by maxFragmentBytes (default
CULTMESH_RUDP_DOCUMENT_SERVER_DEFAULT_MAX_FRAGMENT_BYTES, 1200), and
maxPayloadBytes bounds both inbound frames and replies. A put whose
served-alone reply (sized by the registry, under the shortest message id,
with the send path's encoder) would exceed maxPayloadBytes, 65535
fragments or maxPendingReliablePackets is refused before onDocumentPutRaw:
onError receives a CultMeshRudpUnservableDocumentError naming the sizes,
and the peer a cultnet.error.v0. Every datagram send reports failure to
onError, as does a snapshot reply that cannot be sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:09:50 +02:00
Meta Vens
2cf1e07bac cultnet-ts: the registry sizes the reply that would serve a raw put
CultNetDocumentRegistry.createRawSnapshotResponseForPut(put, messageId)
returns the snapshot response that would serve the put's document alone:
the envelope applyRawDocumentPutMessage stores, as createRawSnapshotResponse
serves it. A server can judge a put by the size it would be served at.

applyRawDocumentPutMessage and the new method share one envelope builder,
and the served record comes from the snapshot path's own record builder,
so the size and the stored record cannot drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:09:50 +02:00
Meta Vens
e079ba1ad3 cultmesh-kotlin: the bind self-test observes a wildcard, not its family
A JVM wildcard socket is dual-stack, so a client bound to 0.0.0.0 reports the
IPv6 wildcard as its local address and the self-test's first check failed on
Linux. Assert that a client for a remote endpoint binds a wildcard, and keep the
exact loopback checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:52:26 +02:00
Meta Vens
e6ddcdd6e1 GameCult.Mesh: an RUDP client binds by its endpoint, as the other runtimes do
CultMeshRudpSocketOptions.BindHost defaulted to 0.0.0.0 for servers and
clients alike (a58daa54, so clients could reach remote routes). That bound an
IPv4 socket for every client, so a client for an IPv6 endpoint could not send,
and a client for a loopback endpoint listened on every interface.

BindHost is now nullable and unset by default. Unset, a client binds loopback
for a loopback endpoint and the unspecified address of the endpoint's family
(0.0.0.0 or ::) otherwise, matching cultnet-rs, cultnet-ts, cultnet-py and
cultmesh-kotlin. A server keeps binding 0.0.0.0. An explicit BindHost wins for
both. The test that pinned the old default's value is replaced by tests of
what each endpoint binds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:50:58 +02:00
Meta Vens
cf62461643 cultmesh-kotlin: an RUDP client for a remote endpoint binds the unspecified address
The CultMesh RUDP client entry points defaulted bindHost to 127.0.0.1. On
Windows a socket bound to loopback cannot send off the host (os error 10051),
so a default client could not reach a remote endpoint.

cultNetRudpClientBindAddress owns the rule: loopback for a loopback endpoint,
otherwise the unspecified address of the endpoint's family (0.0.0.0 or ::).
bindHost defaults to null on every client entry point, which uses the rule. An
explicit bindHost still wins; a CultMesh server still binds 127.0.0.1 unless
told otherwise. The self-test pins all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:49:31 +02:00
Meta Vens
fdb8a60af9 Test: conditions on records loaded under an old id, before a rewrite and after a directory pull
A plain record and a variant loaded under a declared old id pass Expect and
ExpectUnchanged while the file is unchanged, and again after a whole-view commit
rewrites them (the variant case pins the id surviving resolution). A directory
store pull rereads a record whose manifest id alone changed, so a condition on
it holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:48:40 +02:00
Meta Vens
9954652266 cultnet-py, cultmesh-py: an RUDP client for a remote endpoint binds the unspecified address
create_rudp_schema_transport and CultMesh.create_rudp_client (and create_client
and connect_client above them) defaulted bind_host to 127.0.0.1 on an AF_INET
socket. On Windows a socket bound to loopback cannot send off the host (os
error 10051), so a default client could not reach a remote endpoint.

cultnet-py now owns the rule in rudp_client_bind_host: loopback for a loopback
endpoint, otherwise the unspecified address of the endpoint's family (0.0.0.0
or ::). bind_host defaults to None on the client paths, which use the rule; the
socket family follows the bind host. An explicit bind_host still wins; a
CultMesh server still binds 127.0.0.1 unless told otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:44:06 +02:00
Meta Vens
b68ddfe357 cultnet-ts, cultmesh-ts: an RUDP client for a remote endpoint binds the unspecified address
CultMesh.createRudpClient defaulted bindHost to 127.0.0.1 on a udp4 socket, and
invokeCultNetOperation bound 127.0.0.1 unconditionally. On Windows a socket
bound to loopback cannot send off the host (os error 10051), so neither could
reach a remote endpoint.

cultnet-ts now owns the rule in rudpClientBindHost: loopback for a loopback
endpoint, otherwise the unspecified address of the endpoint's family (0.0.0.0
or ::). The operation client and the CultMesh client both use it, and the
CultMesh socket family follows the bind host. An explicit bindHost still wins;
a CultMesh server still binds 127.0.0.1 unless told otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:41:44 +02:00
Meta Vens
fd394d54db cultmesh-py: refuse a put the local server could never serve
With max_snapshot_bytes set, CultMeshLocalServer stored any raw put while
refusing every snapshot response over that size, so it could hold a
document no request would ever return.

_handle_raw_put now asks the database for the snapshot response that would
serve the document alone, built from the envelope the put would store (the
resolved local schema id, not the put's alias) under an empty message id,
the shortest this server answers. It is measured with the snapshot path's
own encoder. Over the bound the put is refused before it is stored, as
cultnet.error.v0 code document_unservable with responseBytes and
maxSnapshotBytes. At the bound it is stored and served at exactly that size.

The database's put and snapshot paths share one envelope builder and one
served-record builder, so the preview and the stored record cannot drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:41:34 +02:00
Meta Vens
88d2d72405 cultnet-rs: an RUDP client for a remote endpoint binds the unspecified address
CultMeshRudpSocketOptions defaulted bind_host to 127.0.0.1 for servers and
clients alike. On Windows a socket bound to loopback cannot send off the host
(os error 10051), so a default client could never reach a remote endpoint such
as rudp://10.77.0.1:17872.

bind_host is now Option<String>. Unset, a server still binds 127.0.0.1; a client
binds loopback for a loopback endpoint and the unspecified address of the
endpoint's family (0.0.0.0 or ::) otherwise. An explicit bind_host wins for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:37:21 +02:00
Meta Vens
027bd8aa47 cultmesh-rs: size an admitted put under the shortest message id CultNet encodes
The served-size check built its response under an empty message id, which
the CultNet encoder refuses, so every put was rejected as
ResponseEncodingFailed. No snapshot request can carry an id shorter than
one character, so a one-character id gives the smallest response that could
ever serve the document; that is the size a put is now judged by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:33:16 +02:00
Meta Vens
85288a5a47 cultmesh-rs: refuse a put the document server could never serve
The server admitted a put up to the 4 MiB per-session budget but serves at
most max_snapshot_response_bytes (1 MiB by default), so it could accept a
document no snapshot request would ever return.

DocumentPutRaw now sizes the snapshot response that carries the document
alone under an empty message id, the smallest response that could serve
it, with the snapshot path's own encoder. Over max_snapshot_response_bytes
it is refused as DocumentUnservable, naming both sizes, before the sink
sees it. At the bound it is admitted and served at exactly that size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:20:55 +02:00
Meta Vens
c9cdad127e cultmesh-rs: an application rejection carries a typed reason
CultMeshRudpApplicationRejection.reason was a String, so a consumer could
only log a refusal, never act on its cause. It is now
CultMeshRudpRejectionReason, one variant per refusal the document server
makes, with sizes and limits as fields. Display keeps the existing
sentences, so `{}` logging reads the same.

The snapshot path encodes its response through one helper, which put
admission will share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:20:41 +02:00
Meta Vens
5607d9703d C#: the directory store's loaded id is its manifest's; the variant fixture names itself
A directory-store record's identity on disk is its manifest entry, which is what
a commit condition compares; the page holds its own copy of the id. A load now
takes StoredSchemaId from the manifest, so a manifest rewritten under an older
id (AConditionOnARecordUnderADeclaredOldIdHoldsInTheDirectoryStore) is the id
the condition expects. Canonicalisation had hidden the page and manifest
disagreeing in that fixture.

The whole-view test's variant overrides its name: a variant may not share its
base's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:52:15 +02:00
Meta Vens
aab72f3d47 Ack Cut D D2: a refused send withdraws the fragments the flow window queued behind the failed one
Rust hub and C# listener: 1,100 one-byte fragments, the first can never be
sent, and nothing of the send stays queued.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:49:46 +02:00
Meta Vens
abb922cd57 Python: one registered document carries each schema id; a second claimant is refused typed
register_document_type refuses a document whose own or compatible ids overlap
another registered document's, raising SchemaConflictError (empty record key,
both schema names, a message naming both document types), as TypeScript does.
Before, the record resolved to whichever document the dict held first (Soul F4).
With one carrier per id, resolution by id is a single pass. SchemaConflictError
takes an optional message for this use.

The test that pinned owner-over-lister resolution inside one cache becomes a
refusal test in both orders; shared compatible ids and a shared own id (neither
declaring its own) are refused in both orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:52 +02:00
Meta Vens
4182c2502b C#: one registered type carries each schema id; a second claimant is refused typed
Registration refuses a type any of whose ids (its own, or one it declares
compatible) another registered type already owns or declares, with
CultSchemaConflictException naming the id, both schema names and both CLR types.
TypeScript already refuses this (cult-cache.ts); before, C# kept whichever type
registered first (Soul F4). An exact wire alias must now also declare the same
compatible ids: a type differing only in its declaration had it dropped when it
registered second (H4), and is refused the same way.

Exact aliases that agree in every respect still register by type, and a record
under their shared id still resolves to the first registered (H3). Removing that
would delete CultMesh's same-schema aliases; it is left for an operator ruling.

Tests: both registration orders for a shared compatible id, an owned id declared
by another type, and a same-schema type with a different declaration; an alias
with the same declaration still registers. The registry tests use the shared
EmittedDocumentTypes helper in place of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:52 +02:00
Meta Vens
5c8af219f9 C#: a commit condition compares the id a record was loaded under with the file's, exactly
Each stored document carries StoredSchemaId: the id its record sits under in its
store, the one it was loaded under until a write puts it under its type's own id.
Expect, ExpectUnchanged, the reload and merge identity checks compare
(StoredSchemaId, storedAt) with the store's bytes exactly. CanonicalSchemaId is
deleted: canonicalising the disk id let an older version's whole-view rewrite
(same storedAt, restamped to its own id, a member shed) pass a newer cache's
condition (Soul F1), and failed every condition on a record resolved by name or
through a file entry listing the local id (F3). A whole-view write marks every
record it wrote as stored under its type's id, in the store and the cache.

Tests: Soul's rolling-deploy probe as AWholeViewRewriteByAnOlderVersionFails...
(Mismatch for Expect and ExpectUnchanged, nothing derived lands, a pull shows the
rewrite and the retry commits); F3's H1, H1b, H2 pass Expect and ExpectUnchanged
when nothing changed; conditions after a whole-view commit hold for a plain
record and a variant. BareDeck is emitted at run time (EmittedDocumentTypes) so
the shared registry never sees it beside DeclaringDeck; the CanonicalSchemaId
tests and their Claimant fixtures go with the function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:40 +02:00
Meta Vens
590744f6f1 Ack Cut D D2 (C#): the oversized-send test compares the outstanding count with the one the listener's own unacknowledged Accept holds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:30:25 +02:00
Meta Vens
69acee38e7 Ack Cut D D2 (Python): a refused Accept leaves the peer with the goodbye only
The peer is told its session ended, and no Accept resend follows it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:27:33 +02:00
Meta Vens
d391770abb Media FEC: own the rs-gf256-v1 math, drop reed-solomon-erasure
Operator ruling, 2026-09-30: "FEC: Please own the math."

reed-solomon-erasure 6.0.0 pulled lru 0.7.8 (RUSTSEC-2026-0253, unsound) and
instant (RUSTSEC-2024-0384, unmaintained) into every cultnet-rs consumer.
media_fec.rs now implements the construction its docs already state, read
against the crate's source rather than its names: GF(2^8) reduced by 0x11D
with 2 as generator, the n x k Vandermonde matrix over the evaluation points
0..n-1 with 0^0 = 1, E = V * inverse(top k rows), parity = rows k..k+m of E,
and recovery through the inverse of E's rows for the first k present shards,
which is the row choice the crate made.

About 130 lines: const log/exp tables, a Gauss-Jordan inverse, the encoding
rows, and one combine loop. It allocates only the block's shards and three
k x k matrices, so the decoder's bound is still set by the wire validators
(<= 256 shards of <= 64 KiB). The shard functions no longer return errors
that only the crate's own checks could raise; every caller already bounds k,
m and the shard length.

The generator test is deleted with the crate; the fixture keeps its output and
names the commit that ran it. New: a seeded property test over 300 random
(k <= 48, m <= 24) geometries, any e <= m erasures recover exactly and m + 1
are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:26:40 +02:00
Meta Vens
d866cd5916 Media FEC: pin rs-gf256-v1 to 35 known-answer cases from the reference crate
tests/fixtures/media_fec_rs_gf256_v1.kat holds what reed-solomon-erasure 6.0.0
produced at 73bf4421: the parity bytes of every standard video geometry
(k = 1..=16), standard audio 4+2 at 1, 37 and 1300 bytes, a 4000-byte shard,
all-zero and all-0xff data, and the policy's edges (1+1, 2+1, 255+1, 1+255,
2+254, 128+128, 200+56, 250+6), plus 120 erasure patterns mixing data and
parity. Its header defines the data, so another runtime can meet it.

Two tests drive the codec's public audio path, which takes any (k, m): every
case's parity matches byte for byte, and every pattern recovers the erased data
from the fixture's own parity bytes. Both pass against the crate here, before
it is replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:25:30 +02:00
Meta Vens
ac5c86033b Ack Cut D D2: pin the disconnect, the resend loop, the Python resend, ack and accept paths
The injected permanent failure now takes precedence over the injected transient
one, so a test can end a session on a peer that is also losing datagrams and
count exactly which datagrams were attempted afterwards. New tests: a hub
disconnect that can never be sent returns the error; a peer or session ended
in the resend loop is sent nothing more (Rust hub, document server, C#); the
goodbye reaches the peer; the Python server ends the session on a permanent
failure in a resend, an ack and an accept. The oversized-send test compares the
outstanding count with the one the hub's own unacknowledged Accept already
holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:25:04 +02:00
Meta Vens
73bf44212e Media FEC: a generator for rs-gf256-v1 known answers from the reference crate
Before reed-solomon-erasure is replaced by an owned GF(2^8) construction, it
produces the vectors the owned code must meet. The ignored test encodes every
standard video geometry (k = 1..=16), standard audio 4+2 at 1, 37 and 1300
bytes, a 4000-byte shard, zero and 0xff data, and the policy's edges (1+1,
255+1, 1+255, 2+254, 128+128, 200+56, 250+6), and reconstructs four erasure
patterns per case, asserting the crate returns the original data.

It is run once and deleted with the crate; git keeps its provenance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:22:33 +02:00
Meta Vens
1c71d98c71 Ack Cut D D1 and D2 (Python): a vanished peer is idle, and a send that can never succeed ends only that peer's session
D1: on Windows the closed port of a vanished peer is reported by the next
recvfrom as ConnectionResetError (WinError 10054), and the RUDP thread returned
on it. It is now idle, as in the Rust document server, the Rust hub and the C#
listener. Both tests fail on Windows without the fix and pass with it.

D2: is_permanent_send_error (EMSGSIZE, EINVAL, EAFNOSUPPORT and their Winsock
equivalents) separates a datagram that can never be sent as built from a lost
one. The server ends that peer's session with a goodbye whose reason names the
error and never the thread; transient failures stay counted losses.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:19:47 +02:00
Meta Vens
6cf92329b6 Ack Cut D D2 (C#): a send that can never succeed throws from a caller-directed send and ends only that peer's session inside a poll
The listener swallowed every send failure as a counted loss. A send now splits
into permanent (MessageSize, InvalidArgument, AddressFamilyNotSupported: the
datagram can never be sent as built) and transient (everything else, still a
counted loss). SendSchema throws the SocketException and queues nothing, with
the packets it created withdrawn and their sequences reissued; a permanent
failure after an earlier fragment left ends the session. Inside a poll the
failure ends that peer's session, PeerDisconnected carries a reason naming the
error, and the poll is never failed. Tests pin each path, the classification,
BytesSent after a healthy send and a zero failure count.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:19:46 +02:00
Meta Vens
64ed64a894 Ack Cut D D2 (Rust): a send that can never succeed is the caller's error inside a send and ends only that peer's session inside a poll
The hub and the document server swallowed every send failure as a counted
loss, so a 70,000-byte reliable frame returned Ok and was resent forever.
Failures now split in two. Permanent means the datagram can never be sent as
built (EMSGSIZE, EINVAL, EAFNOSUPPORT and their Winsock equivalents,
is_permanent_send_error in cultnet-rs); everything else stays a counted loss.
A caller-directed hub send returns the error and queues nothing: the packets
it created are withdrawn and their sequences reissued, so the peer never sees
a gap (a permanent failure after an earlier fragment left ends the session).
Inside a poll the failure ends that peer's session with a goodbye whose reason
names the error, reported to the hub caller as Disconnected, and never fails
the poll. The document server ends the session the same way and reports a
snapshot response it can never send as an application rejection. Tests pin
each path, the classification per OS, BytesSent after a healthy send and a zero
failure count for healthy sends.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:19:46 +02:00
Meta Vens
436b423762 Ack Cut 3 F1: the acknowledged-bytes tests pin pruning on the ack that closes the gap
The bytes counted above a lost packet stop counting once it is acknowledged;
every runtime now asserts that another MiB is admitted after the gap closes.
Rust prunes on expiry only when something expired, so the acknowledgement path
owns the pruning it can observe.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:12:23 +02:00
Meta Vens
0daf6f4cc0 Ack Cut 3 F1: Rust pins that an expired lowest packet releases the acknowledged bytes above it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:07:28 +02:00
Meta Vens
d7eacd1011 Ack Cut 3 F1: the 4 MiB flow bound counts acknowledged bytes above the lowest unacknowledged sequence
The bound summed only packets still awaiting an ack. Once frames above a lost
packet were acknowledged they stopped counting, so a receiver could be made to
hold more than 4 MiB behind one loss, and its hold buffer ended the session.
The sender now keeps the payload size of each acknowledged sequence above the
lowest pending one until the lowest advances past it, in Rust, TypeScript,
Python and C#. Tests per runtime: one lost packet, four acknowledged 1 MiB
frames, the fifth refused until the gap closes; and the floor moving to a new
lowest sequence.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:04:19 +02:00
Meta Vens
42ba0f082a Ack map: Cuts 3 and D Soul rulings; record the stalled-sender liveness gap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:02:32 +02:00
Meta Vens
c99f29c309 Test: the selection guard probe passes the descriptor's new compatible-ids argument
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:48:38 +02:00
Meta Vens
b295bf1688 C#: pin the directory store's conditions, drop the merge-judging canonicalisation
A condition on a record under a declared old id is tested against the directory manifest too, and
a duplicate declared id is listed once. The merge-judging comparison keeps its plain schema-id
equality: a record it re-reads is only judged again, and no test reaches a difference.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:39:11 +02:00
Meta Vens
0cef517d83 C#: a schema name that drifted no longer refuses a record resolved by its schema id
The contract says the name a catalog entry carries is metadata and a schema renamed under a
stable id opens in every runtime that holds the id. The C# shape comparison refused any name
drift, so a record under a declared old id (the entry carrying another name and a different
content hash) or under a renamed stable id could not open. The comparison keeps the slot and
type checks; a record resolved by name has the same name by construction.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:36:43 +02:00
Meta Vens
da0315e69e Test: TypeScript writes a record under an older schema id, C# declares it and rewrites it
The C# interop note declares a compatible id and the peer gains a rewrite mode; the TypeScript
cross-runtime test has TS write a record under that id and another schema name, C# open and
rewrite it, and C#, TypeScript, Rust and Python open the result.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:32:47 +02:00
Meta Vens
d589e61f67 C#: a blank declared compatible schema id is dropped, not refused
A type that failed to build a descriptor poisoned the shared registry's discovery of every
loaded document. A blank id is now filtered like the type's own id, and the test declares one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:31:28 +02:00
Meta Vens
186e5b29ae C#: a document type declares the older schema ids its records may sit under
CultDocumentAttribute.CompatibleSchemaIds, as TypeScript and Python already name it. The
registered catalog entry lists the ids after its own, the registry resolves a record under one
to the type (its own id first, then the first registered type that declares it), and a
condition on a record compares schema ids through that resolution, so a record still under a
declared old id counts as the record the cache holds. The cache already stamps every record it
writes with the descriptor's own id; the store writers still change no identity, so a record a
commit does not rewrite stays under the id the file holds it under, published by the registered
entry. Without the declaration such a commit is refused, typed, as before.

Contract and XML docs describe the declaration.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:30:05 +02:00
Meta Vens
2125635dff Ack Cut D: the document-server send-failure tests take initial_sequence as an Option
Ack Cut 1c made CultNetRudpSocketTransportOptions.initial_sequence optional; the
new client helper still passed a bare u32.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:26:51 +02:00
Meta Vens
5926804e2b Python test: a document that owns an id is resolved before one registered earlier that lists it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:14:38 +02:00
Meta Vens
1a04b0d788 Rust: a registered type owns its schema id, so only it may name an id over records the file holds
The previous cut let any supplied type name a schema id over the arrived one, which let a raw
envelope of another type retype the records under an id (the dynamic-payload test refused it and
failed). A rename under a stable id is a write of the registered type that owns the id; a Rust
registered type's id is its entry type, so a supplied envelope whose type is its schema id names
the id and any other type under an occupied id is refused, typed, with the file left as it was.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:10:06 +02:00
Meta Vens
0996fc96f5 Ack Cut 3: the hold-buffer test acknowledges the gap by hand
Cut 1's the_hold_buffer_takes_exactly_1024_frames test held 1,024 frames behind a
sequence the receiver never sees, by sending them all above one unacked packet.
Cut 3's flow window stops a sender 1,023 sequences above its lowest unacked packet,
so the test's sender was refused at frame 1,023. The missing packet is now
acknowledged by hand (the receiver never saw it), so the window slides and what
the test probes is the receiver's hold buffer, not the sender.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:07:30 +02:00
Meta Vens
86dc2e33d0 Contract: the cache owns the schema-id stamp, records resolve by schema id, writes may not retype
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:16 +02:00
Meta Vens
e6f9b2f8c1 TypeScript: the type check runs across tiers; a renamed stable id reads across runtimes
The writer refuses a write that would change which type an existing record resolves to, unless
the entries of both the existing records and the written ones own the id (a rename; the registered
descriptor wins). Tests pin the arrived-overwrites-registered guard in both id orders, the
content-hash tie-break against the compatible-list order, sorted id processing and the cross-tier
refusal; a cross-runtime test has TS write a schema renamed under a stable id and TS, Rust and
Python open it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:14 +02:00
Meta Vens
9cee5e3f42 C#: no store writer restamps a record; the directory store has one source of arrived entries
Deleted the heuristic that rewrote a record the cache held "unchanged since read" (same key and
storedAt) under the registered id, in the single-file commit and in the directory generation. It
could overwrite a record another writer changed while keeping its storedAt. A commit onto a file
now writes its batch and leaves every other record as the file holds it; a record under an id only
an arrived entry lists is refused, typed, with the file left as it was. Deleted the directory
store's _durableCatalog, a second stale-able source of arrived entries beside the manifest read
under the lease.

Tests: the lost-update probe, the two refusals that replace the restamp tests, the
arrived-overwrites-registered guard in both id orders, the content-hash tie-break against the
compatible-list order, and refusal determinism across record arrival order.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:12 +02:00
Meta Vens
6f219bd919 Rust: a record resolves by schema id, and the registered type names a renamed schema id
pull_all_backing_stores resolves a record to a registered type by its schema id (a registered
type's id is its entry type) before the catalog name is tried. The writer tells the entries the
file already holds from the ones its caller supplies, and a supplied type names the schema id over
an arrived one, so a stable-id rename is not a conflict; two types of one tier under one id still
refuse the write.

Tests: a stable-id rename write, a renamed schema opening by id, and an id not overruled by a name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:11 +02:00
Meta Vens
ebe15ab8c9 Python: a record resolves to its document by schema id, and the type check runs across tiers
A record now resolves to the document that owns its schema id, else one that lists it as
compatible, before the catalog name is tried; a schema renamed under a stable id opens, and an id
that names one document is not overruled by a name that names another. The name resolves a record
only when no local document has the id.

The writer refuses a write that would change which type an existing record resolves to, unless the
entries of both the existing records and the written ones own the id (a rename, the registered
descriptor wins).

Tests pin the arrived-overwrites-registered guard in both id orders, the content-hash tie-break
against the compatible-list order, sorted id processing, the cross-tier refusal and the rename, and
a cache whose document does not list the id a loaded record sits under.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:04:07 +02:00
Meta Vens
b8c00117b3 Merge origin/main into hands/ack-cutD 2026-09-30 10:58:23 +02:00
Meta Vens
ede87f7423 Ack Cut 3: the flow-window tests shake hands the way Cut 1 requires
An Accept now counts only while the session's own Connect awaits it, and reliable
data is refused until the handshake seeds the watermark, so the tests' bare Accept
no longer connected the session. The helpers now pair a sender and a receiver
through CreateConnect/AcceptConnect in all four runtimes; the sender's Connect
is acknowledged, so nothing is pending on it when the tests begin. Rust also
takes initial_sequence as an Option.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:57:53 +02:00
Meta Vens
8c71b5c6dd Merge origin/main into hands/ack-cut3
# Conflicts:
#	packages/cultnet-ts/src/rudp.ts
2026-09-30 10:52:49 +02:00
Meta Vens
29385586bd Ack map: Cuts 1, 1b and 1c merged (05b5705d); record the residuals
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:52:07 +02:00
Meta Vens
05b5705d69 Merge ack Cuts 1, 1b and 1c: one watermark, the write belongs to its session, a new generation is recognised
Cut 1: one watermark owns ordered delivery, seeded only by the handshake.
Cut 1b: every ending drops what the session still owed; flush is bound
to its generation. Cut 1c: a Connect repeats only by its sequence;
stale data and stale Connects are handled as TCP does (challenge Ack);
an unanswered attempt retries at +window-1; the session draws its
initial sequence. Wire bytes unchanged in all four runtimes. Five Soul
passes. Public option types changed: initial_sequence is optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:28:05 +02:00
Meta Vens
acbf031c5b Tests: the arrived entries' hashes sort before the registered one's, so a writer that ignores the tiers picks the arrived one
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:16:53 +02:00
Meta Vens
ca0b62023d Writers: drop the same-tier dedupe refusal (unreachable), and pin tie order and the registered lister
Two entries of one tier that share an own id are refused when the id is carried, so a refusal at dedupe could never fire.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:14:33 +02:00
Meta Vens
7a5b64ad88 TypeScript and Python writers: records are compared for type within a tier only
A read-back record's type is its schema's name and a supplied one's is the document type, so records of different tiers are never
compared; two supplied (or two arrived) records of different types under one id still refuse the write.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:13:44 +02:00
Meta Vens
38ebc2015b Tests: the file-level cases keep the schema name, which the cache requires of a persisted schema under a registered id
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:07:52 +02:00
Meta Vens
01d6055cd0 Tests: C# writer derivation, rename, stale hash, refusals
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:05:03 +02:00
Meta Vens
e77246b657 Writers derive the catalog from the records they write; the merging is deleted
Every writer (C# single-file and directory, TypeScript, Python; Rust already did) chooses, for each schema id a record carries, one
entry that publishes it: the entry that owns the id (a registered descriptor, else an arrived entry), else one that lists it as
compatible. A registered descriptor wins over an arrived entry with the same own id. Entries are written as chosen, with no union of
compatible ids and no dependence on arrival order; an entry no record needs is not written. Two entries of one tier that share an own id
and disagree on the schema name, records of different types under one id, or a record no chosen entry publishes refuse the write with a
typed error (CultSchemaConflictException, SchemaConflictError) naming the id, the names and a record key, and the store is left as it was.
CultSchemaCatalogEntry.MergeById and the Python and TypeScript merge paths are gone. A cache-held record unchanged since the file was read
is rewritten under its registered id (C# single-file and directory), so an entry that lists an older id is not needed to publish it.
Changelogs for cultnet-py (removed export) and cultcache-py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:03:34 +02:00
Meta Vens
4da8bbe291 Ack Cut 1d batch 2 (C#): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:06 +02:00
Meta Vens
c45edf5858 Ack Cut 1d batch 2 (Python): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:06 +02:00
Meta Vens
9c562adaf2 Ack Cut 1d batch 2 (TypeScript): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

The cultmesh-ts document server has a test that it does not acknowledge by name a reliable packet its session did not receive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:06 +02:00
Meta Vens
7b46b4c481 Ack Cut 1d batch 2 (Rust): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:05 +02:00
Meta Vens
f58604a40d Ack map: connect-attempt rulings; record the receive-window divergence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:45:21 +02:00
Meta Vens
1569f013c6 Ack Cut 1d (C#): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

An accepted peer now stamps its liveness on accept, so one that goes silent times out.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:52 +02:00
Meta Vens
e56c96d6be Ack Cut 1d (Python): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:51 +02:00
Meta Vens
ba873d04c4 Ack Cut 1d (TypeScript): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

The cultmesh-ts document server acknowledges what the session received, not every reliable packet by name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:51 +02:00
Meta Vens
e02d027019 Ack Cut 1d (Rust): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:51 +02:00
Meta Vens
1a4289cac6 Test: the directory merge test starts from a stale content hash
The directory store's preference for the entry being written was not pinned: nothing distinguished the entry's hash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:11 +02:00
Meta Vens
29e60353f0 Merge media FEC Cut 3: one Reed-Solomon codec for audio and video
rs-gf256-v1 (construction documented, independent implementation matches
every KAT); audio parity with checked block arithmetic; every encoded
record must fit the producer's datagram budget (default 1431), and no
media record exceeds 64 KiB on encode, decode, send or reassembly. The
"audio" channel is retired. Muninn and Ratatoskr bump in lockstep at
Cuts 4-5. Three Soul passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:05:11 +02:00
Meta Vens
59f9e9aedb One local schema id in cultmesh-py; catalog writers merge only the same schema; a compatible entry before its owner reads as the owner
cultnet-py's resolve_document_and_schema_id_for_raw_record is replaced by resolve_document_for_raw_record, which resolves the
document only; cultmesh-py keys previous values by the local schema id, so a snapshot under a compatible id or the schema name
reports an update with its previous value. Writers (C#, TypeScript, Python) merge same-id entries only when their schema names match;
another schema's entry is dropped or the record gets a default entry under its own id, and a record's own id is claimed before an
entry that lists it as compatible. The C# single-file writer prefers the entry being written now, as the directory writer does, so
name, content hash and members come from the registered schema and the compatible ids are the union. Vector:
compatible-before-owner-v3. A cultnet-py test's schema path no longer depends on the working directory.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:01:00 +02:00
Meta Vens
c12f6df0ea Ack map: stale-data and stale-Connect rulings after the Cut 1c Soul pass
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:46:56 +02:00
Meta Vens
c8326ce3dc Vector: the entry that owns the id does not list itself, so the reader order is observable
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:31:12 +02:00
Meta Vens
4d93dc858e Media wire tests: hold an unfragmented media packet to the ceiling
Mutation run showed the unfragmented receive path of the RUDP session was
undefended (only fragmented reassembly was tested).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:29:05 +02:00
Meta Vens
2fbdf514ce Test: the registry resolves a persisted id to the entry that owns it
The vector test alone did not distinguish the order on C#: the compatible entry's own list reached the same local schema.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:26:27 +02:00
Meta Vens
bcf6ce5cf6 Test: the directory store's wrong-format refusal is the typed exception naming the manifest
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:22:58 +02:00
Meta Vens
df892e40e3 Ack Cut 1c tests (cultmesh-rs): the budget test sets a snapshot limit that fits its session budget
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:20:26 +02:00
Meta Vens
51967aff68 Contract: an id resolves to its owner first, and writers merge entries that share an id
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:19:16 +02:00
Meta Vens
f96a0e7f7a Catalog writers merge entries that share an id; every reader resolves an id by its owner first
Entries that share a schema id are one schema seen by different writers, so the writer lists every id any of them lists, in any
order: C# single-file and directory (CultSchemaCatalogEntry.MergeById), TypeScript and Python. Rust synthesises one entry per id and
has no such merge. An id that one entry owns and another lists as compatible names the owner in Rust, TypeScript, Python and the TS
inspector, as C# already did (own-id-over-compatible-v3). Also: a directory manifest in an unreadable format is the typed refusal
naming it, cultnet-py applies and reports a record under the local schema id, and Rust's error prints its cause once.
Vectors for the Mesh layout discriminator: a nil and an integer at catalog slot 5.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:17:59 +02:00
Meta Vens
7c149bf0c8 Media wire tests: fix helper name clash and move fixtures to the last-pts deadline rule
The sized parity helpers collided with the existing fixtures of the same name,
so they are now sized_audio_parity and sized_video_parity. Two existing cases
encoded the old base-pts deadline rule: the corruption table expects the new
last pts_ticks message, the edge-of-bound cases admit a deadline at the last
pts, and the 254-shard audio case sets a deadline that covers its last slot.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:14:22 +02:00
Meta Vens
5b42eb0250 Merge publication Cut 4: CultNetDatabase rebuilt on the cache journal
Kind, prediction and log are decided in the journal under the cache gate;
the reorder buffer, drainer and prediction gate are gone. Q-P5 A: one
typed CultNetShardLogException per journal call, after publication. The
four old log bugs are fixed. No shard owns a schema means no write on any
path (typed refusal, wire code unowned_schema) and no row served; replicas
relay skipped entries unchanged. Four Soul passes; follow-ups recorded in
docs/cultcache-publication-cut.md (crash-safe log store is a hard
prerequisite before any live daemon enables durable shard logs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:13:38 +02:00
Meta Vens
1bbc0ace69 Media wire: the encoder refuses a record over the ceiling too
A producer that builds an oversized record without going through the FEC
policy would otherwise learn only when the receiver silently dropped it.
encode_media_wire_record now refuses anything over
GAMECULT_MEDIA_MAX_WIRE_BYTES, with the decoder's message. The decode ceiling
test pads a valid record by one byte to stand in for a peer that sent one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:11:26 +02:00
Meta Vens
51e9a550f9 Media wire: bound every record by one ceiling, and validate audio parity against its last pts
Removing the 720-byte shard cap left the decoder with no per-record size
bound: RUDP reassembles up to 16 MiB and the media channel set no cap, so a
4 MiB parity record decoded and recovery work scaled with it.

GAMECULT_MEDIA_MAX_WIRE_BYTES (64 KiB, one maximum UDP datagram, ~7x a jumbo
frame) is now the ceiling. decode_media_wire_record refuses a larger record,
the RUDP session caps the media channel to it on send and on reassembly (and
the profile advertises it), and MediaFecPolicy::validate refuses a
max_wire_bytes above it so no producer can promise a record the decoder drops.

validate_audio_parity_record now checks the deadline against the last slot's
pts, not the base, so recovery can no longer produce a packet that violates
deadline_ticks >= pts_ticks.

Tests: ceiling +-1 at decode for audio and video parity; jumbo record accepted;
session send and reassembly on the media channel only; profile advertisement;
the audio deadline boundary and recovery refusal; a policy over the ceiling
refused; a budget at the ceiling end to end; audio parity larger than every
packet it protects (the budget check relies on it). MediaFecPolicy docs tell
callers to build by struct update from STANDARD.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:10:07 +02:00
Meta Vens
c5063ab3c4 Publication map: record Cut 4 final-Soul follow-ups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:59:05 +02:00
Meta Vens
2d2fad1f45 Tests: the directory store names its refused manifest and keeps the entry that publishes a compatible id
The two paths the previous batch left unpinned. Replication routing comments in cultnet-py and cultnet-ts say what the payload-based
and binding-based resolution is and is not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:45:31 +02:00
Meta Vens
c08d982947 Vectors: the malformed current-layout catalogs carry one entry and only its records
The older-layout reader refused the second current-layout entry on its own, so an unconditional fallback still refused the vector.
With one entry, only a store not in the older layout is refused for its malformed slot, and the fallback's condition is pinned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:32:18 +02:00
Meta Vens
ebb8a468dd Ack map: record the unpinned schema/latest channel arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:28:56 +02:00
Meta Vens
27edda82ea Python and TypeScript: tests for the store's publishing rule and the admission stamp; cultnet-py keeps its payload-based resolution
cultnet-py resolves a raw record whose schema id no document lists by the schema version its payload opens with, and three
cultmesh-py tests (foreign UI-generated ids) depend on it, so that resolution is restored pending a ruling. The compatible-id
replication test goes through the real apply_raw_document_record and the reopened store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:28:17 +02:00
Meta Vens
f66fd3d714 Ack Cut 1c tests (cultmesh-rs): the restarted client's message is the same size as the first
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:03:27 +02:00
Meta Vens
2647f10d20 C#: a rewrite keeps the catalog entry that publishes a record's schema as a compatible id
The single-file and directory writers dropped an entry whose own id no record carried, leaving a record under a compatible id with
no catalog entry that publishes it: the same store nothing could open. An entry now stays while it publishes any id a record
carries. The conditional-commit readability test over compatible-id-only-v3 pins it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:02:06 +02:00
Meta Vens
b9ce601844 Python: a record is stamped with its registered schema id and the catalog publishes it; cultnet no longer infers a schema from a payload
put_envelope, put_envelopes and load stamp the registered schema's id, and the store writer publishes every record's id, so a
replicated record under a compatible id no longer leaves a store nothing can open. cultnet-py resolves a raw record by the schema
ids its documents list and nothing else.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:18 +02:00
Meta Vens
39a9de3b23 TypeScript: a record is stamped with its registered schema id and the catalog publishes it; the inspector reads what the reader reads
putEnvelope and admission stamp the registered schema's id, and the store writer publishes every record's id (the entry the
envelope carries when it lists that id, else a default), so a replicated record under a compatible id no longer leaves a store
nothing can open. cultnet-ts stops preserving the incoming id. The inspector refuses a store of the wrong slot count and a record
whose schema the catalog does not publish, instead of recovering one from the payload.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:18 +02:00
Meta Vens
c6f87fd3bf Rust: compatible schema ids publish a schema, and the refusal's message carries its cause
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:17 +02:00
Meta Vens
969d78de39 C#: compatible schema ids publish a schema on open, Mesh takes only the older layout, the refusal names its file
A record whose schema id the catalog lists only as a compatible id now opens (the cache resolved a persisted schema by entry id
alone). CultMesh's fallback to the older catalog-entry reader fires only for a store in that layout, decided by the entry's slot 5,
not for any decode failure, so a current-layout store with a malformed slot is refused as CultCache refuses it.
CultStoreUnreadableException.Path names the refused file. New vectors: compatible-id-only-v3 (accepted by all four) and three
current-layout catalogs with one malformed slot. The contract states the writer's invariant.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:54:17 +02:00
Meta Vens
0abb8d4380 Ack Cut 1c: a new generation drops what the old one owed before the queue is asked for room
A bounded session whose queue still held an unacknowledged Accept for a peer
that vanished refused the next Connect; the old server-mode path reset before
accepting, so this would have been a regression. accept_connect and
create_connect now end the generation first in every runtime, and a test per
runtime holds a one-slot queue to it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:50:23 +02:00
Meta Vens
b90fdc45aa Pin the zero-field policy refusal to the validator, not to a downstream failure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:46:14 +02:00
Meta Vens
a179e9c7c6 Ack Cut 1c tests (cultmesh-rs): a restarted client starts with a fresh payload budget
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:40:06 +02:00
Meta Vens
86c170fa0b Ack Cut 1c tests: a reconnect forgets what the old server sent; an Accept for an abandoned Connect does not connect; the cultmesh-py server test observes what the server sent
Two rules the first mutation pass found undefended in every runtime: a new
Connect on a session that had a peer clears the old peer's received window (a
restarted server's sequences overlap the old ones), and ending a session
abandons its pending Connect. The cultmesh-py server test now counts the
server's own data packets by sequence, so a peer replaced on a retransmitted
Connect is seen, and asserts a restarted client inherits no subscription.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:34:23 +02:00
Meta Vens
9d0701d4a7 Ack Cut 1c tests (cultmesh-ts): the document server replaces a restarted client and keeps a retransmitted Connect's session
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:15:12 +02:00
Meta Vens
7969d4dbf4 Ack Cut 1c tests (C#): the sequences that follow the Connect
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:03:17 +02:00
Meta Vens
d5bc28e207 Vectors: manifest row for missing-schema-versioned-v3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:02:24 +02:00
Meta Vens
ed61462a70 Tests: the recovery tests assert the refusal
Rust, TypeScript and Python each had a test that a record missing its catalog entry is recovered from its payload; each now asserts
the store is unreadable.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:58:47 +02:00
Meta Vens
a39d04e53e Ack Cut 1c tests (C#): sessions that stood in for a handshake with a hand-built Accept now handshake
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:58:30 +02:00
Meta Vens
45f98c5638 Ack Cut 1c tests (Python): the timeout probe uses a clock past the epoch clock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:58:29 +02:00
Meta Vens
8c950c06ea Ack Cut 1c tests (TypeScript): the request follows the Connect in sequence
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:57:31 +02:00
Meta Vens
acce92d406 Ack Cut 1c tests (Python): sessions that stood in for a handshake with a hand-built Accept now handshake
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:56:00 +02:00
Meta Vens
50d6eac405 Ack Cut 1c tests (TypeScript): sessions that stood in for a handshake with a hand-built Accept now handshake
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:55:39 +02:00
Meta Vens
02fa6ab854 Ack Cut 1c tests (cultmesh-py): the server keeps a peer on a retransmitted Connect and admits a restarted client
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:53:09 +02:00
Meta Vens
3a3de56341 Store readers: a schema is never recovered from a record's payload
The persistence contract requires the catalog to publish every schema its records name and specifies no recovery, so the catalog
is the store's own description: Rust, TypeScript and Python stop inferring a schema name from a payload's first field, as C#
never did, and a store naming an unpublished schema is unreadable in all four. missing-schema-versioned-v3 pins it with payloads
that open with a schema version.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:53:01 +02:00
Meta Vens
9741bfe2fa Ack Cut 1c (TypeScript): export randomInitialSequence from the package index
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:52:42 +02:00
Meta Vens
536e650d82 Ack Cut 1c tests (C#, Python): Connect generation, handshake-seeded watermark, restarts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:52:37 +02:00
Meta Vens
64eb3bd4b2 Ack Cut 1c (TypeScript): export randomInitialSequence; the repeated Connect test names acceptConnect
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:52:37 +02:00
Meta Vens
5ff1ee3222 Ack Cut 1c (C#): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
The session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. AcceptConnect answers both, ConnectRepeats is the predicate for servers
that keep one session per peer, and AnswerRepeatedConnect is private. Server
mode admits a Connect from a new endpoint and the session decides; the listener
no longer compares payloads (the payload stays application evidence).

CreateConnect on a session that has had a peer starts a new generation. An
Accept counts only while the Connect is pending and only if it names it. The
watermark is seeded by the Connect or Accept alone; reliable data before that
is neither remembered, acknowledged nor delivered. The default initial sequence
is random in [1, 2^31) in every options type. An ICMP connection reset no
longer sets DisconnectReason: nothing ended. The interop peer flushes before it
disconnects.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:51:14 +02:00
Meta Vens
2e56b46656 Ack Cut 1c (Python): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
The session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. accept_connect answers both, connect_repeats is the predicate for servers
that keep one session per peer, and answer_repeated_connect is private.
Server-mode transports admit a Connect from a new endpoint and the session
decides; the cultmesh-py server replaces a peer only when the Connect does not
repeat, so a retransmit no longer resets its subscriptions.

create_connect on a session that has had a peer starts a new generation. An
Accept counts only while the Connect is pending and only if it names it. The
watermark is seeded by the Connect or Accept alone; reliable data before that
is neither remembered, acknowledged nor delivered. The default initial sequence
is random in [1, 2^31) wherever a default is produced, and a timeout names
itself as the disconnect reason.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:51:14 +02:00
Meta Vens
efefbe270a Ack Cut 1c (TypeScript): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
The session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. acceptConnect answers both, connectRepeats is the predicate for servers
that keep one session per peer, and answerRepeatedConnect is private. The
server-mode transport no longer computes its own repeat; the cultmesh-ts
document server no longer compares payloads (the payload stays application
evidence).

createConnect on a session that has had a peer starts a new generation. An
Accept counts only while the Connect is pending and only if it names it. The
watermark is seeded by the Connect or Accept alone; reliable data before that
is neither remembered, acknowledged nor delivered, and createAckForReceived
acknowledges by name only what was received. The default initial sequence is
random in [1, 2^31).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:51:13 +02:00
Meta Vens
86909c1d1e Ack Cut 1c tests (TypeScript): Connect generation, handshake-seeded watermark, restarts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:49:46 +02:00
Meta Vens
40c670d652 Ack Cut 1c tests (Rust): read past another client's acks; a repeat's Accept was already acknowledged
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:48:32 +02:00
Meta Vens
450ec33352 Ack Cut 1c tests (Rust): Connect generation, handshake-seeded watermark, receipts, boundaries
Session and transport tests for the rules the change makes: a reconnect on the
same session and a server that accepts a new client are not stranded by what an
ended session owed (Soul F1); a retransmitted Connect after data has flowed does
not reset; a Connect with another sequence is not a repeat whatever it carries;
an Accept counts only when it names the pending Connect, and not after a local
disconnect (F6); reliable data before the Accept is neither delivered nor
acknowledged (F2); the default initial sequences are random; the hub, server
mode and the cultmesh-rs document server admit a restarted client; a receipt
asked of another transport reads Invalidated (F4); a timed-out flush names the
timeout; the hold buffer takes exactly 1024 frames and 64 channels (F5).

Existing tests that handshook with a hand-built Accept now use assume_connected
or a real handshake, and pin distinct sequences for peers that must differ.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:42:25 +02:00
Meta Vens
f7f8b17b39 Ack Cut 1c (Rust) tests: the repeated Connect test names accept_connect
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:35:17 +02:00
Meta Vens
a0007f3aeb Ack Cut 1c (Rust): a Connect's sequence says whether it repeats; the handshake alone seeds the watermark
A session records the sequence of the Connect that started its generation. A
Connect repeats exactly when the session is connected and carries that
sequence; anything else ends the generation, forgets the peer and accepts a new
one. accept_connect answers both cases, connect_repeats is the predicate for
servers that keep one session per peer, and answer_repeated_connect is gone.
create_connect on a session that has had a peer starts a new generation.

An Accept is honoured only while this side's Connect is pending and only if it
names that Connect. The watermark is seeded by the Connect or Accept alone;
reliable data before that is neither remembered, acknowledged nor delivered.
create_ack_for_received acknowledges by name only what was received.

The default initial sequence is random in [1, 2^31). The hub, the cultmesh-rs
document server and the server-mode transport no longer compare payloads or
reset on every Connect. A receipt carries its session's random id. A timeout
names itself as the disconnect reason.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:32:40 +02:00
Meta Vens
91d464a038 Rust test: a refused store's cause is the error's source
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:23:02 +02:00
Meta Vens
be5ba22264 Vectors: the variant renames itself and the legacy catalog publishes every schema it holds
variant-slot-v3 overrides the variant's name, which the reader requires of a variant, so C# reads it and the others refuse the
fifth slot. The legacy-layout catalog vectors carry an entry per schema, so a store the reader accepts differs from the one it
refuses only by the bytes after it. missing-schema-v3 is refused by C# too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:21:23 +02:00
Meta Vens
67575c4ec4 Store reader: a record whose schema the catalog does not publish is unreadable
C# read such a store on flush and commit and refused it on open, with an untyped InvalidOperationException from the cache. The
store reader now refuses it with CultStoreUnreadableException, so open, flush and commit give one verdict, as Rust, TypeScript
and Python already do. The readability open test builds a cache with a codec, so a variant store opens as it would in use.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:19:35 +02:00
Meta Vens
fdafa15022 Python: StoreUnreadableError for a store file it cannot read; a store is exactly three slots
The refusal is a ValueError subclass with the cause chained. A store with more than three slots is refused instead of read.
The readability test walks every vector in the folder, asserts the error type and cause, the header a replaced file carries
and what it holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:04 +02:00
Meta Vens
c001358e8c TypeScript: a flush asks the same full reader as an open, and refuses with StoreUnreadableError
pushAll, push and delete read the file through the one store reader that pullAll uses, so a store holding a variant or a bad
body is not replaced by a header-only check. A store with fewer or more than three slots is refused. The readability test
walks every vector in the folder, asserts the error type and cause, the header a replaced file carries and what it holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:03 +02:00
Meta Vens
ed7fd14c1c Rust: a rewrite asks the same full reader as an open, and refuses with StoreUnreadableError
push_all and every rewrite read the file through the one store reader (framing, header, schema catalog, records), so a v3
store holding a variant or a bad body is no longer replaced by a header-only check. The refusal is one typed error with the
cause as its source. The readability test walks every vector in the folder, asserts the error type and the header a replaced
file carries, and the truncated v1 and v2 stores are refused again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:03 +02:00
Meta Vens
cd07f03a9f Mesh: the older-catalog reader takes only a store the current catalog layout cannot decode
A store refused for its framing, slot count or a record keeps that refusal instead of being re-read with the older layout, so
the record it names is the record the message names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:15:02 +02:00
Meta Vens
436eed73e4 Store readability: one typed refusal, one full reader, one store shape
CultStoreUnreadableException (a NotSupportedException) is the one refusal for a store file this runtime cannot read, thrown
by the single-file reader, the rewriting paths and CultMesh's reads, with the cause as the inner exception. The reader is exactly
one array of three slots (or an empty array): a header alone and an extra top-level slot are refused instead of read as empty or
skipped. CultMesh's older-catalog fallback is the same reader with the older entry reader, so it also requires one array.

Vectors: bad-body, header-only, missing-schema, variant-slot, extra-top-slot in the readability set, and a legacy-catalog store
with and without bytes after it for CultMesh. The persistence contract says what is true.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:06:44 +02:00
Meta Vens
e18624f72b Merge origin/main into hands/variants-c2a
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 03:58:11 +02:00
Meta Vens
51a6a6c1c1 Media channel test: observe expiry through the next send, where the session reclaims packets
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:51:45 +02:00
Meta Vens
a813c09b6d Media FEC: enforce the datagram invariant at the producer, refuse overflowing audio blocks
The 720-byte shard cap stood in for "an encoded record fits one datagram" and
failed it at legal maximum-width identifiers. The invariant is now checked where
records are produced: protect_video_frame and protect_audio_block take the
producer's provenance, encode each record, and refuse the block when one exceeds
MediaFecPolicy::max_wire_bytes (default 1431, the IPv4/1500 MTU budget; smaller
paths pass less). The fixed cap is deleted; it contradicted a larger budget and
bounded nothing the decoder does not already hold in memory.

Audio block arithmetic lives in one function, audio_slot, used by the protect
path, the wire validator and recovery. The validator refuses a parity record
whose last packet id or pts overflows; recovery uses the same checked function
instead of adding.

Tests pin the policy 256-shard bounds, block identity on block_count and
block_data_start, the k=16 m=4 known answer, and the burst property
(worst always-recovering burst = blocks x smallest m for 1..=399 chunks), and
the wire behaviour of the media channel profile (delivery and expiry). Docs
state the rs-gf256-v1 construction and that audio playout gates on pts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:50:11 +02:00
Meta Vens
ae0f545a06 Readability vectors: a legacy array with bytes after it
Rust's read path let a legacy envelope array with trailing bytes through when the
whole-array check was loosened, and no vector caught it. Add the vector; every
runtime refuses it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:39:29 +02:00
Meta Vens
5d1fc496d7 Caching tests: register the shared valid store's record type
The open verdict for v3-base.msgpack needs a cache that knows vectors.item.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:30:30 +02:00
Meta Vens
4b3c76e418 Caching tests: the open verdict is asserted where the store is attached
AddBackingStore pulls, so a file the reader refuses throws on attach, outside
the assertion. Wrap attach and pull together.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:11:40 +02:00
Meta Vens
e1ff5b2278 Mesh tests: raw and typed writes share one readability vector test
The raw-write refusal test spelled the old header-only verdict (NotSupported for
a first slot that is a string, a legacy array replaced). The shared vector test
now drives both write paths; the old test keeps only the marked-file case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:05:56 +02:00
Meta Vens
4028205ff1 Store readability: one reader decides open, flush, commit and CultMesh writes
A rewrite replaces a file only when the runtime's own reader opens it. C#
drops ReadStoreHeader (a second, header-only verdict that let a flush replace
files the reader refuses) and asks DeserializeSnapshot from open, flush and
commit; the reader now requires exactly one array, so C# refuses bytes after
the store on read as TS and Python do. A zero-byte file is an empty store for
all three paths. CultMesh.WriteSingleFileDocument reads the file it replaces
through the Mesh reader before writing. Rust reads and rewrites through one
classification (trailing bytes refused on read; a non-store first slot must be a
legacy envelope array the reader accepts). TypeScript checks a legacy array with
pullAll's own decoder before replacing it. Python already conformed.

Shared byte vectors and per-runtime verdicts live in
tests/vectors/document-variants-c2a/readability; every runtime's test reads
them. Holds gains second-dictionary-value and second-list-item cases. The
persistence format contract now marks v3 by content and states the readability
rule.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 01:59:28 +02:00
Meta Vens
76e449f33e Merge origin/main into hands/variants-c2a 2026-09-30 01:52:26 +02:00
Meta Vens
a6dc231d40 Ack Cut 3: TypeScript flow-window tests typecheck under noUncheckedIndexedAccess
readyToSend is optional on the receive result; the tests read it as empty
when absent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 01:10:33 +02:00
Meta Vens
4f13eb6689 Ack Cut 3: a direct send behind a queued packet is refused and takes no sequence
The FIFO test now also pins the single-packet send's refusal while an earlier
packet waits in the queue, in Rust, C#, TypeScript and Python.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:57:21 +02:00
Meta Vens
ce552b0e1b Ack Cut D: fix the C# using and the Python request encoding in the new tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:56:51 +02:00
Meta Vens
eeda6d7fd6 Ack Cut 3: the sender's flow window (1,023 sequences and 4 MiB above the lowest unacked)
Admission in Rust, C#, TypeScript and Python goes through one predicate,
window_admits: the 32-packet slot, a sequence at most 1,023 above the lowest
unacked one, and at most 4 MiB of payload above it. With nothing pending any
packet is admissible. Admission is FIFO: nothing overtakes a queued packet.
The single-packet send refuses on the same predicate instead of queueing a
packet and then reporting that it produced none. The receive window is stated
in docs/cultnet-transport-parity.md. Kotlin trails (Cut K3).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:55:14 +02:00
Meta Vens
15e0654ca4 Ack Cut 3: failing tests for the sender's flow window in Rust, C#, TypeScript and Python
A reliable packet goes on the wire only while its sequence is at most 1,023
above the lowest unacked one and the payload above that sequence stays within
4 MiB. Tests cover the span (P8: a lost packet is still delivered), the byte
bound, a lone oversize packet, FIFO admission and the direct-send refusal.
They fail on the unbounded window.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:54:56 +02:00
Meta Vens
0b2f580c82 Ack Cut D (Python): a failed send to one peer no longer kills the RUDP thread
CultMeshLocalServer._send_rudp_packet let an OSError from sendto escape
_rudp_loop, which ended the RUDP thread for every peer. It now counts the
failure (rudp_send_failures) and returns. The test patches socket.sendto to
fail for one peer and serves another.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:51:31 +02:00
Meta Vens
8e609284d2 Ack Cut D (C#): a failed send to one peer is that peer's lost datagram
CultNetRudpSocketTransportServer.SendPacket let a SocketException escape, so
the first failed SendTo ended PollResends and TryReceiveOnce for every peer.
SendPacket now counts the failure in CultNetTransportStats.SendFailures and
returns; only an encode failure throws. The refusal goodbye's own catch is
redundant and goes.

The tests inject the failure at SendPacket through an internal FailingSendPeers
set, exposed to GameCult.Networking.Tests by InternalsVisibleTo.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:51:29 +02:00
Meta Vens
def93c5d89 Ack Cut D (Rust): a failed send to one peer is that peer's lost datagram
The cultmesh-rs document server and the cultnet-rs hub returned the first
failed send_to from maintain, poll_once, poll_resends and receive_event_once,
so one peer whose datagrams cannot be sent failed every poll for every peer.
send_packet now counts a send failure (send_failures() on the document server,
CultNetTransportStats.send_failures on the hub) and returns; only an encode
failure stays an error. The document server treats recv_from ConnectionReset
as Idle, as the socket transport does.

Tests inject the failure at the one send function through a cfg(test) set of
failing peers and prove the other peer is still served.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:47:53 +02:00
Meta Vens
b841274f0e Ack Cut 1b tests: an ending does not forget what the peer sent
A frame held behind a gap survives a timeout and is released when the gap
fills, and a retransmit of a frame received before the end is still a
duplicate, in all four runtimes. Cut 1b drops what the session owes; what it
received from the peer is the peer's retransmit protection.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:24:19 +02:00
Meta Vens
b299582d71 Ack Cut 1b tests: queued writes, flush after a reconnect, the refusal goodbye
The lost write in every ending test now leaves part of a fragmented write
queued. A flush that starts after a reconnect, or after a server-mode
transport accepts a new Connect, must succeed. The three stray-packet tests
that read the peer's pending set after a Disconnect encoded the old rule
(the peer's owed write survives the goodbye); they now assert on the
goodbye's ack field and mask, which is where the refused frame must not be
acknowledged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:20:05 +02:00
Meta Vens
43d7e53efd Ack Cut 1 tests: three held frames, the Rust byte bound, reset reseeds
A gap now holds three ordered frames so a watermark that advances one step
per call fails; the reset test follows a second handshake at a different
sequence so a watermark that survives the reset fails; Rust gains the
4 MiB hold bound in bytes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:20:04 +02:00
Meta Vens
ebc7080165 Ack Cut 1b: every ending drops what it owed; a flush is bound to its generation
Each session gets one private primitive that ends its current generation:
not connected, pending and queued writes dropped, generation advanced,
"ended" raised until the next Connect or Accept begins one. A peer's
Disconnect, a local disconnect, a timeout and a reset (refusal included) all
call it, and it leaves what was received from the peer alone. The socket
transports' flush captures the session generation and fails when it changes
or the session has ended, in all four runtimes; the disconnect reason and,
in TypeScript, the transport's generation pair are report-only or gone. Rust
receipts carry the generation in place of the session scope UUID, so a
receipt reads Invalidated after any ending and stays so after a reconnect.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:13:22 +02:00
Meta Vens
f2b0f008db Ack Cut 1b tests: a write belongs to the session it was issued in
Every ending path (peer Disconnect, local disconnect, timeout, refusal or
reset) must drop the writes it owed, so that a later session neither
retransmits nor delivers them; Rust adds the receipt after a local
disconnect and a reconnect. A flush that starts after an ending, or that
started before one and sees a Connect begin a new session, must fail. The
C# and Python flush tests use a second thread for the mid-wait reconnect.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:13:09 +02:00
Meta Vens
fd1cf22ffe Pin Holds on every list shape, and drop C#'s redundant first-slot check
CultElementIds.Holds is the one fact a store marks its header by; a test now
drives it directly over a dictionary value, a list of lists, a list under an
unset element and an element, and over unset ids that hold nothing, so a
mutant that skips any of those branches fails.

C# ReadStoreHeader refused a first-slot string without the cultcache.store.
prefix and then ran RequireSingleFileFormat, which refuses every string that
is not v1, v2 or v3 anyway. The prefix check is gone; a non-header string is
refused as a format this runtime does not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:11:13 +02:00
Meta Vens
0be33c1198 Rust: a zero-byte store file is rewritten as an empty v1 store
Soul's mutant deleting the empty-file arm of write_all_unlocked survived: no
test rewrote a zero-byte file. Without the arm the empty file reaches
store_header and is refused as truncated. The test pushes onto a zero-byte
file and expects a v1 store holding the pushed record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:11:12 +02:00
Meta Vens
ea1bd6a797 Every runtime rewrites only a file that is exactly one store
A rewrite read the header of the file it replaces but not whether the file was
a store: C# ReadStoreHeader skipped one value and ignored bytes after it, and
took a non-array value or an array whose first slot is a string that is not a
store header for a headerless legacy file (so did the C# Mesh raw write, which
uses it); TypeScript's pushAll took any non-array value or non-header first slot
for v1; Rust ignored bytes after the array.

The rule, one per runtime: a file is a store on rewrite only when it is exactly
one complete MessagePack array. Bytes after it, a value that is not an array,
and a first slot that is a string but not a header this runtime reads are
refused, and the file is left untouched. An empty file and a legacy envelope
array (first slot not a string, empty included) carry no header and are still
rewritten.

- C#: ReadStoreHeader refuses non-array values, trailing bytes and a non-header
  first string, with a NotSupportedException naming which.
- TypeScript: storeHeader (store-format.ts) decides the rewrite header;
  @msgpack/msgpack's decode already refuses trailing bytes.
- Rust: store_header(bytes, whole) walks with a cursor; the rewrite path asks
  for the whole file, the read paths keep reading as before.
- Python: push_all already reads the whole file (unpackb refuses trailing
  bytes, the legacy decoder refuses anything but a list of maps); pinned by a
  test.

Tests in each runtime run the same file matrix (truncated, trailing, scalar,
map, string first; empty, empty array, legacy), plus a C# unconditional commit
over a store with trailing bytes and the C# Mesh raw write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:01:58 +02:00
Meta Vens
385123277f Ack Cut 1: one watermark owns ordered delivery in all four runtimes
The per-channel next pointer, its first-use initialisation, the skip over
other channels' sequences, and the per-channel drain are deleted. Each
session keeps one contiguous received-through watermark, seeded by the
peer's Connect or Accept (or the first packet of an unhandshaken session),
advanced in the received-sequence bookkeeping. Ordered frames wait in one
map keyed by first sequence, and every reliable receipt drains the frames at
or below watermark + 1 in sequence order, after the unordered frame of the
same call. A frame is held only while a sequence below it is missing, so a
5 MiB ordered frame with no gap is never held. Rust bounds what is still
held after the drain (frames, bytes, channels with held frames).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:00:32 +02:00
Meta Vens
b25effebe4 Ack Cut 1 tests: ordered delivery is held only behind a missing sequence
P1, P1b and P6 as asserting tests in Rust, C#, TypeScript and Python, plus
the handshake-seeded watermark, duplicate and reset rules. They fail on the
per-channel next pointer and pass on one contiguous watermark. The three
tests that fed the receiver an impossible handshake (an Accept at sequence 91
for data that starts at 1) now use a real Connect and Accept, and the Rust
channel-bound test holds its frames behind a gap.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:00:05 +02:00
Meta Vens
4f742d05c3 C#: a flush refuses to overwrite a store file whose header it cannot read
SingleFileBackingStore.PushAll replaced the file without reading it, so a
truncated store or one in a format this runtime does not read was clobbered,
where Rust, TypeScript and Python refuse. PushAll now reads the header of a
non-empty file under the lock before it writes; a file it cannot read throws
and is left as it was. An empty file and a legacy envelope array carry no
header and are still replaced.

The header is read by the store's codec: SingleFileBackingStore gains a
protected virtual ReadStoreHeader (default: read the whole snapshot), and the
MessagePack store overrides it with
CultDocumentMessagePackSerialization.ReadStoreHeader. An unconditional commit
reads the file it rewrites through the same check.

Doc text: last-writer-wins now says it holds only over a file the writer can
read (CultCache.cs PushAll comment, cultcache-persistence-format.md
"Conditional commit is the only safe multi-process write" and writer rule 4,
cultcache-store-composition.md "A plain flush and an unconditional commit are
last-writer-wins").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:51:47 +02:00
Meta Vens
619e089fa6 C#: HoldsIds is read off the stored form each time a store writes
A record's HoldsIds was decided by its admission and trusted afterwards, so a
document changed after it was committed, or loaded and changed in place,
flushed under the header its admission-time content needed (Soul P7, P10).

HoldsIds now has one owner, the record's stored form, read when asked:
- a plain record: CultElementIds.Holds over its document, a scan that reads
  and refuses nothing;
- a variant: the same scan over each override value, decoded with the codec
  the record was built with (the cache's, or the attached cache's at load);
- a variant built with no codec (a store that loaded it with no cache) cannot
  see its values and counts as holding an id, so a store never writes an
  unmarked header over ids it could not see.

Cut: the admission path that set HoldsIds on plain records, on variants from
their resolved copies, and on flattened records; the internal setter and its
backing flag; IdPlan.HeldIds and IdPlan.HoldsIds. The Mesh typed write reads
Holds off the document it writes. Admission keeps its own id counting
(IdPlan.Count) for IdsInMemoryOnly.

Tests: an id added to a committed or loaded deck after admission marks a
PushAll v3, and taking it away unmarks it; a variant read without a codec
counts as holding ids while the same record read with one does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:46:50 +02:00
Meta Vens
cf16cb7c1f Merge origin/main: CultCache publication Cuts 1-3 and the RUDP hardening
One conflict, at the end of Admit in CultCache.cs. The branch had added the
IdsInMemoryOnly marking of a load's minted ids before Apply; main had turned
the thread's hold into a Hold carrying Changes and Failures and run the
journals right after Apply. Both are kept, in that order: the marking touches
only the records and runs no user code, then Apply mints the Sequences, the
hold records the changes, and the journals run under the gate.

Id planning and Project do not collide. Ids are planned and applied first,
Resolve (variants, then Project, then the name and index rules) reads the
filled documents, land runs, and only then Apply mints. A Project getter that
throws is caught by the same handler that undoes the id plans, so a refused
admission changes no document and mints nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:33:11 +02:00
Meta Vens
bd58963701 Pin each disjunct and edge of the parity record validators
The audio parity validator now has a refusal row for every field in its
identity and timing disjunctions, and both parity validators have tests for
the inclusive edges: 256 shards, the video block that would make 257, and a
deadline equal to the base presentation time. cargo-mutants over the diff
had left these operators standing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:11:20 +02:00
Meta Vens
3616d0617c Rust: the header is read and the file proven whole in one pass
rmp_serde refuses a sequence visitor that stops early, so the prefix-only header read the
previous commit introduced could not decode any store. store_header is now one visitor: the
first slot is the header (None when it is not a string, the legacy envelope array), the rest
is walked, and a truncated file is an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:58:38 +02:00
Meta Vens
9c62187562 Test: the directory store keeps a marked manifest on a partial flush, so only the single file drops back
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:56:26 +02:00
Meta Vens
aa6b11006f CultMesh: a raw write refuses a file whose header it cannot read
ExistingHeader read the whole snapshot and swallowed every failure into "no header": a
truncated v3 file got a v1 raw write and an unknown v9 file was overwritten, where
TypeScript, Python and Rust refuse. CultDocumentMessagePackSerialization.ReadStoreHeader
reads the prefix header, refuses a file that is not one complete MessagePack value or whose
format this runtime cannot read, and the raw write keeps a whole marked file marked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:53:13 +02:00
Meta Vens
c4fbb53a3b C#: HoldsIds means what the stored form persists, wherever the record came from
A CultStoredDocument built by a caller and handed straight to a store never passed an
admission, so HoldsIds stayed false and its ids landed under a v1/v4 header. An admission
still decides it (it counts the ids it fills); a record that never had one reads it off the
document it stores. CacheBackingStore.HeaderFor documents the rule for third-party stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:53:13 +02:00
Meta Vens
8272b40645 Rust: a store cut short is refused by a rewrite, never taken for a legacy file
store_header returned None for any bytes it could not walk, so push_all read a truncated
variant store as legacy and overwrote it v1. It now proves the file is one complete
MessagePack value first (an error otherwise, the rewrite refuses and leaves the file), then
reads the header from the prefix; the reader's own header read no longer walks to the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:53:12 +02:00
Meta Vens
e9a5a60dbf TypeScript and Python: pin pushAll/push_all onto a store whose header the runtime cannot read
The behaviour was already a refusal that leaves the file untouched; now a test fails if a rewrite
ever reads the disk header less strictly than a load does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:56:08 +02:00
Meta Vens
7ea1aa6a9a Rust: a rewrite reads the header with the snapshot reader, not a hand-written prefix parser
push_all reads the file and reuses store_header and readable_store_format, the same pair a load
uses. The 96-byte prefix parser it replaces misread a str16 header and any str8 header past the
prefix as v1 and overwrote a store it could not read. Pins both encodings refused with the file
untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:56:07 +02:00
Meta Vens
41d0dd9bfb C# header: one function decides it for every writer; a store's own ids are the only source
CacheBackingStore.HeaderFor(holdsIds, existingHeader, wholeStore, directoryStore) replaces FormatFor,
DirectoryFormatFor and the writers' private marker logic. A writer that rewrites the whole store
decides by content; a writer that touches part of it keeps a header already marked. Flush,
unconditional and conditional commit, the directory store and CultMesh's single-file write all call
it. A typed CultMesh write replaces the file with a document it can see, so it decides by content;
a raw payload is opaque and keeps a marked file marked.

A record's HoldsIds is what its stored form persists: a load keeps a variant's delta as read, so
the ids a load mints in an override exist only in memory and no longer mark the store. A
conditional commit onto the file no longer reads this cache's view of what the file held, so a
stale view cannot over-mark it. IdPlan.HoldsIds is derived from HeldIds and the fills.

Reverts InternalsVisibleTo GameCult.Caching.MessagePack and the public DirectoryFormatV4/V5
constants: the directory store reaches the decision through the protected member it inherits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:54:01 +02:00
Meta Vens
591eeff70d Rust: a whole-store flush reads the on-disk header first and refuses one it cannot read
push_all overwrote a v9, variant-v2 or variant store with a v1 file, dropping its variants; TypeScript and Python
refuse. The rewrite header now comes from the file's own prefix and shares one readable-format rule with the
decoder. TypeScript pullAll resets its header when the file is gone, empty or legacy, as Python does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:20:11 +02:00
Meta Vens
6a7d17bdda C2a fix: a flattened record is marked by the document it stores; every writer decides its header by content
A flattened variant's HoldsIds is read off the document re-resolved against the post-batch base, not carried from the
variant or from the document the flatten was planned against. The conditional commit onto a file decides by the same
rule as the unconditional commit (ids in the records the file will hold, ids only this cache minted excepted, a marked
file staying marked). The directory manifest's header choice moves beside FormatFor. Pins the three-batch flatten
probe on the commit and flush paths, the conditional commit, the last id leaving, an id-bearing override that is not
the last, and a failed CultMesh write giving its ids back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:20:01 +02:00
Meta Vens
4b85a173e8 Drop the redundant complete-block short circuit from FEC recovery
A block with every data shard present passes through the code as a no-op
and yields no recovered records, so the early return in both recovery
functions did nothing the code did not already do. Mutation showed both
survive removal. The audio payload-length rule is now pinned by its
message, since the codec's own shard-size error would otherwise mask its
removal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:35:18 +02:00
Meta Vens
1da2a64fbc Pin the media FEC and impairment rules that mutation left standing
Adds the policy fields that must be non-zero, the inclusive shard limit at
the decoder for video and audio parity, admission statistics, seeded jitter
bounds, exact seeded loss, and arrival order for datagrams due together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:26:49 +02:00
Meta Vens
6285a92841 Close the media FEC test gaps found before mutation
Pins the block-boundary chunk and last-chunk length refusals in video
recovery, the audio recovery refusals (packets one either side of the block,
another session, wrong length, parity from different blocks), the remaining
audio block identity rules (session, stream, timebase, duration), and the
data-parity alternation inside a block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:20:06 +02:00
Meta Vens
858e6ec608 Add the cultnet_impair proxy example and a socketless media FEC loss matrix
cultnet_impair is a deterministic bidirectional UDP proxy for pressure
testing real CultNet RUDP sessions with seeded loss, burst, duplicate,
reorder, jitter and stall schedules. It is ported from the Odin donor
(80adfd3) as an example, not library surface. The decision policy (Profile,
Scheduler, seeded RNG) is split from the sockets into policy.rs, which the
proxy and the tests both compile, so a schedule has one owner. The ten
donor profiles live in tests/impairment/.

tests/impairment_policy.rs keeps the donor's six deterministic policy tests
(the burst test now pins the exact drops rather than a count) and adds
profile loading and rejection. tests/media_fec_loss.rs sends protected video
frames through those schedules with no sockets and asserts the guarantee: a
block recovers exactly when at most m of its records were lost, recovered
chunks equal what was sent, and a burst that the send order spreads under m
per block loses no frame. The recovered-frame ratios are printed, not
asserted; tests/impairment/loss-matrix.txt is that output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:17:46 +02:00
Meta Vens
9ed61dd290 C2a test: a flattened variant that holds override ids keeps the flush marked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:09:05 +02:00
Meta Vens
f7c58ce5bc Add the media FEC codec, video parity v3 and audio parity records to cultnet-rs
The codec owns the erasure math for both ends: protect_video_frame and
protect_audio_block on the producer, recover_video_block and
recover_audio_block on the consumer. Each block is systematic Reed-Solomon
over GF(2^8) (reed-solomon-erasure 6, default features), named on every
parity record by the scheme id rs-gf256-v1. Any <= m erasures recover
exactly; more report BeyondRepair. Video frames split into ceil(n/16)
near-equal blocks with m = max(2, ceil(k/4)); audio is 4+2. A frame's
records come back in send order, rotating one record from each block so a
burst lands as one loss per block.

Deleted first: gamecult.media_video_parity_shard.v2 and its XOR-stripe
validator, and the reliable "audio" channel (profile entry, send-options
arm, constant, tests). Audio now rides "media" with parity.

Added: gamecult.media_video_parity_shard.v3 (block geometry, scheme id,
shard length), gamecult.media_audio_parity_shard.v1, the AudioParity wire
variant with validation, and the parity record key now names the block.
Every shard is bounded at MEDIA_FEC_MAX_SHARD_PAYLOAD_BYTES (720) so its
wrapped record fits MEDIA_FEC_MAX_WIRE_BYTES (1431), measured with
identifiers of up to 32 bytes.

Tests pin: exhaustive recovery for every shipped (k, m) and 4+2, m+1
refusal, known-answer parity bytes for both paths, the datagram bound on
every record kind, send order, deadline carriage and decoder validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:08:54 +02:00
Meta Vens
e8886ff8c0 C2a test: a reloaded variant holding override ids keeps the whole-store flush marked
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:03:22 +02:00
Meta Vens
dfde61aba3 C2a test: a preset id and a loaded record holding one mark the store
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:59:52 +02:00
Meta Vens
9da5b35f34 TypeScript and Python: a whole-store flush applies the header the file on disk carries
TS pushAll wrote the header its instance last read (v1 when fresh) without reading the
file. It now reads the on-disk header first, as push and delete do through pullAll. Python
resets its remembered header when the file is gone or empty, so the disk decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:44:16 +02:00
Meta Vens
cc6615b675 C2a test: the id-bearing variant overrides its name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:43:55 +02:00
Meta Vens
490f7b811f C2a: mark the header by content, refuse an element twice in a list, route CultMesh single-file through the id rule
A store is written v3 (v5 for a directory) only when a record in it holds a non-empty
element id. CultElementIds.Plan reports that as IdPlan.HoldsIds; the admission stamps it
on the stored record and every store reads it. The descriptor's CarriesElementIds is
deleted. A file already marked on disk stays marked. CultPersistedStoreSnapshot.FormatFor
is the one header decision.

Plan refuses one element object sitting twice in a list at write, naming the record and
list, instead of writing both ids and refusing the store at load.

CultMesh single-file write mints, refuses and marks as a cache write does; the read applies
the load-time plan, so a write and a load refuse the same things.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:40:36 +02:00
Meta Vens
e85b201bd2 Contract: element ids, the v3 header and its rules
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:18 +02:00
Meta Vens
f4f8e3ed93 Old-reader probes: a reader from before element ids refuses the v3 header
old-reader-refusal.sh stages the C0 merge (e382bb4, or OLD_READER_REV) plus the v3 vector
and one probe per runtime onto a commit; the probe run from that tree is the old reader.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:17 +02:00
Meta Vens
adb4047936 TypeScript, Python and Rust read the v3 header and keep it on rewrite
A store that can hold element ids declares cultcache.store.v3. These runtimes pass payload
bytes through, so they read it and write back the header they found; they do not decide
when a document carries ids. Vector: tests/vectors/document-variants-c2a/v3-base.msgpack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:17 +02:00
Meta Vens
fed7204d9c Tests: the element id rules, each pinned against its own mutation
Engine tests for the plan (duplicates, format, empty derived source, atomicity, two
cultures, interface unions, key sharing); cache tests for the marker on single-file and
directory stores, override values on write and load, minting that is given back, and the
in-memory-only mark.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:16 +02:00
Meta Vens
fc52f984db CultCache: element id rules, atomic minting, minted override values, the v3 marker
The id rule is one plan, made before anything is written: Plan decides every fill and
refuses a duplicate, a random id that is not 12 lowercase hex characters, and a derived
id whose source is null or empty, naming the record and list (CultElementIdException).
A load refuses what a write refuses. Apply and Undo make minting part of the admission:
a refused or uncommitted write leaves the caller's documents as they were.

Override values mint inside Resolve, into the decoded copy, so the public
CultVariantOverride.Set, C1-era overrides and Override() all get ids on write and on load;
Override() no longer mints. Interface unions are judged on their concrete types, the same
rule registration and minting share, and a subtype reusing the id's key is refused at
registration. Dictionary keys format with the invariant culture.

Records minted at load are marked per entry (IdsInMemoryOnly) instead of listed by key, so
a deleted record leaves the mark with it, and a flush of the whole store clears it. A store
whose types reach an object list declares cultcache.store.v3 (directory: v5), which every
reader older than ids refuses; a marked file stays marked when a writer merges onto it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:27:16 +02:00
Meta Vens
884602c708 Merge origin/main into variants-c2a 2026-09-29 16:35:00 +02:00
Meta Vens
640ad9a437 Interop: an element id crosses C#, TypeScript, Python and Rust
The C# cultcache interop peer gains an object-list document (a deck of marks).
C# writes it with cache-minted ids; TypeScript, Python and Rust read the ids as
ordinary members, and TypeScript-written ids survive a C# read.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:03:09 +02:00
Meta Vens
09dabd38c5 CultNet: witness element types carry ids; peers and fixtures follow
CultWitnessContractPin, CultWitnessArtifactEntry and CultWitnessTimingEntry get
an id slot. The JSON schema contract and the Python schema copy gain an optional
id per element, and the Python and TypeScript fixtures carry and assert ids.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:03:09 +02:00
Meta Vens
ded5436569 CultCache: derived element ids; the CDN chunk ref carries one
[CultElementId(nameof(Member))] declares an id derived from that member's
invariant text, never random, so content-addressed elements keep encoding to the
same bytes. CultMeshCdnChunkRef derives its id from Offset; PackArtifact sets
it, the Rust pack_content writes it, and the committed content vectors are
re-cut once so both runtimes carry it. Engine tests drive Problems and Assign
directly (InternalsVisibleTo the Caching tests).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:03:09 +02:00
x
3594a88e4b Docs: consumer sweep list for element ids (C2a)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:31:12 +02:00
x
0d2645d056 CultCache: element ids on every object-list element (C2a)
[CultElementId] marks a string member; the registry refuses a document type
whose list element type (union bases included) lacks one, naming the type and
member. A write mints a random 12-hex id for each unset id and refuses a
duplicate within one list. A store written before ids loads with ids minted in
memory from SHA-256(record key, element path), so every reader and every reload
agrees; they persist on the store's first write. CultCache.MintElementIds()
rewrites the records whose ids exist only in memory.

CultLib's own list-element types gain an id (Geometry, Mesh body, discovery,
entity prefab). CultMeshCdnChunkRef and the CultWitness* elements are pinned by
cross-runtime vectors and are left alone; see the C2a report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:31:12 +02:00
775 changed files with 215527 additions and 6649 deletions

12
.gitattributes vendored
View file

@ -29,3 +29,15 @@ packages/cultmesh-ts/native/**/*.so.* binary
# ends in `\r`) while CI, checking out on Linux, stays green.
packages/cultmesh-ts/native/SHA256SUMS text eol=lf
packages/cultmesh-ts/native/MANIFEST.txt text eol=lf
# CultMath source keeps CRLF in the working tree on every checkout. The release
# gate compares the CultMath.dll it builds with the one org.gamecult.cultmath
# tracks, byte for byte, and the bytes follow the checkout's line endings: the
# pdb's document checksums feed the DLL's pdb id, timestamp and MVID. The
# tracked DLL was built from CRLF source, so CRLF here keeps it valid, and an
# LF-configured or Linux clone builds the same bytes as a Windows one. The index
# stays LF; only the checkout changes.
packages/cultmath/src/** text eol=crlf
# Stores the Python typed-leaf tests write for the C# reference to open.
tests/vectors/**/*.cc binary

View file

@ -9,44 +9,6 @@ on:
workflow_dispatch:
jobs:
getting-started:
name: ${{ matrix.os }} one-command multiplayer onboarding
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- name: Check out CultLib
uses: actions/checkout@v4
with:
path: CultLib
- name: Check out the matching Eve contract
uses: actions/checkout@v4
with:
repository: GameCult/Eve
# Eve main is the contract this sample is written against; the codex
# branch this used to pin was never merged and stopped on 2026-08-25.
ref: ${{ vars.EVE_INTEGRATION_REF || 'main' }}
path: Eve
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Run the documented one-command checkpoint
shell: pwsh
run: ./CultLib/scripts/verify-eve-getting-started.ps1 -EveRoot ./Eve
reactive-and-resource-lifetime:
name: ${{ matrix.os }} reactive semantics and leases
runs-on: ${{ matrix.os }}
@ -54,7 +16,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
# The forge runner is Ubuntu-only: Windows legs run on workflow_dispatch (GitHub or a Windows runner), Ubuntu gates push and PR.
os: ${{ fromJSON(github.event_name == 'workflow_dispatch' && '["windows-latest", "ubuntu-latest"]' || '["ubuntu-latest"]') }}
steps:
- name: Check out CultLib
@ -101,23 +64,42 @@ jobs:
# pull requests: cultnet-interop.yml is windows-latest and main-only, so
# until these steps existed the shared verifier's tests never ran on
# Linux at all, and the browser client's never ran in CI on any OS.
# packages/cultnet-ts verifies both vector files; cultmesh-browser proves
# the browser client reads the shared rules rather than its own.
# packages/cultnet-ts verifies both vector files (content.test.ts judges both
# C# content files, the Rust one and its own); cultmesh-browser proves
# the browser client reads the shared rules rather than its own, and that its content reader releases only verified
# bytes, requesting the same URLs as the C# HTTPS connector.
# No workflow ran cultcache-ts's tests, and its test script listed two files by hand while a third sat compiled
# and unrun. The wiring check fails when any TypeScript test file is run by no script, and the cache's own suite
# runs here beside the packages built on it.
- name: Check that every TypeScript test file is run by its package
run: node scripts/check-typescript-tests-wired.mjs
- name: Verify the CultCache document owner with the TypeScript cache
run: npm run test --workspace packages/cultcache-ts
# Packs the four TypeScript packages and installs them into a clean consumer, so a package that builds but does
# not install or load fails here.
- name: Verify the packed TypeScript packages install and load from a clean consumer
run: npm run test:pack:ts
- name: Verify the CultMesh authority rules with the TypeScript verifier
run: npm run test --workspace packages/cultnet-ts
- name: Verify the browser client against the shared TypeScript verifier
run: npm run test --workspace packages/cultmesh-browser
# The realtime frame codec. Its byte parity rests on two vector files,
# cultmesh-browser's test compiles test/interop, which imports cultmesh-ts/content, so cultmesh-ts builds first,
# as in scripts/run-typescript-workspaces.mjs.
# The realtime frame codec and the HTTP content chunk provider (content.test.ts,
# its answers over node:http). The codec's byte parity rests on two vector files,
# contracts/cultmesh/realtime-frame-vectors.json written by the C#
# reference and decoded by this step, and realtime-frame-vectors.ts-written.json
# written by this codec and decoded by the C# step below. Neither half ran
# in CI on any OS until these two steps existed, so a codec that agreed
# only with itself would have gone green.
- name: Verify the realtime frame codec with the TypeScript codec
- name: Verify the realtime frame codec and the content provider with the TypeScript runtime
run: npm run test --workspace packages/cultmesh-ts
- name: Verify the browser client against the shared TypeScript verifier
run: npm run test --workspace packages/cultmesh-browser
# Both directions of the CultMesh authority vectors: the C#-written
# file (contracts/cultmesh/authority-route-vectors.json) and the
# TypeScript-signed file (authority-route-vectors.ts-signed.json) are
@ -138,3 +120,20 @@ jobs:
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
--filter FullyQualifiedName~RealtimeWireProtocol
--logger "console;verbosity=minimal"
# The content plane: the reference writes the manifest and record-key
# vectors and judges the manifests the TypeScript packer wrote
# (content-vectors.ts-written.json). This suite also holds the C#/Rust
# content parity, which no CI step ran before this one. CULTNET_WRITE_VECTORS
# is never set in CI: the committed files are the contract.
- name: Verify the content vectors with the C# reference
run: >-
dotnet test tests/GameCult.Mesh.Tests/GameCult.Mesh.Tests.csproj
--filter FullyQualifiedName~CultMeshContentVectorTests
--logger "console;verbosity=minimal"
# The cross-process HTTPS content parity: the C# transfer service over the unmodified HTTPS connector and the
# TypeScript reader fetch from the TypeScript provider in its own process. Both refuse bytes tampered in flight,
# and the provider refuses a corrupt store.
- name: Verify HTTPS content parity across C#, the TypeScript provider and the TypeScript reader
run: npm run test:interop --workspace packages/cultmesh-browser

View file

@ -1,10 +1,9 @@
name: CultNet cross-runtime interop
# workflow_dispatch only: this job is Windows-only (pwsh Kotlin build, setup steps), and the forge
# runner is Ubuntu-only, so a push or pull_request trigger would queue a job that can never run.
# Run it from GitHub or any Windows runner.
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
@ -12,6 +11,13 @@ jobs:
name: C# / TypeScript / Rust / Python / Kotlin
runs-on: windows-latest
timeout-minutes: 60
# Windows runners default to pwsh, which does not stop when a native command fails: a
# multi-command step reported only its last command, and a failing cultnet-py suite
# went green that way. Git Bash runs as `bash -e -o pipefail`, so every command counts.
# Steps that name `shell: pwsh` check $LASTEXITCODE themselves.
defaults:
run:
shell: bash
env:
DOTNET: dotnet
@ -69,7 +75,11 @@ jobs:
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$stray = git ls-files '*.schema.json' | Where-Object { $_ -notmatch '^contracts/cultnet/' }
# packages/cultnet-rs/contracts/cultnet/ holds R-AB's vendored byte copies (the crate must build
# when copied alone). Their drift guard is packages/cultnet-rs/tests/schema_discovery_contracts.rs:74-99.
$tracked = git ls-files '*.schema.json'
if ($LASTEXITCODE -ne 0) { throw "git ls-files failed" }
$stray = $tracked | Where-Object { $_ -notmatch '^(packages/cultnet-rs/)?contracts/cultnet/' }
# -I skips binary files: unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll
# still embeds the retired namespace until it is rebuilt (REC-032 D4, deferred).
$oldNamespace = git grep -I -lE 'GameCult/cultnet-ts/(integration/)?contracts' -- ':!.voidbot'

View file

@ -8,15 +8,26 @@ name: Publish packages
# directly, which is already how Ghostlight, Epiphany, CodexConnector and Odin
# consume the core stack.
#
# Tag a release to publish, for example `cultcache-ts-v0.13.6` or
# `cultnet-py-v0.1.0`. The tag's version must match the version in the package
# manifest; the workflow refuses the publish otherwise rather than shipping a
# mislabelled artifact.
# Tag a Python release to publish, for example `cultnet-py-v0.1.0`. The tag's
# version must match the version in the package manifest;
# scripts/release-after-suite.mjs refuses the release otherwise rather than
# shipping a mislabelled artifact.
#
# npm publishing of @gamecult/cultcache-ts is deliberately off: `cultcache-ts-v*`
# is not a tag trigger until it is ruled on (the cultcache-ts job below runs from
# workflow_dispatch only, which publishes nothing). A tag run uses the workflow
# file at the tagged commit, not the one on main, so a tag on an older commit
# (`cultcache-ts-v0.15.0` sits on 7d292085, before the guard) still runs that
# commit's triggers and jobs. Read this file at the tagged sha before pushing a tag.
on:
push:
# cultcache-ts-v* is deliberately not a trigger: publishing @gamecult/cultcache-ts is disabled
# until it is ruled on, and whether NPM_TOKEN is set is unknown, so a pushed tag must not reach
# `npm publish`. The cultcache-ts jobs below still run from workflow_dispatch, which publishes nothing.
# (The comment sits above `tags:` because the tag-prefix pin in scripts/check-changelog-semver.test.mjs
# reads the list as `tags:` followed directly by its items.)
tags:
- "cultcache-ts-v*"
- "cultcache-py-v*"
- "cultnet-py-v*"
- "cultmesh-py-v*"
@ -42,7 +53,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# fetch-depth: 0 so the semver check below can see every previously
# fetch-depth: 0 so the semver check in the release step can see every previously
# published tag, not just the one that triggered this run.
- name: Check out CultLib
uses: actions/checkout@v4
@ -55,42 +66,30 @@ jobs:
node-version: 24.x
registry-url: https://registry.npmjs.org
# The release-checks suite, which Publish runs, needs .NET for the API gate.
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Install workspace dependencies
run: npm ci
- name: Verify the tag matches the manifest version
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
run: |
manifest="$(node -p "require('./packages/cultcache-ts/package.json').version")"
tagged="${GITHUB_REF_NAME#cultcache-ts-v}"
if [ "$manifest" != "$tagged" ]; then
echo "tag $GITHUB_REF_NAME does not match manifest version $manifest" >&2
exit 1
fi
# docs/semver-policy.md: a breaking change under too small a bump, or a
# version that skips or reverses the previous release, fails here
# instead of shipping with a number that lies.
- name: Verify semver policy
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
run: |
tagged="${GITHUB_REF_NAME#cultcache-ts-v}"
node scripts/check-changelog-semver.mjs \
--package "@gamecult/cultcache-ts" \
--changelog packages/cultcache-ts/CHANGELOG.md \
--version "$tagged" \
--tag-prefix cultcache-ts
# Manual runs publish nothing, so they build and test here. A tag run's
# Publish runs these tests itself.
- name: Build and test
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
working-directory: packages/cultcache-ts
run: npm test
# Scoped packages default to restricted even on public npm, and this is
# MIT like the rest of the repo, so say public explicitly.
# One process checks that the tag is the manifest version, runs the
# release-checks suite, the semver policy check (docs/semver-policy.md)
# and npm test, and only then runs npm publish --access public. Only
# npm publish receives the token. It starts without NODE_OPTIONS, so no
# preloaded module runs inside it.
- name: Publish
if: startsWith(github.ref, 'refs/tags/cultcache-ts-v')
working-directory: packages/cultcache-ts
run: npm publish --access public
run: env -u NODE_OPTIONS node scripts/release-after-suite.mjs npm-publish packages/cultcache-ts "$GITHUB_REF"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@ -122,7 +121,7 @@ jobs:
echo "run=false" >> "$GITHUB_OUTPUT"
fi
# fetch-depth: 0 so the semver check below can see every previously
# fetch-depth: 0 so the semver check in the release step can see every previously
# published tag, not just the one that triggered this run.
- name: Check out CultLib
if: steps.selected.outputs.run == 'true'
@ -142,42 +141,27 @@ jobs:
with:
node-version: 24.x
- name: Verify the tag matches the manifest version
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')
run: |
manifest="$(python -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('packages/${{ matrix.package }}/pyproject.toml').read_text())['project']['version'])")"
tagged="${GITHUB_REF_NAME#${{ matrix.package }}-v}"
if [ "$manifest" != "$tagged" ]; then
echo "tag $GITHUB_REF_NAME does not match manifest version $manifest" >&2
exit 1
fi
# docs/semver-policy.md: a breaking change under too small a bump, or a
# version that skips or reverses the previous release, fails here
# instead of shipping with a number that lies.
- name: Verify semver policy
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')
run: |
tagged="${GITHUB_REF_NAME#${{ matrix.package }}-v}"
node scripts/check-changelog-semver.mjs \
--package "${{ matrix.package }}" \
--changelog packages/${{ matrix.package }}/CHANGELOG.md \
--version "$tagged" \
--tag-prefix "${{ matrix.package }}"
- name: Set up .NET
if: steps.selected.outputs.run == 'true'
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
# Lower layers install from source so a release of one package is tested
# against the sibling revision it was cut with, not whatever PyPI has.
- name: Install and test
- name: Install
if: steps.selected.outputs.run == 'true'
run: |
python -m pip install -e packages/cultcache-py -e packages/cultnet-py -e packages/cultmesh-py
python -m unittest discover -s packages/${{ matrix.package }}/tests
run: python -m pip install --upgrade build -e packages/cultcache-py -e packages/cultnet-py -e packages/cultmesh-py
# One process checks, on a tag, that the tag is the manifest version, runs
# the release-checks suite, on a tag the semver policy check
# (docs/semver-policy.md), and the package's unit tests, and only then
# builds the sdist and wheel. Publish uploads only what this step built,
# so it has nothing to upload unless every check passed. It starts without
# NODE_OPTIONS, so no preloaded module runs inside it.
- name: Build sdist and wheel
if: steps.selected.outputs.run == 'true'
run: |
python -m pip install --upgrade build
python -m build packages/${{ matrix.package }}
run: env -u NODE_OPTIONS node scripts/release-after-suite.mjs python-build packages/${{ matrix.package }} "$GITHUB_REF"
- name: Publish
if: steps.selected.outputs.run == 'true' && startsWith(github.ref, 'refs/tags/')

4
.gitignore vendored
View file

@ -322,6 +322,10 @@ node_modules/
!packages/cultmesh-browser/**
!packages/cultmesh-kotlin/
!packages/cultmesh-kotlin/**
!packages/heimdall-delegation/
!packages/heimdall-delegation/**
!packages/heimdall-stake/
!packages/heimdall-stake/**
packages/**/node_modules/
packages/**/dist/
packages/**/dist-test/

View file

@ -35,10 +35,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Networking.WebSock
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Networking.WebSockets.Tests", "tests\GameCult.Networking.WebSockets.Tests\GameCult.Networking.WebSockets.Tests.csproj", "{B9A441EC-DADE-4500-B092-C635F3EB296A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EveBrowserNetworkSample", "samples\eve-browser-network\EveBrowserNetworkSample.csproj", "{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{809F86A1-1C4C-B159-0CD4-DF9D33D876CE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cultmath", "cultmath", "{B48EAE30-152F-01E3-6E31-82B350494D3C}"
@ -49,6 +45,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath.Tests", "packages\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.ConsumerBuild", "tests\GameCult.Caching.ConsumerBuild\GameCult.Caching.ConsumerBuild.csproj", "{D3138F3C-9531-4906-A5D3-74BD857D0685}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.GeneratorConsumer", "tests\GameCult.Caching.GeneratorConsumer\GameCult.Caching.GeneratorConsumer.csproj", "{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameCult.Caching.OutsideStoreTests", "tests\GameCult.Caching.OutsideStoreTests\GameCult.Caching.OutsideStoreTests.csproj", "{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -227,18 +227,6 @@ Global
{B9A441EC-DADE-4500-B092-C635F3EB296A}.Release|x64.Build.0 = Release|Any CPU
{B9A441EC-DADE-4500-B092-C635F3EB296A}.Release|x86.ActiveCfg = Release|Any CPU
{B9A441EC-DADE-4500-B092-C635F3EB296A}.Release|x86.Build.0 = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x64.ActiveCfg = Debug|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x64.Build.0 = Debug|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x86.ActiveCfg = Debug|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Debug|x86.Build.0 = Debug|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|Any CPU.Build.0 = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x64.ActiveCfg = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x64.Build.0 = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x86.ActiveCfg = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x86.Build.0 = Release|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -275,6 +263,30 @@ Global
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x64.Build.0 = Release|Any CPU
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.ActiveCfg = Release|Any CPU
{D3138F3C-9531-4906-A5D3-74BD857D0685}.Release|x86.Build.0 = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.ActiveCfg = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x64.Build.0 = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.ActiveCfg = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Debug|x86.Build.0 = Debug|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|Any CPU.Build.0 = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.ActiveCfg = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x64.Build.0 = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.ActiveCfg = Release|Any CPU
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B}.Release|x86.Build.0 = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x64.ActiveCfg = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x64.Build.0 = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x86.ActiveCfg = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Debug|x86.Build.0 = Debug|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|Any CPU.Build.0 = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x64.ActiveCfg = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x64.Build.0 = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x86.ActiveCfg = Release|Any CPU
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -294,10 +306,11 @@ Global
{21F26329-2119-4037-93CC-DD1801A1AB78} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
{7F32D96B-AB5D-4A72-9217-B5DCCFEFC7E5} = {E733CBFF-42AE-4B6A-B5B9-0071ECFB45F1}
{B9A441EC-DADE-4500-B092-C635F3EB296A} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
{B48EAE30-152F-01E3-6E31-82B350494D3C} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
{D3138F3C-9531-4906-A5D3-74BD857D0685} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
{1A75EA8F-00B3-49AB-8398-B1B7723BE10B} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
{A54C2C29-941A-46A9-9DDE-9711B2F2EADD} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
EndGlobalSection
EndGlobal

View file

@ -1,5 +1,7 @@
# CultLib
**New here? Start at [`docs/start`](docs/start/README.md)**: a language x package matrix with tested first-call snippets.
CultLib is GameCult's shared state, transport, and runtime substrate. It started
as reusable C# game infrastructure, but the useful thing now is larger: typed
`.cc` persistence, schema-stable wire formats, ergonomic reliable-UDP
@ -30,6 +32,30 @@ bespoke JSON bridge at every boundary. A frontend is just another CultMesh
client: the daemon publishes a typed Eve/CultUI surface, and any compatible
runtime can lower that surface where it needs to live.
## Entry Points
- C# reference runtime: `src/` (`GameCult.Caching`, `GameCult.Networking`,
`GameCult.Mesh`, `GameCult.Geometry`, `GameCult.Unity`). Build and test with
`dotnet build CultLib.sln` and `dotnet test CultLib.sln`.
- Other runtimes, wire-compatible with the C# CultCache: `packages/cultcache-ts`,
`cultcache-py`, `cultcache-rs`, and the CultNet and CultMesh packages beside
them under `packages/`; portable math in `packages/cultmath`.
- Smallest working path: `src/GameCult.Mesh/docs/durable-node-quickstart.md`.
Application path: `src/GameCult.Mesh/docs/getting-started/README.md` and
`samples/eve-two-runtime`.
## Live And Planned
Live: typed CultCache persistence, CultNet schema-v0 transports and native RUDP,
CultMesh nodes with Verse discovery, shard replication and authority leases,
the geometry documents, and CultUI for Unity. The browser client boundary
(`packages/cultmesh-browser`, `GameCult.Networking.WebSockets`) is proven on
loopback under an explicit `local-development` trust policy by
`samples/eve-browser-network`.
Planned: a complete public Verse proof over deployed infrastructure; the
remaining gates are listed in `src/GameCult.Mesh/docs/browser-verse-transport.md`.
## Which Package Do I Want?
If the job is shared game state, start here before inventing a fourth drawer
@ -72,7 +98,8 @@ start with the maintained
series. Its first executable package-consumer checkpoint is
[`samples/eve-two-runtime`](samples/eve-two-runtime/README.md).
The real-process browser continuation is
[`samples/eve-browser-network`](samples/eve-browser-network/README.md).
[`samples/eve-browser-network`](samples/eve-browser-network/README.md); it
needs a sibling Eve checkout (`--eve-root`) and is not in `CultLib.sln`.
One documented command runs both layers:
```powershell
@ -97,7 +124,6 @@ The solution includes:
- `GameCult.Logging`: common logging abstraction plus console and file implementations
- `GameCult.Caching`: `DatabaseEntry`-based cache, indexes, global entries, and backing-store abstractions
- `GameCult.Caching.MessagePack`: MessagePack-backed persistence for the cache
- `GameCult.Caching.NewtonsoftJson`: Newtonsoft.Json-backed persistence for the cache
- `GameCult.Math` / CultMath: portable numeric primitives, deterministic random, noise, and shader-parity helpers
- `GameCult.Geometry` / CultGeometry: engine-neutral geometry algorithms plus CultCache-native domain, selected-cut, and mesh artifact documents
- `GameCult.Networking`: encrypted login/register/verify flows, schema-v0 contracts, transport adapters, and native RUDP sessions
@ -125,7 +151,6 @@ src/
GameCult.Logging/
GameCult.Caching/
GameCult.Caching.MessagePack/
GameCult.Caching.NewtonsoftJson/
GameCult.Geometry/
GameCult.Networking/
GameCult.Networking.WebSockets/
@ -344,7 +369,6 @@ Each subproject has a local README with package-specific detail:
- [GameCult.Logging](src/GameCult.Logging/README.md)
- [GameCult.Caching](src/GameCult.Caching/README.md)
- [GameCult.Caching.MessagePack](src/GameCult.Caching.MessagePack/README.md)
- [GameCult.Caching.NewtonsoftJson](src/GameCult.Caching.NewtonsoftJson/README.md)
- [GameCult.Geometry](src/GameCult.Geometry/README.md)
- [GameCult.Networking](src/GameCult.Networking/README.md)
- [GameCult.Mesh](src/GameCult.Mesh/README.md)

View file

@ -0,0 +1,702 @@
{
"artifacts": [
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
32,
32,
77,
105,
120,
101,
100,
32,
67,
97,
115,
101,
47,
80,
97,
116,
104,
32,
32
],
"versionUnits": [
32,
32,
32
],
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:artifact:mixed-case-path:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
304,
115,
116,
97,
110,
98,
117,
108
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:artifact:\u0130stanbul:1.0.0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
913,
931
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:\u03B1\u03C3:v2-beta:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
65279,
98,
111,
109
],
"versionUnits": [
252
],
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:bom:\u00FC:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
133,
110,
101,
108,
133
],
"versionUnits": [],
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:artifact:nel:unversioned:\u0130\u03C3\uD801\uDC28"
},
{
"artifactIdUnits": [
26085,
26412,
35486,
47,
36039,
29987
],
"versionUnits": [
32,
32,
32
],
"contentHash": "sha256:",
"recordKey": "mesh:cdn:artifact:\u65E5\u672C\u8A9E-\u8CC7\u7523:unversioned:"
},
{
"artifactIdUnits": [
55357,
56898,
32,
115,
109,
105,
108,
101
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:artifact:smile:1.0.0:\u017Fha256:abc"
},
{
"artifactIdUnits": [
45,
45,
45
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:cb3f91d54eee30e53e35b2b99905f70f169ed549fd78909d3dac2defc9ed8d3b:v2-beta:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
97,
55296,
98
],
"versionUnits": [
252
],
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:artifact:a-b:\u00FC:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
55296
],
"versionUnits": [],
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:artifact:83d544ccc223c057d2bf80d3f2a32982c32c3c0db8e2674820da5064783fb097:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
120,
178,
121
],
"versionUnits": [
32,
32,
32
],
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:x-y:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
1635,
100
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:\u0663d:1.0.0:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
55297,
56320,
65
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:artifact:a:v2-beta:\u0130\u03C3\uD801\uDC28"
},
{
"artifactIdUnits": [
32,
32,
45,
45,
45,
32,
32
],
"versionUnits": [
252
],
"contentHash": "sha256:",
"recordKey": "mesh:cdn:artifact:fcd2a2fb27957aa9a10d2d9eca16ccbc9c35a76933b03f470211d327f6946c3f:\u00FC:"
},
{
"artifactIdUnits": [
9398
],
"versionUnits": [],
"contentHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:artifact:7d9633ae307dae04a8b362df41dca4b38a653823d69f27a259e7b6928508d3a6:unversioned:\u017Fha256:abc"
},
{
"artifactIdUnits": [
8555
],
"versionUnits": [
32,
32,
32
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:8c0e55c55f47de02406174ec976279a977e2e73ea5aec8ab90f94c40a6ab2d60:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
32,
32,
32
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
49,
46,
48,
46,
48
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:1.0.0:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
86,
50,
32,
66,
101,
116,
97
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:v2-beta:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [
252
],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:\u00FC:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:\u0130\u03C3\uD801\uDC28"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "sha256:",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:"
},
{
"artifactIdUnits": [
101,
117,
114,
101,
107,
97,
47,
99,
111,
110,
116,
101,
110,
116,
45,
118,
101,
99,
116,
111,
114
],
"versionUnits": [],
"contentHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:artifact:eureka-content-vector:unversioned:\u017Fha256:abc"
}
],
"chunks": [
{
"chunkHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": "SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": " e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ",
"recordKey": "mesh:cdn:chunk:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": " sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:chunk:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": "sha256:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"recordKey": "mesh:cdn:chunk:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"chunkHash": "Sha256:\u0130\u03A3\uD801\uDC00",
"recordKey": "mesh:cdn:chunk:\u0130\u03C3\uD801\uDC28"
},
{
"chunkHash": "sha256:",
"recordKey": "mesh:cdn:chunk:"
},
{
"chunkHash": "\u017Fha256:abc",
"recordKey": "mesh:cdn:chunk:\u017Fha256:abc"
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -168,22 +168,22 @@
{
"label": "manifest",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQgA=="
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQgA=="
},
{
"label": "manifest_out_of_order",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY5TZQDMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmUAzgAEAADZT21lc2g6Y2RuOmNodW5rOjMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmWU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOU2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NpTZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAylNlAYzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZs4ACAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmM3ZjFiZGU5ZTFlZmE3ZDEzODVkNTFiMDc3ZWE4N2IwYzM4NjBkMjhkYTQzZDVkNmYyMjFlODQ1YzRjMjM2MGaQgA=="
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOnMTMxMDcyMJXZQGUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzbOAAQAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazplMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2pjI2MjE0NJXZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAypzEwNDg1NzaV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiQgA=="
},
{
"label": "manifest_metadata_order",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
},
{
"label": "manifest_empty_body",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZTZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NZCA"
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZXZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NaEwkIA="
}
]
}
}

View file

@ -138,22 +138,22 @@
{
"label": "manifest",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQgA=="
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQgA=="
},
{
"label": "manifest_out_of_order",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY5TZQDMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmUAzgAEAADZT21lc2g6Y2RuOmNodW5rOjMxYTFmOWRlYTAxNjk1NTEwOTJkMDVlOGJmNGE0NDYyMjhjOGMzZWI0YzliNzEzYzY2YWRjYjdmZDUzYzg5YmWU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOU2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NpTZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAylNlAYzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZs4ACAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmM3ZjFiZGU5ZTFlZmE3ZDEzODVkNTFiMDc3ZWE4N2IwYzM4NjBkMjhkYTQzZDVkNmYyMjFlODQ1YzRjMjM2MGaQgA=="
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOnMTMxMDcyMJXZQGUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzbOAAQAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazplMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2pjI2MjE0NJXZQDI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDLOABAAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazoyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAypzEwNDg1NzaV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiQgA=="
},
{
"label": "manifest_metadata_order",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaU2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJllNlAZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3Ns4ABAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOmUzMWJkYzYzNTc3ZmU3NmY2NDQxMDE0YWFhYTIyMjZkOWZmY2UxMWI0NGQwZDUxMGNhZGUxMzJiNzlmZDdkNzaU2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZpTZQGFhMTQ0NGJlNGI2NDA5OGY1ZGQ4NjNiNWIwYWM1NjE0ODBiM2ZiNWM0MWZjMDdjZjUwYTYyZWRjOTQxMjRiY2POAAwAAM4ABAAA2U9tZXNoOmNkbjpjaHVuazphYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjlNlAMjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMs4AEAAAzgAEAADZT21lc2g6Y2RuOmNodW5rOjI4MzE3MTU4NTlmMDFmM2Y5NjNkZDBmMTE5NGRiZGJiNzYwOTk5NzQyMzAxNWQzMWY2MDc5MTFlMWJmODY0MDKU2UA1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjzgAUAADNEt/ZT21lc2g6Y2RuOmNodW5rOjU5OTNhYzYwNDA3ZTg2NjAxODczYmMzOGVlNmY4ZGIzZWE5MTJlOTg3NjFjYjU1OGFiOTc5NWRiMzA2YjQ3NGOQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZjRhZTJhN2M5NWY0ZDk2NjA1OGY3NWMxMzE4NWFhMGFiNmE5MWZjNWFmYWQ4ODYyOTZlZjJhZWU3NTVhMmFmMc4AFBLfvGFwcGxpY2F0aW9uL3gtY3VsdG5ldC12ZWN0b3LZITIwMjYtMDktMjJUMDA6MDA6MDAuMDAwMDAwMCswMDowMJaV2UAzMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJlAM4ABAAA2U9tZXNoOmNkbjpjaHVuazozMWExZjlkZWEwMTY5NTUxMDkyZDA1ZThiZjRhNDQ2MjI4YzhjM2ViNGM5YjcxM2M2NmFkY2I3ZmQ1M2M4OWJloTCV2UBlMzFiZGM2MzU3N2ZlNzZmNjQ0MTAxNGFhYWEyMjI2ZDlmZmNlMTFiNDRkMGQ1MTBjYWRlMTMyYjc5ZmQ3ZDc2zgAEAADOAAQAANlPbWVzaDpjZG46Y2h1bms6ZTMxYmRjNjM1NzdmZTc2ZjY0NDEwMTRhYWFhMjIyNmQ5ZmZjZTExYjQ0ZDBkNTEwY2FkZTEzMmI3OWZkN2Q3NqYyNjIxNDSV2UBjN2YxYmRlOWUxZWZhN2QxMzg1ZDUxYjA3N2VhODdiMGMzODYwZDI4ZGE0M2Q1ZDZmMjIxZTg0NWM0YzIzNjBmzgAIAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YzdmMWJkZTllMWVmYTdkMTM4NWQ1MWIwNzdlYTg3YjBjMzg2MGQyOGRhNDNkNWQ2ZjIyMWU4NDVjNGMyMzYwZqY1MjQyODiV2UBhYTE0NDRiZTRiNjQwOThmNWRkODYzYjViMGFjNTYxNDgwYjNmYjVjNDFmYzA3Y2Y1MGE2MmVkYzk0MTI0YmNjzgAMAADOAAQAANlPbWVzaDpjZG46Y2h1bms6YWExNDQ0YmU0YjY0MDk4ZjVkZDg2M2I1YjBhYzU2MTQ4MGIzZmI1YzQxZmMwN2NmNTBhNjJlZGM5NDEyNGJjY6Y3ODY0MzKV2UAyODMxNzE1ODU5ZjAxZjNmOTYzZGQwZjExOTRkYmRiYjc2MDk5OTc0MjMwMTVkMzFmNjA3OTExZTFiZjg2NDAyzgAQAADOAAQAANlPbWVzaDpjZG46Y2h1bms6MjgzMTcxNTg1OWYwMWYzZjk2M2RkMGYxMTk0ZGJkYmI3NjA5OTk3NDIzMDE1ZDMxZjYwNzkxMWUxYmY4NjQwMqcxMDQ4NTc2ldlANTk5M2FjNjA0MDdlODY2MDE4NzNiYzM4ZWU2ZjhkYjNlYTkxMmU5ODc2MWNiNTU4YWI5Nzk1ZGIzMDZiNDc0Y84AFAAAzRLf2U9tZXNoOmNkbjpjaHVuazo1OTkzYWM2MDQwN2U4NjYwMTg3M2JjMzhlZTZmOGRiM2VhOTEyZTk4NzYxY2I1NThhYjk3OTVkYjMwNmI0NzRjpzEzMTA3MjCQg6R6ZXRhoTGlYWxwaGGhMqNtaWShMw=="
},
{
"label": "manifest_empty_body",
"kind": "manifest",
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZTZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NZCA"
"messagePackBase64": "mrVldXJla2EvY29udGVudC12ZWN0b3KlYXNzZXShMdlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQC8YXBwbGljYXRpb24veC1jdWx0bmV0LXZlY3RvctkhMjAyNi0wOS0yMlQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZXZQGUzYjBjNDQyOThmYzFjMTQ5YWZiZjRjODk5NmZiOTI0MjdhZTQxZTQ2NDliOTM0Y2E0OTU5OTFiNzg1MmI4NTUAANlPbWVzaDpjZG46Y2h1bms6ZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NaEwkIA="
}
]
}
}

View file

@ -0,0 +1,120 @@
{
"vectors": [
{
"label": "ts multi-chunk body",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 131072,
"tags": [
"a",
"b"
],
"metadata": [
[
"10",
"x"
],
[
"alpha",
"y"
],
[
"2",
"z"
]
],
"body": {
"length": 777777,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:cf8a453a36981321977355448d24df49bcefee22be6e3730b9140e87c29013f9",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBjZjhhNDUzYTM2OTgxMzIxOTc3MzU1NDQ4ZDI0ZGY0OWJjZWZlZTIyYmU2ZTM3MzBiOTE0MGU4N2MyOTAxM2Y5zgAL3jG/YXBwbGljYXRpb24veC1jdWx0bmV0LXRzLXZlY3RvctkhMjAyNi0xMC0xOVQwMDowMDowMC4wMDAwMDAwKzAwOjAwlpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDgAzgACAADZT21lc2g6Y2RuOmNodW5rOjM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDihMJXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAIAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjEzMTA3MpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAQAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjI2MjE0NJXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAYAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjM5MzIxNpXZQDM3N2M3YjBjYzIyOWY0NzA1NjcwZDYzZDU0NGMyNzViNjYzZGY2ZThjODMxYjA5ODQ2YTVjNDhjY2EwOGI4NDjOAAgAAM4AAgAA2U9tZXNoOmNkbjpjaHVuazozNzdjN2IwY2MyMjlmNDcwNTY3MGQ2M2Q1NDRjMjc1YjY2M2RmNmU4YzgzMWIwOTg0NmE1YzQ4Y2NhMDhiODQ4pjUyNDI4OJXZQGEwYmI2OWY0NDg2MWViM2RiNTYwMjZhMWFiYmY5MWZmYTYyMTMyNTA4ZDVhMDZlMGQwZGRjNWNjMjZiZTMzZmPOAAoAAM4AAd4x2U9tZXNoOmNkbjpjaHVuazphMGJiNjlmNDQ4NjFlYjNkYjU2MDI2YTFhYmJmOTFmZmE2MjEzMjUwOGQ1YTA2ZTBkMGRkYzVjYzI2YmUzM2ZjpjY1NTM2MJKhYaFig6IxMKF4pWFscGhhoXmhMqF6"
},
{
"label": "ts empty body",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 131072,
"tags": [
"a",
"b"
],
"metadata": [
[
"10",
"x"
],
[
"alpha",
"y"
],
[
"2",
"z"
]
],
"body": {
"length": 0,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSg2UBlM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1AL9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRldlAZTNiMGM0NDI5OGZjMWMxNDlhZmJmNGM4OTk2ZmI5MjQyN2FlNDFlNDY0OWI5MzRjYTQ5NTk5MWI3ODUyYjg1NQAA2U9tZXNoOmNkbjpjaHVuazplM2IwYzQ0Mjk4ZmMxYzE0OWFmYmY0Yzg5OTZmYjkyNDI3YWU0MWU0NjQ5YjkzNGNhNDk1OTkxYjc4NTJiODU1oTCSoWGhYoOiMTCheKVhbHBoYaF5oTKheg=="
},
{
"label": "ts single chunk, versioned",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": "asset",
"version": "V1 Beta",
"mimeType": "application/x-cultnet-ts-vector",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 4194304,
"tags": [],
"metadata": [],
"body": {
"length": 1000,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:v1-beta:89f4ff56a25dd1db06a4ce6033603775d705fb96f30f8693733fef602a1ca532",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOlYXNzZXSnVjEgQmV0YdlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMs0D6L9hcHBsaWNhdGlvbi94LWN1bHRuZXQtdHMtdmVjdG9y2SEyMDI2LTEwLTE5VDAwOjAwOjAwLjAwMDAwMDArMDA6MDCRldlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMgDNA+jZT21lc2g6Y2RuOmNodW5rOjg5ZjRmZjU2YTI1ZGQxZGIwNmE0Y2U2MDMzNjAzNzc1ZDcwNWZiOTZmMzBmODY5MzczM2ZlZjYwMmExY2E1MzKhMJCA"
},
{
"label": "ts whitespace version, padded kind and mime",
"kind": "manifest",
"input": {
"artifactId": "Pixel Heroes/Hüt ☃",
"kind": " build ",
"version": " ",
"mimeType": " text/plain; charset=utf-8 ",
"createdAtUtc": "2026-10-19T00:00:00.0000000+00:00",
"chunkSizeBytes": 4194304,
"tags": [],
"metadata": [],
"body": {
"length": 1000,
"multiplier": 7,
"modulus": 256
}
},
"recordKey": "mesh:cdn:artifact:pixel-heroes-hüt:unversioned:89f4ff56a25dd1db06a4ce6033603775d705fb96f30f8693733fef602a1ca532",
"messagePackBase64": "mrVQaXhlbCBIZXJvZXMvSMO8dCDimIOnIGJ1aWxkIKIgINlAODlmNGZmNTZhMjVkZDFkYjA2YTRjZTYwMzM2MDM3NzVkNzA1ZmI5NmYzMGY4NjkzNzMzZmVmNjAyYTFjYTUzMs0D6L0gIHRleHQvcGxhaW47IGNoYXJzZXQ9dXRmLTggINkhMjAyNi0xMC0xOVQwMDowMDowMC4wMDAwMDAwKzAwOjAwkZXZQDg5ZjRmZjU2YTI1ZGQxZGIwNmE0Y2U2MDMzNjAzNzc1ZDcwNWZiOTZmMzBmODY5MzczM2ZlZjYwMmExY2E1MzIAzQPo2U9tZXNoOmNkbjpjaHVuazo4OWY0ZmY1NmEyNWRkMWRiMDZhNGNlNjAzMzYwMzc3NWQ3MDVmYjk2ZjMwZjg2OTM3MzNmZWY2MDJhMWNhNTMyoTCQgA=="
}
]
}

View file

@ -111,8 +111,10 @@ Not implemented yet:
not guess. Adding a code is therefore a coordinated change across every runtime, made together: the schema (both
copies), the C# `CultNetErrorMessage` factory, the Rust `CultNetErrorCode`, and a captured-bytes case in
`packages/cultnet-rs/tests/error_message.rs` for it. TypeScript reads the schema. The current codes are
`selection_invalid`, `cursor_stale`, `cursor_invalid`, `reference_outside_target`, `variant_unsupported` and
`unowned_schema` (a write, or a remote put or delete, for a schema and key no shard owns).
`selection_invalid`, `cursor_stale`, `cursor_invalid`, `reference_outside_target`, `variant_unsupported`,
`unowned_schema` (a write, or a remote put or delete, for a schema and key no shard owns),
`write_not_authorized` (a peer's put or delete its server's remote write authority did not grant) and
`unsupported_schema_version` (a message whose `schemaVersion` the peer does not speak; `details` names `schemaVersion` and the offered value).
## Live Invariants
@ -122,6 +124,9 @@ copies), the C# `CultNetErrorMessage` factory, the Rust `CultNetErrorCode`, and
- One primary owns writes for a shard until a stronger policy exists.
- Non-owner writes are rejected or explicitly forwarded. They are not silently
applied.
- A peer's put or delete is applied or forwarded only when the server's
`RemoteWriteAuthority` grants it; none configured refuses every remote write.
The connection key admits a peer to the transport and is never a grant.
- Raw snapshot/put/delete messages must pass through `CultNetDatabase`, not
bypass shard policy.
- The accepted shard log is stored in replica catch-up wire form. Object logs

View file

@ -28,7 +28,7 @@
"code": {
"description": "Machine-readable refusal code (R-N). Additive: a v0 peer that does not know this field ignores it. The set is a closed enum: Rust and TypeScript refuse a message whose code is not listed, so adding a code is one coordinated change across every runtime (this schema, the C# factory, the Rust CultNetErrorCode, and a captured-bytes case for each).",
"type": ["string", "null"],
"enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported", "unowned_schema"]
"enum": [null, "selection_invalid", "cursor_stale", "cursor_invalid", "reference_outside_target", "variant_unsupported", "unowned_schema", "write_not_authorized", "unsupported_schema_version"]
},
"details": {
"description": "Code-specific structured detail (R-N). Every field is populated only for the code that defines it; the others are null, not omitted.",
@ -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 }
}

View file

@ -59,6 +59,7 @@
],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"role": { "type": "string", "minLength": 1 },
"schemaId": { "type": "string", "minLength": 1 },
"schemaVersion": { "type": "string", "minLength": 1 },
@ -74,6 +75,7 @@
],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"role": { "type": "string", "minLength": 1 },
"uri": { "type": "string", "minLength": 1 },
"mediaType": { "type": "string", "minLength": 1 },
@ -92,6 +94,7 @@
],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"stage": { "type": "string", "minLength": 1 },
"startedAt": { "type": "string", "minLength": 1 },
"completedAt": { "type": "string", "minLength": 1 },

View file

@ -0,0 +1,346 @@
{
"vectors": [
{
"name": "snapshot_request_v1_defaults",
"schemaVersion": "cultnet.snapshot_request.v1",
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9uu2N1bHRuZXQuc25hcHNob3RfcmVxdWVzdC52MaltZXNzYWdlSWShbalzZWxlY3Rpb26Jp3NjaGVtYXPApGtleXPApmZpZWxkc8ClY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywKdzaGFyZElkwKpzaGFyZEVwb2NowA==",
"expected": {
"schemaVersion": "cultnet.snapshot_request.v1",
"messageId": "m",
"selection": {
"schemas": null,
"keys": null,
"fields": null,
"cites": null,
"cited": null,
"projection": "header",
"descending": false,
"limit": null,
"cursor": null
},
"shardId": null,
"shardEpoch": null
}
},
{
"name": "snapshot_request_v1_full_selection_and_shard",
"schemaVersion": "cultnet.snapshot_request.v1",
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9uu2N1bHRuZXQuc25hcHNob3RfcmVxdWVzdC52MaltZXNzYWdlSWSmbS1mdWxsqXNlbGVjdGlvbomnc2NoZW1hc5KpbGVhZl9hLnY52UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqRrZXlzkqRhLWVxqPCfmIAtcm93pmZpZWxkc5KEpWluZGV4pGtpbmSib3CmYW55X29mpnZhbHVlc5Kmd2VhcG9upHRvb2ymbnVtYmVywISlaW5kZXikbWFzc6JvcKJnZaZ2YWx1ZXPApm51bWJlcqstMjIzMzc1OS4yNaVjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5pGEtZXGkcm9sZaZEZXNpZ26lY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqhkb2N1bWVudKpkZXNjZW5kaW5nw6VsaW1pdMzIpmN1cnNvcq1vcGFxdWUtY3Vyc29yp3NoYXJkSWSnc2hhcmQtMapzaGFyZEVwb2NoBw==",
"expected": {
"schemaVersion": "cultnet.snapshot_request.v1",
"messageId": "m-full",
"selection": {
"schemas": [
"leaf_a.v9",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff"
],
"keys": [
"a-eq",
"\uD83D\uDE00-row"
],
"fields": [
{
"index": "kind",
"op": "any_of",
"values": [
"weapon",
"tool"
],
"number": null
},
{
"index": "mass",
"op": "ge",
"values": null,
"number": "-2233759.25"
}
],
"cites": {
"target": {
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"recordKey": "a-eq"
},
"role": "Design"
},
"cited": {
"role": "Design",
"exists": false
},
"projection": "document",
"descending": true,
"limit": 200,
"cursor": "opaque-cursor"
},
"shardId": "shard-1",
"shardEpoch": 7
}
},
{
"name": "database_subscribe_v1_defaults",
"schemaVersion": "cultnet.database_subscribe.v1",
"messagePackBase64": "iK1zY2hlbWFWZXJzaW9uvWN1bHRuZXQuZGF0YWJhc2Vfc3Vic2NyaWJlLnYxqW1lc3NhZ2VJZKFtrnN1YnNjcmlwdGlvbklko3N1YqlzZWxlY3Rpb26Jp3NjaGVtYXPApGtleXPApmZpZWxkc8ClY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywK9pbmNsdWRlU25hcHNob3TDsWNvbnN1bWVyUnVudGltZUlkwKdib2R5SWRzwLdzdXBwb3J0ZWRCb2R5VHJhbnNwb3J0c8A=",
"expected": {
"schemaVersion": "cultnet.database_subscribe.v1",
"messageId": "m",
"subscriptionId": "sub",
"selection": {
"schemas": null,
"keys": null,
"fields": null,
"cites": null,
"cited": null,
"projection": "header",
"descending": false,
"limit": null,
"cursor": null
},
"includeSnapshot": true,
"consumerRuntimeId": null,
"bodyIds": null,
"supportedBodyTransports": null
}
},
{
"name": "database_subscribe_v1_full",
"schemaVersion": "cultnet.database_subscribe.v1",
"messagePackBase64": "iK1zY2hlbWFWZXJzaW9uvWN1bHRuZXQuZGF0YWJhc2Vfc3Vic2NyaWJlLnYxqW1lc3NhZ2VJZKZtLWZ1bGyuc3Vic2NyaXB0aW9uSWSoc3ViLWZ1bGypc2VsZWN0aW9uiadzY2hlbWFzkqlsZWFmX2EudjnZR3NoYTI1NjpmZjM1ZjNkOWQ2ZGUzZjEwNjZiOTM2OWU3M2ZlNmM3NjQ4ZTg4MzY4ZDE4ZWZjMjI3NWM5OTQ3MzBjZDg0Y2ZmpGtleXOSpGEtZXGo8J\u002BYgC1yb3emZmllbGRzkoSlaW5kZXika2luZKJvcKZhbnlfb2amdmFsdWVzkqZ3ZWFwb26kdG9vbKZudW1iZXLAhKVpbmRleKRtYXNzom9womdlpnZhbHVlc8CmbnVtYmVyqy0yMjMzNzU5LjI1pWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlk2UdzaGEyNTY6ZmYzNWYzZDlkNmRlM2YxMDY2YjkzNjllNzNmZTZjNzY0OGU4ODM2OGQxOGVmYzIyNzVjOTk0NzMwY2Q4NGNmZqlyZWNvcmRLZXmkYS1lcaRyb2xlpkRlc2lnbqVjaXRlZIKkcm9sZaZEZXNpZ26mZXhpc3Rzwqpwcm9qZWN0aW9uqGRvY3VtZW50qmRlc2NlbmRpbmfDpWxpbWl0zMimY3Vyc29yrW9wYXF1ZS1jdXJzb3KvaW5jbHVkZVNuYXBzaG90wrFjb25zdW1lclJ1bnRpbWVJZKhjb25zdW1lcqdib2R5SWRzkaZib2R5LWG3c3VwcG9ydGVkQm9keVRyYW5zcG9ydHOSpHJ1ZHCkcXVpYw==",
"expected": {
"schemaVersion": "cultnet.database_subscribe.v1",
"messageId": "m-full",
"subscriptionId": "sub-full",
"selection": {
"schemas": [
"leaf_a.v9",
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff"
],
"keys": [
"a-eq",
"\uD83D\uDE00-row"
],
"fields": [
{
"index": "kind",
"op": "any_of",
"values": [
"weapon",
"tool"
],
"number": null
},
{
"index": "mass",
"op": "ge",
"values": null,
"number": "-2233759.25"
}
],
"cites": {
"target": {
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"recordKey": "a-eq"
},
"role": "Design"
},
"cited": {
"role": "Design",
"exists": false
},
"projection": "document",
"descending": true,
"limit": 200,
"cursor": "opaque-cursor"
},
"includeSnapshot": false,
"consumerRuntimeId": "consumer",
"bodyIds": [
"body-a"
],
"supportedBodyTransports": [
"rudp",
"quic"
]
}
},
{
"name": "snapshot_response_raw_v1_headers",
"schemaVersion": "cultnet.snapshot_response_raw.v1",
"messagePackBase64": "i61zY2hlbWFWZXJzaW9u2SBjdWx0bmV0LnNuYXBzaG90X3Jlc3BvbnNlX3Jhdy52MaltZXNzYWdlSWShbadtYXRjaGVkAqRhc09mBaRuZXh0q25leHQtY3Vyc29yp2hlYWRlcnOSiqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaqc2NoZW1hTmFtZaZsZWFmX2Gtc2NoZW1hVmVyc2lvbqExsXNjaGVtYUNvbnRlbnRIYXNoo2FiY6lyZWNvcmRLZXmkYS1lcahzdG9yZWRBdLgxOTcwLTAxLTAxVDAwOjAwOjAwLjAwMFqvc291cmNlUnVudGltZUlkonJ0rXNvdXJjZUFnZW50SWSlYWdlbnSqc291cmNlUm9sZaRyb2xlpHRhZ3PAiqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaqc2NoZW1hTmFtZaZsZWFmX2Gtc2NoZW1hVmVyc2lvbqExsXNjaGVtYUNvbnRlbnRIYXNoo2FiY6lyZWNvcmRLZXmkYS1sb6hzdG9yZWRBdLgxOTcwLTAxLTAxVDAwOjAwOjAwLjAwMFqvc291cmNlUnVudGltZUlkonJ0rXNvdXJjZUFnZW50SWSlYWdlbnSqc291cmNlUm9sZaRyb2xlpHRhZ3OSpWFscGhhpGJldGGpZG9jdW1lbnRzwKVlZGdlc8Cnc2hhcmRJZKdzaGFyZC0xqnNoYXJkRXBvY2gDsHNoYXJkTG9nU2VxdWVuY2Up",
"expected": {
"schemaVersion": "cultnet.snapshot_response_raw.v1",
"messageId": "m",
"matched": 2,
"asOf": 5,
"next": "next-cursor",
"headers": [
{
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"schemaName": "leaf_a",
"schemaVersion": "1",
"schemaContentHash": "abc",
"recordKey": "a-eq",
"storedAt": "1970-01-01T00:00:00.000Z",
"sourceRuntimeId": "rt",
"sourceAgentId": "agent",
"sourceRole": "role",
"tags": null
},
{
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"schemaName": "leaf_a",
"schemaVersion": "1",
"schemaContentHash": "abc",
"recordKey": "a-lo",
"storedAt": "1970-01-01T00:00:00.000Z",
"sourceRuntimeId": "rt",
"sourceAgentId": "agent",
"sourceRole": "role",
"tags": [
"alpha",
"beta"
]
}
],
"documents": null,
"edges": null,
"shardId": "shard-1",
"shardEpoch": 3,
"shardLogSequence": 41
}
},
{
"name": "snapshot_response_raw_v1_documents_and_edges",
"schemaVersion": "cultnet.snapshot_response_raw.v1",
"messagePackBase64": "i61zY2hlbWFWZXJzaW9u2SBjdWx0bmV0LnNuYXBzaG90X3Jlc3BvbnNlX3Jhdy52MaltZXNzYWdlSWShbadtYXRjaGVkAaRhc09mBaRuZXh0wKdoZWFkZXJzwKlkb2N1bWVudHORjKhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmaqc2NoZW1hTmFtZcCtc2NoZW1hVmVyc2lvbsCxc2NoZW1hQ29udGVudEhhc2jAqXJlY29yZEtleaRhLWVxqHN0b3JlZEF0uDE5NzAtMDEtMDFUMDA6MDA6MDAuMDAwWq9wYXlsb2FkRW5jb2RpbmerbWVzc2FnZXBhY2uncGF5bG9hZMQEgaFrAa9zb3VyY2VSdW50aW1lSWTArXNvdXJjZUFnZW50SWTAqnNvdXJjZVJvbGXApHRhZ3PApWVkZ2VzkoWkZnJvbYKoc2NoZW1hSWTZR3NoYTI1Njo5ZDBiYzlhZWJjMzYxOWIwN2FhYmY1NjRmOTg2NjdhZmRlNDE0NDRjMWI4NjY2NjFkNzc3Njg3ZDQ0MzYwOGRjqXJlY29yZEtleadjaXRlci0xpHJvbGWmRGVzaWduonRvgqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5pGEtZXGvcGF5bG9hZEVuY29kaW5nwKdwYXlsb2FkwIWkZnJvbYKoc2NoZW1hSWTZR3NoYTI1Njo5ZDBiYzlhZWJjMzYxOWIwN2FhYmY1NjRmOTg2NjdhZmRlNDE0NDRjMWI4NjY2NjFkNzc3Njg3ZDQ0MzYwOGRjqXJlY29yZEtleadjaXRlci0xpHJvbGWmRGVzaWduonRvgqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5pGEtZXGvcGF5bG9hZEVuY29kaW5nq21lc3NhZ2VwYWNrp3BheWxvYWTEAwECA6dzaGFyZElkwKpzaGFyZEVwb2NowLBzaGFyZExvZ1NlcXVlbmNlwA==",
"expected": {
"schemaVersion": "cultnet.snapshot_response_raw.v1",
"messageId": "m",
"matched": 1,
"asOf": 5,
"next": null,
"headers": null,
"documents": [
{
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"schemaName": null,
"schemaVersion": null,
"schemaContentHash": null,
"recordKey": "a-eq",
"storedAt": "1970-01-01T00:00:00.000Z",
"payloadEncoding": "messagepack",
"payload": "gaFrAQ==",
"sourceRuntimeId": null,
"sourceAgentId": null,
"sourceRole": null,
"tags": null
}
],
"edges": [
{
"from": {
"schemaId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc",
"recordKey": "citer-1"
},
"role": "Design",
"to": {
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"recordKey": "a-eq"
},
"payloadEncoding": null,
"payload": null
},
{
"from": {
"schemaId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc",
"recordKey": "citer-1"
},
"role": "Design",
"to": {
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"recordKey": "a-eq"
},
"payloadEncoding": "messagepack",
"payload": "AQID"
}
],
"shardId": null,
"shardEpoch": null,
"shardLogSequence": null
}
},
{
"name": "error_plain",
"schemaVersion": "cultnet.error.v0",
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3KkYm9vbatyb3V0aW5nSGludMCkY29kZcCnZGV0YWlsc8A=",
"expected": {
"schemaVersion": "cultnet.error.v0",
"error": "boom",
"routingHint": null,
"code": null,
"details": null
}
},
{
"name": "error_selection_invalid",
"schemaVersion": "cultnet.error.v0",
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3LZOnNlbGVjdGlvbl9pbnZhbGlkOiBmaWVsZHNbMF0uaW5kZXggImtpbmQiIGlzIG5vdCBkZWNsYXJlZC6rcm91dGluZ0hpbnTApGNvZGWxc2VsZWN0aW9uX2ludmFsaWSnZGV0YWlsc4SlZmllbGSvZmllbGRzWzBdLmluZGV4pXZhbHVlpGtpbmSkYXNPZsCnY3VycmVudMA=",
"expected": {
"schemaVersion": "cultnet.error.v0",
"error": "selection_invalid: fields[0].index \u0022kind\u0022 is not declared.",
"routingHint": null,
"code": "selection_invalid",
"details": {
"field": "fields[0].index",
"value": "kind",
"asOf": null,
"current": null
}
}
},
{
"name": "error_cursor_stale",
"schemaVersion": "cultnet.error.v0",
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3KzY3Vyc29yX3N0YWxlOiBzdGFsZatyb3V0aW5nSGludMCkY29kZaxjdXJzb3Jfc3RhbGWnZGV0YWlsc4SlZmllbGTApXZhbHVlwKRhc09mA6djdXJyZW50CQ==",
"expected": {
"schemaVersion": "cultnet.error.v0",
"error": "cursor_stale: stale",
"routingHint": null,
"code": "cursor_stale",
"details": {
"field": null,
"value": null,
"asOf": 3,
"current": 9
}
}
},
{
"name": "error_cursor_invalid_no_details",
"schemaVersion": "cultnet.error.v0",
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3KzY3Vyc29yX2ludmFsaWQ6IGJhZKtyb3V0aW5nSGludMCkY29kZa5jdXJzb3JfaW52YWxpZKdkZXRhaWxzwA==",
"expected": {
"schemaVersion": "cultnet.error.v0",
"error": "cursor_invalid: bad",
"routingHint": null,
"code": "cursor_invalid",
"details": null
}
},
{
"name": "error_write_not_authorized",
"schemaVersion": "cultnet.error.v0",
"messagePackBase64": "ha1zY2hlbWFWZXJzaW9usGN1bHRuZXQuZXJyb3IudjClZXJyb3K0d3JpdGVfbm90X2F1dGhvcml6ZWSrcm91dGluZ0hpbnTApGNvZGW0d3JpdGVfbm90X2F1dGhvcml6ZWSnZGV0YWlsc4SlZmllbGSpcmVjb3JkS2V5pXZhbHVloWukYXNPZsCnY3VycmVudMA=",
"expected": {
"schemaVersion": "cultnet.error.v0",
"error": "write_not_authorized",
"routingHint": null,
"code": "write_not_authorized",
"details": {
"field": "recordKey",
"value": "k",
"asOf": null,
"current": null
}
}
}
]
}

View file

@ -0,0 +1,150 @@
{
"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.",
"vectors": [
{
"name": "selection_invalid_schemas_null",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92473656C656374696F6E5F696E76616C69643A206D757374206E6F7420626520656D707479AB726F7574696E6748696E74C0A4636F6465B173656C656374696F6E5F696E76616C6964A764657461696C7384A56669656C64A7736368656D6173A576616C7565C0A461734F66C0A763757272656E74C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "selection_invalid: must not be empty",
"routingHint": null,
"code": "selection_invalid",
"details": {
"field": "schemas",
"value": null,
"asOf": null,
"current": null
}
}
},
{
"name": "selection_invalid_field_value",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92273656C656374696F6E5F696E76616C69643A20626C616E6B206B657920656E747279AB726F7574696E6748696E74C0A4636F6465B173656C656374696F6E5F696E76616C6964A764657461696C7384A56669656C64A46B657973A576616C7565A3202020A461734F66C0A763757272656E74C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "selection_invalid: blank key entry",
"routingHint": null,
"code": "selection_invalid",
"details": {
"field": "keys",
"value": " ",
"asOf": null,
"current": null
}
}
},
{
"name": "cursor_stale",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72BD637572736F725F7374616C653A20637572736F72206973207374616C65AB726F7574696E6748696E74C0A4636F6465AC637572736F725F7374616C65A764657461696C7384A56669656C64C0A576616C7565C0A461734F6601A763757272656E7402",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "cursor_stale: cursor is stale",
"routingHint": null,
"code": "cursor_stale",
"details": {
"field": null,
"value": null,
"asOf": 1,
"current": 2
}
}
},
{
"name": "cursor_invalid",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D923637572736F725F696E76616C69643A2063616E6E6F7420706172736520637572736F72AB726F7574696E6748696E74C0A4636F6465AE637572736F725F696E76616C6964A764657461696C73C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "cursor_invalid: cannot parse cursor",
"routingHint": null,
"code": "cursor_invalid",
"details": null
}
},
{
"name": "reference_outside_target",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D9317265666572656E63655F6F7574736964655F7461726765743A206F757473696465206465636C6172656420746172676574AB726F7574696E6748696E74C0A4636F6465B87265666572656E63655F6F7574736964655F746172676574A764657461696C73C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "reference_outside_target: outside declared target",
"routingHint": null,
"code": "reference_outside_target",
"details": null
}
},
{
"name": "variant_unsupported_subscription_ended",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72B876617269616E7420756E737570706F727465642068657265AB726F7574696E6748696E74C0A4636F6465B376617269616E745F756E737570706F72746564A764657461696C7384A56669656C64AE737562736372697074696F6E4964A576616C7565A57375622D31A461734F66C0A763757272656E74C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "variant unsupported here",
"routingHint": null,
"code": "variant_unsupported",
"details": {
"field": "subscriptionId",
"value": "sub-1",
"asOf": null,
"current": null
}
}
},
{
"name": "variant_unsupported_no_subscription",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72B876617269616E7420756E737570706F727465642068657265AB726F7574696E6748696E74C0A4636F6465B376617269616E745F756E737570706F72746564A764657461696C73C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "variant unsupported here",
"routingHint": null,
"code": "variant_unsupported",
"details": null
}
},
{
"name": "unowned_schema",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D95C756E6F776E65645F736368656D613A204E6F207368617264206F776E7320736368656D612027736F6D652E736368656D612E763127206B65792027736F6D653A6B6579273B207468652077726974652077617320726566757365642EAB726F7574696E6748696E74C0A4636F6465AE756E6F776E65645F736368656D61A764657461696C7384A56669656C64A8736368656D614964A576616C7565AE736F6D652E736368656D612E7631A461734F66C0A763757272656E74C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "unowned_schema: No shard owns schema 'some.schema.v1' key 'some:key'; the write was refused.",
"routingHint": null,
"code": "unowned_schema",
"details": {
"field": "schemaId",
"value": "some.schema.v1",
"asOf": null,
"current": null
}
}
},
{
"name": "write_not_authorized",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92177726974655F6E6F745F617574686F72697A65643A206E6F74206772616E746564AB726F7574696E6748696E74C0A4636F6465B477726974655F6E6F745F617574686F72697A6564A764657461696C7384A56669656C64A97265636F72644B6579A576616C7565A8736F6D653A6B6579A461734F66C0A763757272656E74C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "write_not_authorized: not granted",
"routingHint": null,
"code": "write_not_authorized",
"details": {
"field": "recordKey",
"value": "some:key",
"asOf": null,
"current": null
}
}
},
{
"name": "unsupported_schema_version",
"messagePackHex": "85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72D92C756E737570706F727465645F736368656D615F76657273696F6E3A2063756C746E65742E68656C6C6F2E7631AB726F7574696E6748696E74C0A4636F6465BA756E737570706F727465645F736368656D615F76657273696F6EA764657461696C7384A56669656C64AD736368656D6156657273696F6EA576616C7565B063756C746E65742E68656C6C6F2E7631A461734F66C0A763757272656E74C0",
"message": {
"schemaVersion": "cultnet.error.v0",
"error": "unsupported_schema_version: cultnet.hello.v1",
"routingHint": null,
"code": "unsupported_schema_version",
"details": {
"field": "schemaVersion",
"value": "cultnet.hello.v1",
"asOf": null,
"current": null
}
}
}
]
}

441
docs/c2a-land-map.md Normal file
View file

@ -0,0 +1,441 @@
# C2a land map
What C2a (`hands/variants-c2a`, a5c7954e) must still prove to merge to main, measured
on 2026-10-22 against main 7a8933b0 and the 2.0.0 release candidate
`hands/cultlib-unity-release-2.0.0` (5b5d8573, which carries f91e30ed). The land cut
is the merge plus the gate. Everything else is classified below. Governing ruling:
`cultlib-gaps:ruling:release-now-c2a-bumps-format`.
## Cut sequence to main (2026-10-27, branch head 7c7cc799)
Landed since the facts below: `land-c2a` (h1), `land-c2a-merge-api-docs` (h1-h6), the 2.0.1 and
cultcache-rs 0.3.1 merges (5baa02fa, f54472e5), `land-c2a-gate` r4 (h1), `store-gate-one-owner`
(h1-h7, Soul s6) and `restamp-on-write` (h1-h2, 8ee66714..7c7cc799, Soul s2 holds). One static `Owns`
decides ownership for cache and cacheless writers, and the High finding
`cut-land-c2a-gate.s1.reflist-record-read-only-at-head` is Fixed by 7c7cc799. origin/main is 73e684b1:
21 commits past the merge base 02cf04b5, all docs and research (15 files, +1879, none under src/,
tests/ or packages/, none on a path the branch changed). What stands between the branch and main, in
order:
1. **claimant-one-owner** (spec r1 at 7c7cc799, Hands then Soul). Restamp's Soul s2 left four Lows
on the landing path. Which entry reads a persisted entry is chosen twice (`Claimant` at
CultCache.cs:703-750, `ClaimantEntry` at :797-799), and so a cacheless writer handed registry {A,B}
overwrites a record that a cache with {A,B} holds read-only (probe X4). The cut keeps one `Claimant`
precedence over a by-id/by-name lookup that the registry or a single entry supplies. A cacheless
writer with a registry then takes its claimant from that registry. The cut also pins the
unpublished-id case and fixes the stale test comment. It closes `claimant-selection-duplicated`,
`cacheless-inverts-registry-claimant`, `unpublished-id-guard-unpinned` and `stale-flag-copy-comment`.
It is kept apart from the gate so that Soul can falsify it alone. Its files (CultCache.cs, the
persistence contract, RestampOnWriteTests.cs) are disjoint from the gate's, so it can run before or
beside step 2. The second cut to land rebases.
2. **land-c2a-gate** (spec r7 at 7c7cc799, Hands h2 then Soul). The ref-list gate does the same write
at 2.0.1 and at head and compares schema ids. The committed vectors are pinned byte for byte under a
fixed clock, and every stage counts its cases. r7 drops r6's head-only restamp assertions, which
`RestampOnWriteTests.TheCommitted1060VectorsListRecordIsWrittenUnderTheHoldersOwnId` already pins.
It also drops the red-at-193cf1c3 run, because restamp is measured. The cut closes
`single-vector-hand-edit-passes`, `gate-green-with-no-refusal-probes` and follow-up
`gate-reads-1x-written-store`.
3. **Ledger** (Self, no Hands; can run now, beside steps 1 and 2). Each of these is in force though
its fix has landed:
- The seven `cut-land-c2a.s1.*` findings. `land-c2a-merge-api-docs` carried their pins (source reads
2026-10-27: GameCult.Caching grants InternalsVisibleTo only to `.Tests` and `.MessagePack`, the
residual being follow-up `caching-messagepack-ivt`; PreCut2StoreFormatTests pins "with a suffix
of"). Close each one against its commit, or defer it. `stale-unity-plugin-dlls` defers to the
Unity 3.0.0 release cut (the plugins are 2.0.1 builds, 1eeb1a01).
- `cut-c2a-write-set.s1.untouched-dropped-slot-resurrects` and follow-up `slot-reuse-contract-doc`.
Ruling `slot-reuse-documented` decided the behaviour, and the paragraph is in
cultcache-schema-compatibility.md:64-66.
- `cut-restamp-on-write.s1.websockets-tests-unreported`, Fixed by h2 (s2 promise
`websockets-tests-reported` holds).
- Follow-up `gate-bound-test-tighten`, carried by restamp r2 (50362447, a768b821). Close it if
restamp's Soul passes measured it; otherwise route it.
- After steps 1 and 2 hold: the four s2 Lows, the two remaining `cut-land-c2a-gate.s1` Lows and
`gate-reads-1x-written-store`.
4. **Consumer pins** (ruling `unpinned-consumers-at-land-3`, follow-up `consumer-pins-before-land`).
Brokkr, Aquarium, Ymir, AquaSynth and Mimir have no `CultLibRevision`/`VerifyCultLibRevision` guard,
and each builds against the sibling `..\CultLib` checkout. In its own repo, each adopts Aetheria's
guard at the 2.0.0 tag, with one Hands per repo, before the merge reaches main. The pins touch no
CultLib file, so they can start now and run in parallel with steps 1-3. Eve and Gjallar get no pin
(ruled). The step is done when each repo's guard fails a build against a CultLib checkout at
another revision and passes at the tag, and the follow-up is resolved.
5. **Main into the branch, then full verification.** After steps 1, 2 and 4, `git merge origin/main`
into `hands/variants-c2a`. It should be clean, because main's commits since 02cf04b5 are docs and
research only. Re-check that before merging (`git diff --stat 02cf04b5 origin/main -- src tests
packages unity`). Then run every suite at the merged head on Yggdrasil: Caching, Mesh, Networking,
Geometry, WebSockets, ConsumerBuild, InteropPeer, cultcache-ts/-rs/-py, cultnet-ts and cultmesh-ts.
Run `old-reader-refusal.sh gate` for csharp, typescript, python and rust. If main gained code by then,
a Soul pass covers the merge.
6. **Land.** The land is `git merge --no-ff`, never a squash, so the SHAs that every report and verdict cites stay reachable from main (skip the branch's non-building commits with `git bisect --first-parent`). Steps 1 and 2 closed at 723b7996 (claimant-one-owner, land-c2a-gate); step 5 closed at the merge commit 3157d4b3 (Soul `cut-land-c2a-main.s1` holds). Step 3 closed except `cut-land-c2a.s1.stale-unity-plugin-dlls` and `cut-land-c2a-gate.s2.mutation-table-reads-gate-as-zero`, which wait on the 3.0.0 release cut and the Eureka tools respectively; step 4 closed with `consumer-pins-before-land` (pins since 2026-10-22). Run `git merge --no-ff hands/variants-c2a` onto main, with an empty diff against the
verified head. There is no tag: tags, the Unity plugin rebuild and `declared-range-gate-own-version`
belong to the 3.0.0 release cut. Open follow-ups `c2a-load-slower` (load 2 to 2.3 times slower than
main on a synthetic Aetheria catalog) and `drift-write-ownership-leaves` do not say "before land".
`c2a-load-slower` still needs a reason or a fix before the 3.0.0 release, under
`cultlib-gaps:ruling:database-shaped-built-for-load`.
## Facts
**1. The merge.** A trial `git merge origin/main` on a5c7954e gives 12 conflicted files and
33 hunks of about 950 marker lines (CultCache.cs 17 hunks/196; cult-cache.test.ts 337;
single-file-messagepack-backing-store.ts 4/106; rs lib.rs 2/51; VariantTests 51; three
CHANGELOG add/adds; store-composition contract 32; Directory store 14; InteropPeer 21).
Main moved 4.8k lines in the cache runtimes since base 1a12bc2d. Three semantic collisions
are not textual:
- *Durable record identity has two owners.* Main added `CultStoredDocument.PersistedSchemaId`
and `CacheBackingStore.MarkWritten` (73d438db, afd799ba, 3f6dda8b). Conditional commit
compares the persisted schema id plus storedAt. C2a already owns this as
`StoredSchemaId` + `StoredVersion` (SHA-256 of the stored bytes, which names the schema
id and storedAt) set by `CultCache.Wrote`. After the merge, 9 references in
ConditionalCommitTests and the contract name main's owner. The version is the owner.
PersistedSchemaId and MarkWritten are deleted, and main's +309 test lines are ported as
pins against StoredVersion. 2.0.0 ships MarkWritten as protected API, and C2a also
removes `SerializeSnapshot`/`DeserializeSnapshot` overrides and changes `ConditionsHold`.
C2a is therefore a major release (3.0.0), which semver forces.
- *TS store commit moved.* Main's `store-file.ts` (0e6452c9, 0010ee92; 391 lines) now owns
commit, replace and the `<path>.lock`. C2a's v3 read and keep-v3-on-rewrite
(`STORE_FORMAT_ELEMENT_IDS`) lives in the old store file and must move into store-file.ts.
- *Rust decode door.* Main's cultcache-rs 0.3.0 grown decoder (`decode_stack`, fc630d3b)
must sit under C2a's `readable_store_format` and v3 `decode_store_file`.
**2. The gate already holds; it needs a test, not a format change.** C2a stamps
`cultcache.store.v3` (single file) or `...v5.directory-content-addressed-pages` exactly when
a record holds an id (`HeaderFor`, CultCache.cs:3543). It also marks when a copied record
decodes holding one (`CopiedHoldIds`, :4285). The 2.0 C# reader accepts only v1/v2
(CultDocumentMessagePackSerialization.cs:229) and directory v4 (Directory store :452).
Main's TS, Rust and Python readers accept only v1 (store-format.ts `isV1Snapshot`, lib.rs:1274,
stores.py:192). Probe on Yggdrasil at 5b5d8573 plus C2a's `v3-base.msgpack` (job
`gate1.log`): 4/4 pass. The single file is refused naming v3, and a later flush leaves it
byte-identical. A v5-stamped directory store is refused naming v5, and its 4 files are
byte-identical after a later add and flush. **The pin:** C2a already ships
`tests/vectors/document-variants-c2a/old-reader-refusal.sh`, which stages an old revision
plus probes on a scratch ref and runs them per runtime. The land cut sets its
`OLD_READER_REV` default to the tag `cultlib-unity-v2.0.0`. It also adds the never-rewritten
assertion and the v5 directory case to the C# probe, and runs all four runtimes at the
tag. If the tag is absent, the land waits for it. The tag, not a package, is the pin: the tag's
tree holds every runtime's released reader.
*Outside C2a's reach (release session):* in 2.0, `CultMesh.WriteSingleFileDocument`/
`WriteSingleFileDocumentPayload` (CultMeshSingleFileDocuments.cs:211-245 at 7a8933b0) build
a fresh one-record v1 snapshot and replace the file without reading it. A 2.0 Mesh write
to a C2a v3 path therefore discards it whole, ids and other records included, and no header
gate can stop a writer that never reads. C2a's version reads under the lock and refuses.
Fix it in 2.0.0 before tagging, or record it.
**5. Consumers.** Aetheria pins a CultLib revision (`VerifyCultLibRevision`) and Unity tags.
Ghostlight, Eve's Rust crates, Huginn and the Brokkr daemon pin cultcache-rs by git rev.
None of them moves when main moves. Two C# consumers build against an unpinned sibling
`..\CultLib` checkout: Eve `GameCult.Eve.Surface.csproj:16` and Brokkr's three csprojs.
Eve.Surface has `[MessagePackObject]` element arrays with no `[CultElementId]`
(`EveAssetCatalogEntry[]`, `EveEntitySoaBuffer[]`, `EveInputActionDocument[]`,
`IReadOnlyList<EveCommandTemplate>`, 0 ids in the repo), so C2a registration refuses its
documents as soon as the checkout pulls the land. Grep finds no object lists in Brokkr's
`[CultDocument]` files (not build-verified). Removed C2a API (`Options`, `AsSchemaAlias`) is
unused by Eve and Brokkr. Fork: `variants:question:unpinned-consumers-at-land`.
## Classification
| Item | Class | Why |
|---|---|---|
| c2a-format-version-gate | **a** | The gate test above. No new header: v3/v5 already refuse. |
| slot-reuse-contract-doc | **a** | The ruled consequence of the write set C2a lands. One paragraph in cultcache-schema-compatibility.md, absent at a5c7954e (grep). |
| foreign-ids-under-v1-header | **b** | Obsoleted by fb065f0d (write-time `CopiedHoldIds`, after the finding) for every C# writer with the type. The residual (non-C# or opaque writers) is what `non-csharp-writers-mark-ids-2` ruled to carry, and the contract states it (persistence-format.md:434-439). The gate changes neither side, so no fork remains. Resolve as Superseded by that ruling. |
| custom-resolver-element-ids | **b** | Fixed by b091d21f (19:52Z, after the 18:27Z follow-up): `ListElementsClaimedByAResolverAreRefused`, `AResolverClaimingTheListTypeOfAnObjectListIsRefused`. s6 holds on the claim loop. |
| codec-door-proven-mismatch | **b** | Done in h6 57fdf70c (s6 `only-proven-mismatch` Holds; Geometry 76/0). |
| consumer-element-id-sweep-r2 | **c** | Each repo's sweep is its own work (q3). Only the unpinned trackers matter at land, and that is the question below. Aetheria bumps a pin on purpose (C4). |
| schema-discovery-holder-recogniser | **c** | `TryGetDictionaryRefTypes` is identical on main (CultCache.cs:1288). It describes record-ref dictionaries and is not on the id path. Changing it moves schema hashes, which is its own epoch event. |
| copy-check-memo | **c** | Performance only (about 3x flush on unmarked stores of 20k records). Correct as is. |
| string-key-field-and-shadow-pins | **c** | No wrong behaviour at 64ec14f9. These are test pins and may ride the land cut if Hands touches the file. |
| declared-range-gate-own-version, range-check-in-ci | **c** | The script exists only on C2a and runs in no CI. It guards a TS publish, not a merge. Fix both before the first TS release from main after the land. |
| s6 Unproven: Unity/IL2CPP identity, other MessagePack versions | **c** | The merge reaches no Unity build. Unity sees C2a only at the 3.0.0 tag, so measure it in the Unity release cut (operator Unity check, C3). |
| multi-store-commit-atomicity, store-replace-cut, reader-refuses-duplicate-keys, soft-flush-all-runtimes, writer-dangling-link-tolerance, unique-index-cut, py-shared-write-lock, rust-lock-open-waits, ts-lock-test-hardening, c0/legacy/directory vectors, culture-test, wire-id-cuts, remaining-variant-cuts, golden-path, paged store | **c** | None says "before land". Each is pre-existing on main or C2a-independent. None lets a released reader strip ids or corrupt a C2a store. |
## The land cut's shape (transcribe once the question is ruled)
- **Base.** Merge main at or after the `cultlib-unity-v2.0.0` tag into `hands/variants-c2a`.
Versions stay main's. CHANGELOGs get Unreleased entries, and the Unity entry says major.
- **Deletes and collapses.** Delete `PersistedSchemaId`, `PersistedIdentity`,
`CultStoredDocument.MarkWritten` and `CacheBackingStore.MarkWritten` (main, about 60 lines),
and the store-composition contract's MarkWritten paragraph. C2a's `StoredVersion`/`Wrote`
is the one owner. In TS, delete C2a's header handling in
single-file-messagepack-backing-store.ts and move it into store-file.ts. In Rust, C2a's v3
decode goes through the grown decoder.
- **Adds.** Retarget old-reader-refusal.sh to the tag. Add the C# probe's never-rewritten
assertion and the v5 directory case (as in gate1.log). Add the slot-reuse paragraph.
Port main's ConditionalCommitTests to the version identity.
- **Verification.** Full suites: Caching, Mesh, Networking, Geometry, WebSockets,
ConsumerBuild, InteropPeer, cultcache-ts/-rs/-py, cultnet-ts and cultmesh-ts, all on
Yggdrasil. Run old-reader-refusal.sh for all four runtimes at the tag, and it must refuse.
Negative greps over the merged tree: `PersistedSchemaId|MarkWritten`, conflict markers.
Mutation: drop the v3/v5 stamp in `HeaderFor` and the gate probe must fail.
- **Estimate.** About 950 conflict lines resolved. Net -60 to -150 from the identity
collapse and TS de-duplication, +80 to +120 tests and docs. One Hands pass of 100-150
calls, then one Soul pass.
- **Precondition (question).** Eve.Surface, and Brokkr if confirmed, are pinned or swept
before the merge reaches main.
## Store gate: attachment is the attach's hold (store-gate-one-owner r2)
Spec `variants:cut_spec:cut-store-gate-one-owner.r2` (base ec94ab98), question
`variants:question:pulled-variant-codec`. It closes Soul s3's
`attach-window-cacheless-write` (Medium) and `codec-loan-pinned-by-reflection` (Low).
**Facts (source reads at ec94ab98 and a probe, 2026-10-25).**
- The window. AddBackingStore holds the cache's gate, then the store's gate through
`InStoreHold`, lends the registry and codec, sets `Loaded`, adds the store to `_stores`,
pulls, and only then sets `store.Cache` (CultCache.cs 2268-2312). `InStoreHold` lets any
call on the same store's gate re-enter it (2825-2831). So a journal the pull's Admit runs
on the attaching thread can call `store.WriteDocument`: `Gate(cacheless)` guesses null,
re-enters the store's gate, finds `Cache == null` and runs the write, and its `finally`
calls `UseRegistry(null, null)` (Soul s3 P1, full1.log lines 49-53). Other threads are not
in the window: they queue on the store's gate and see either the published cache or a
store that the failed attach put back.
- The codec loan has no reader on the attach path. A pulled variant's codec is read only
by `CultStoredDocument.HoldsIds` (543-557). Header decisions read `HoldsIds` only on
staged entries (single-file 4318, 4447 and 4472; directory store 362), and a staged entry
is always the document the cache pushed (`Push` sets `Entries[key] = entry`, 4211-4215).
A single-file `PullAll` returns without loading while the store is dirty (4157-4159), so
a pulled copy is never staged. A refused write unstages its keys before it adopts the
reload. The directory store refuses variants (ruling `q6-directory-and-cultnet-refuse`;
DirectoryMessagePackBackingStore.cs 139-144 and 642-643). Admit re-resolves every pulled
variant with the cache's codec (CultCache.cs 3259). Soul's N4 mutant (the attach lends
no codec) was caught only by the reflection test, and h4's M2 only by a bare-store test
that sets `Loaded` and calls `UseRegistry` by hand.
- Probe (scratch clone `imagination-store-gate-r2/clone`, commits 4663934e and c099b9e8 on
ec94ab98; Yggdrasil t1.log, t2.log and t3.log; 2026-10-25 around 20:50 CEST). The
probe did three things. `InStoreHold` refuses a cacheless call that re-enters the same
store's gate. The `_codec` field and the two-argument `UseRegistry` are deleted. The pull
builds variants with a null codec. Results:
- Mesh is 342/0/4.
- In the Caching suite, the only failures were two probe tests, both from authoring
errors: a key `AddAsync` minted, and an element id that was not 12 hex characters.
No existing test failed, `UserCodeInACachelessCallMayCallItsOwnStore` included.
- Soul's P1 as a test: the journal's write is refused with the new message. The attach
keeps its registry and the file holds only `a`. After the attach, a cache write, a
flush and `store.PullAll()` all succeed.
- A cacheless write nested in another on the same store is refused, the outer write
fails, and no file is written.
- Through a real attach, a store holding a name-override variant flushes an unrelated
commit with a `cultcache.store.v2` header. With no codec loan, the pulled copy does not
mark the store.
- Mutant `return cacheless` changed to `return false`, which is ec94ab98's behaviour,
fails both new gate tests. In that mutant the nested write succeeds and the file exists.
**Rationale.**
- Attachment has one fact again. While the attach runs, the store's gate is the attach's
hold, and `Cache` is published when that hold ends. A cacheless write is a write the
store owns outright, so it must be the outermost hold of its store's gate. One more
argument to the existing owner of per-thread gate facts (`InStoreHold`) refuses it. No
field mirrors `Cache`, and `Loaded` and `_registry` are not read. Precedent:
`ConcurrentHashMap.computeIfAbsent` refuses a mapping function that updates the same
map with `IllegalStateException("Recursive update")` since Java 9. That is from
memory, not fetched this pass; the 2026-10-23 prior-art note covers the documented
restriction in its counter-positions. Non-cacheless store calls from user code still
re-enter: `UserCodeInACachelessCallMayCallItsOwnStore` stays green.
- The late publish stays. It is what bounds `Gate` at two passes (s2's finding). Publishing
early would bring back the failure-rate-bounded retry.
- The codec loan goes: it is a value nothing on the attach path reads. A pulled variant
carries no codec and counts as holding ids (the documented conservative answer), and
the cache's resolved copy is what any header decision sees. This reverses the codec
half of Defaulted ruling `late-publish-needs-attach-codec`, so it goes to Self as
`variants:question:pulled-variant-codec`. The spec is written for the recommended option.
- Scar check: `git log -S` over CultCache.cs for `reentrant`, `outermost` and `_attaching`
finds nothing. `Cache?.Codec` appears in 1468a83a (h4) and 619e089f only. No earlier
re-entry refusal was built and deleted.
- Behaviour change, stated in the CHANGELOG: a cacheless write made from user code that
another cacheless write of the same store runs used to succeed, and its `finally` took
the outer call's registry. It is now refused.
## Store gate: Cache is the one attachment fact (store-gate-one-owner r3)
Spec `variants:cut_spec:cut-store-gate-one-owner.r3` (base 0fa836cb), superseding r2. It
closes Soul s4's `attach-window-held-store-writes` (Medium), `cacheless-refusal-order-unpinned`
(Low) and `interop-consumer-suite-unrun` (Low). This is the third time the attach window has
beaten the rule (s3 from the cacheless side, s4 from the held side), so r3 moves the
decision instead of patching the next entry point.
**Facts: every entry point that reads or decides attachment, at 0fa836cb.** Source reads of
CultCache.cs, DirectoryMessagePackBackingStore.cs and CultMeshSingleFileDocuments.cs. The
window column is a journal on the attaching thread, through a real attach of a store holding
`a`. Probe: scratch clone `imagination-store-gate-r3/clone`, commit b2330b6d (test R3Probes),
Yggdrasil `probe-base.log`, 2026-10-25 around 23:00 CEST. "Disagree" means that after the
attach, a commit of `later` and a flush, the attaching cache holds `a,later` and a fresh cache
over the same file reads something else.
| Entry point (line) | Fact it reads | In the attach window at 0fa836cb |
| --- | --- | --- |
| Single-file `PullAll` (4156) | `Gate`: `Cache` guessed unlocked, then re-read under the re-entered store gate | Runs as detached; its `Loaded` re-enters the cache and is refused as a journal write. Agrees. |
| Single-file `Push` (4212) | same | Runs as detached and stages `b`. Disagree: fresh reads `a,b,later`. |
| Single-file `Delete` (4223) | same | Runs as detached and stages a removal the cache never made (dirty after the attach). |
| Single-file `PushAll` (4239), and `Dispose` (3722) through it | same, then in-body `Cache == null` (4531, 4543, 4588, 4603, 4611, 4661) | Runs as a cacheless writer. Push then PushAll writes `b` to the file inside the attach (disagree). Delete then PushAll is refused with the cacheless writer's text. On a variant store it is refused as "a writer with no CultCache" (Soul s4). |
| Single-file `CommitBatch` (4256) | same | Runs as detached and commits `b`. Disagree. |
| Single-file `JudgeChecks` (4568), `RefuseRemovalOfUndeclared` and `StageChecks` (4558, 3915) | same | Run as detached. Agree in the probe, but under the wrong gate's rules. |
| Single-file `ReadStoreSnapshot` (4687) | same | Runs as detached and reads the file. |
| `WriteRecord` (4342), `WriteDocument` (4366) | `InStoreHold`'s per-thread hold (cacheless re-entry), before `Cache` | Refused with the re-entry text (h5). Agree. |
| Directory `PullAll` (48) | `Gate`, as above | Refused as a journal write. Agree. |
| Directory `Push` (147), `Delete` (160), `CommitBatch` (172) | same | Run as detached. Disagree: fresh reads `a,b,later` after Push or CommitBatch, and `later` after Delete. |
| Directory `PushAll` (235) | same | Runs as detached. Push then PushAll and Delete then PushAll disagree. |
| Directory `JudgeChecks` (269), `RefuseRemovalOfUndeclared` (259) | same | Run as detached. Agree. |
| In-body reads: `StoredAtOver` clock (4028), refusal text and `DurableAt` (4531, 4543), declares and cacheless refusal (4588, 4603, 4611), `Wrote` (4661) | `Cache`, under the gate `Gate` chose | On the attaching thread they read null while the thread holds the cache's gate, so a held call runs as a cacheless writer. |
| `AddBackingStore` (2259-2314) | writes `Cache` under both gates, after the pull | The window's author: the store is attached for the pull (registry lent, `Loaded` set, in `_stores`) while `Cache` is null. |
| Mesh `CultMeshSingleFileDocuments` writes and reads (99, 143, 174, 226) | none of the attached store's | `StoreAt` (239) builds a new store per call, so they never touch an attached store object. A file it shares with an attached store is the file lock's and the write set's business. |
| `IsDirty`, `HasStagedChecks`, `ForeignRecords`, `LastSchemaMigrationReports`, `HeaderFor`, `HeaderForWrite` | none | Ungated values or static decisions. They read no attachment fact. |
So during an attach, cacheless entry points decide attachment from the hold, and every held
entry point, plus every in-body read, decides it from `Cache`, which is still null. These are
two facts.
**Probe of the chosen shape** (commit 4695390c on b2330b6d; `proto-full.log`). `AddBackingStore`
publishes `Cache` before the pull and takes it back in the same hold if the pull fails. `Gate`
becomes straight-line code: guess, then the store's gate, where `Cache` is settled, then the
settled cache's gate. Results:
- All 21 probe cases are refused. The cacheless writes get the re-entry text and every other
case gets "A journal entered the cache it journals". After the flush, the attaching cache
and a fresh cache both read `a,later`.
- Caching: the only failures are the two tests that pin the late publish:
`AnAttachLendsTheRegistryAndPublishesTheCacheAfterThePull` (asserts no cache during the
pull) and `ACallWhoseGateChangesTwiceUnderItThrowsInsteadOfLooping` (asserts the old message
text; the tampering reader still throws). The churn, queued-reader and failed-attach tests
pass unchanged.
- Mesh and Networking are green.
- The cultcache-ts suite, which builds and drives the C# InteropPeer, passes on this shape:
175 tests, 171 pass, 0 fail, 4 skipped. The C#, Rust and Python interop tests ran, the C#
edit-in-place one included (`proto-ts2.log`, commit f5f02053). Recipe `clone/r3ts.sh`:
image node:24-bookworm, apt python3-msgpack, dotnet-install for 10.0, rustup minimal.
**Shapes weighed (rationale).**
- (a) Publish `Cache` before the pull, as a616431e did. Chosen. What s2 found was not that an
early publish is unbounded. It was that `Gate` re-read `Cache` *unlocked* after a stale
guess, so a churn of failing attaches could hand it a new stale guess every pass. The fix is
where `Gate` reads on its second step. Under the store's gate, `Cache` is settled: an attach
publishes and takes it back inside one hold of that gate, and no other thread can hold that
gate during the attach. A settled cache is permanent, because there is no detach. So `Gate`
needs at most three acquisitions, with no loop and no counter: the guess's gate, the store's
gate, and then the settled cache's gate, which cannot miss. This deletes a fact rather than
reconciling two. During the attach the store *is* attached, so every entry point, cacheless
or held, and every in-body read sees one answer. A journal's call is then refused by the rule
journals already have (AddJournal: "any hold on this cache while a journal runs throws"),
exactly as it is after the attach. Prior art: `ConcurrentHashMap.computeIfAbsent` installs a
ReservationNode in the bin *before* running the mapping function, so a recursive update on
the same thread finds the owner and throws, and on failure the reservation is removed (see
`docs/research/2026-10-23-store-lock-user-code-prior-art.md` item 4 on main: the re-check
after locking, and the ReservationNode on the empty-bin path). The validate-after-acquire in
`Gate` is the same item.
- (b) Keep the late publish and make the attach's hold the one fact for every entry point.
Rejected. `InStoreHold` knows that this thread holds the store's gate, not *whose* hold it is.
Refusing every same-store re-entry kills `UserCodeInACachelessCallMayCallItsOwnStore` (r2's
R5), so (b) needs an "attaching" marker beside `Cache`. That is the second fact that s3 and
s4 rule out, plus every in-body `Cache == null` read would need to consult it as well.
- (c) Run no user code on the attaching thread under the store's hold during a pull: defer the
pull's journals to after the hold (open calls). Rejected as insufficient on its own. Getters
and constructors still run during the pull's decode, so the window stays open to any user
code that is not a journal. It also moves journal timing, which is a documented contract
("right after an admission is applied").
- Scar check: `git log -S` over CultCache.cs finds `store.Cache = null` and `MaxStaleGuesses`
only in ba658965 and 1468a83a. The early publish was not removed for a correctness reason of
its own: 1468a83a removed it to bound `Gate` and to lend the codec, and the codec loan is gone
(`pulled-variant-codec`). No settled read under the store's gate was built before. The
precedent recipe (no root filter) finds no same decision.
- Not a product fork. A journal's store call during an attach now throws, as it already does
after the attach. Cacheless writes on the attaching thread stay refused, with the same text.
Getters and constructors during the pull see an attached store, as they do during any held
call. It changes the late-publish clause of Defaulted ruling `variants:ruling:pulled-variant-codec`,
so Self defaults a superseding ruling (recommended label `cache-published-for-the-pull`)
before dispatch.
- Residual, not in r3. `Push`, `Delete`, `PushAll` and `CommitBatch` are public, so user code
that is not a journal (another thread, or a getter during any held call) can stage records
behind an attached store's cache. Soul s4 shows this with `other-thread-push`: file `a,b`,
cache `a`. That is true before, during and after an attach, so it is not the window's
two-fact problem. Closing it changes public API (a store's writes taken only from its
cache), so it is a follow-up and an operator question for Self, not part of this cut.
## Body facts: claimant-one-owner r2 (Imagination, 2026-10-27, base fc74cf6f)
Why a revision: Soul beat the same rule three times (s1 claimant choice, s2 compatible ids, s3 shape). Each time a
cacheless write lent a registry let the registry's entry for the written schema id decide ownership beside the entry
the caller passed. Two descriptions of one type reached `Owns`; each fix (h2 schema-id compare, h3 subset of compatible
ids) compared one more facet of the two and left the rest open. r2 removes the second description from the lookup.
### Where the two descriptions met (source read, fc74cf6f)
- `OwnsOnDisk` cacheless arm (`CultCache.cs:4725-4740`): with a lent registry it called
`lent.Owns(id, catalog, declared, written)`, whose lookup is the registry's `_indexes` and whose `entryOf` is
`descriptor.ToCatalogEntry()` (`:824-825`). The claimant for the written schema id was the registry's descriptor,
so `ReadsWhole(persisted, claimantEntry)` (`:848`) judged shape against the registry's entry, and the claimant
precedence (`:756-806`) walked the registry's compatible ids. `written` (`:4734-4736`) then compared the two entries
by schema id plus a subset of compatible ids.
- `WriteDocument` (`:4495-4524`) takes its entry from the registry (`resolvedRegistry.GetRequired<TDocument>().ToCatalogEntry()`),
so its two descriptions are always identical. Only `WriteRecord(record, catalogEntry, holdsIds, registry)` (`:4471`) can
pass an entry that disagrees with the registry's entry for the same id.
- `CopiedHoldIds` (`:4600-4630`) also reads `AttachedRegistry`, to decide the v3 header from copied records. It is a
positive-only header decision, not ownership, and out of this cut.
### Consumer audit (2026-10-27)
- `WriteRecord` and `WriteDocument` are new in the Unreleased 3.0.0 entry; `main` has neither (`git show main:src/GameCult.Caching/CultCache.cs`).
- Per-repo `rg` over every git repo directly under `F:\Projects` for `.WriteRecord(`, `.WriteDocument(`, `WriteSingleFileDocument`
(Library, obj, bin, PackageCache excluded): hits only in CultLib and in vendored CultLib copies under Heimdall and StreamPixels
(`vendor\CultLib`, older pins without these methods). No consumer outside CultLib calls either method.
- Inside CultLib at fc74cf6f: Mesh `WriteSingleFileDocument` calls `WriteDocument(key, document, storedAt, registry)`;
Mesh's raw-payload path calls `WriteRecord(..., registry: null)` (`CultMeshSingleFileDocuments.cs:129, 226`). The only
`WriteRecord` calls with a registry are tests: `CachelessSingleFileTests.cs:453, 660` (a registry `{IdDeck}` that does
not hold the written entry `One` at all, used for the copied-record header), `RestampOnWriteTests.cs:595, 599`, and
Soul's probes. Packages: no TS, Python or Rust counterpart takes a registry (cultnet-rs `write_record` is an example helper).
- Consequence: the registry's documented role in `WriteRecord` is to describe the other records in the store. A shape
that derives the written entry from the registry (or refuses an entry the registry does not hold) breaks
`CachelessSingleFileTests.cs:660` and removes that role.
### Candidate shapes weighed
| Shape | One description? | Public API | Kills s2/s3 | Cost |
|---|---|---|---|---|
| A. Lent registry's entry decides; refuse a passed entry that disagrees | yes, after a door check | signature kept; new refusal | yes | an equality check over a redundant parameter; breaks the registry-describes-others use (`:660`) when the registry lacks the written type unless it falls back to the passed entry, which is two descriptions again |
| B. Passed entry is the only description of its schema id; the registry supplies other types only | yes, by construction | signature kept; doc only | yes (probed) | one lookup class gains the registry; one registry overload deleted |
| C. Overloads: `WriteRecord(record, catalogEntry, holdsIds)` and `WriteRecord(record, registry, holdsIds)` | yes, by type | changes a 3.0.0 signature | yes | loses the registry-describes-others use |
| D. Delete `WriteRecord`'s registry parameter | yes | changes a 3.0.0 signature | yes | loses the copied-record header decision for raw writes |
Picked B. It is the only shape that keeps every documented use of the registry, needs no public signature change,
and deletes the comparison instead of extending it. It is ordinary scoping (an inner binding shadows the outer one
for its name), applied to schema ids.
### Probes (Starfire, local filtered runs, 2026-10-27)
Scratch clone of fc74cf6f at `<scratchpad>/imagination-claimant-r2/clone`, Soul's `SoulClaimantProbe.cs`,
`SoulClaimant2Probe.cs`, `SoulClaimant3Probe.cs` copied into `tests/GameCult.Caching.Tests`.
Base fc74cf6f (`probe-base.log`):
- P7 (registry {N lists own, A, B}; passed entry N with the list varied; record k under O, named like N): lent R / no
registry C for rows b, b2, e, g, h, k, l, l2; lent C for a, c, d, f, i, j. Confirms `subset-refuses-own-drift`.
- P9 (registry {NW: Name, Value, Colour}; passed NW minus Colour): lent C (restamped, Colour lost), no registry R.
Confirms `lent-shape-widens-passed-entry`.
- P6 (bare X1 entry, registry {X1} listing OX, record under OX): lent R, no registry R, cache C. h3 closed the id axis.
- P4: 12 states x 8 registries, violations=0. P1d: a cache {N,L} commits N over L's listed old id (`cache-replaces-other-types-record`).
Prototype B (scratch commit 349ea3f3 on the clone, not for landing; `probe-protoB2.log`): `CatalogEntryLookup` takes
the written entries and an optional registry and drops every registry descriptor whose schema id a written entry has;
the registry's `Owns(schemaId, catalog, declared, mayClaim)` overload is deleted; the cacheless arm always asks
`CultDocumentRegistry.Owns(id, catalog, declared, new CatalogEntryLookup(writes, AttachedRegistry), entry => entry,
claimant => writes has claimant's schema id)`.
- P7: lent == no registry on all 14 rows (all C).
- P9: lent R, no registry R, wide entry C.
- P6: unchanged (R, R, C). P4: identical table to base, violations=0. P1a-f, P2a-d: identical to base.
- `RestampOnWriteTests`, `CachelessSingleFileTests` and the probes: 139/139 passed locally.
- Yggdrasil, 349ea3f3, `ygg-protoB-full.log`: GameCult.Caching.Tests 1180 passed / 0 failed / 2 skipped;
GameCult.Mesh.Tests 342 / 0 / 4.
Negative greps, each tested at fc74cf6f (each matches there, and none may match after the cut, src and tests):
`claimant.CompatibleSchemaIds.All(` (1, the subset), `lent.Owns(` (1), `Func<CultSchemaCatalogEntry, bool> mayClaim) =>`
(1, the deleted overload), `CatalogEntryLookup(writes)` (3, one-argument construction). CHANGELOG and docs:
`never widens what the passed entry declares` (1), `with no compatible id the passed entry does not list` (1).
### Not settled by r2
- `cache-replaces-other-types-record` (s2, PreExisting, cache arm): r2 touches only the cacheless lookup, so it does
not settle whether a cache may replace another registered type's record at a shared key.
- Whether a registry-less `WriteDocument` should consult `CultDocumentRegistry.Shared` for claimants at all (s1
`shared-decides-registryless-write`, closed Fixed by documentation in h2). r2 keeps today's answer.
- `variants:finding:cut-restamp-on-write.s2.cacheless-inverts-registry-claimant` is still in force although r1 was
mapped to close it; Self should close or re-triage it after r2's Soul pass.

View file

@ -826,3 +826,15 @@ Recorded, not forked:
- **Crash between the store write and the log append** leaves the log lagging the store, and nothing detects
it; a restarted primary reuses that sequence (A2's divergence reached by a plain kill). The spec called this
lag detectable; for a crash it is not.
- **Cut 4 final Soul (4d), recorded 2026-09-30 (Self), not fixed in Cut 4:**
- **F2, read cost.** `CultNetDatabase.Serves` (`:1980-1989`) runs per row in `GetAll`/`GetByName`/`GetByIndex` for every
layout except a single owns-everything shard: a reflection `GetTrackedKey`, a gate lock and a shard re-scan per row.
`database.GetAll` measured 24.8x `cache.GetAll` on 20,000 rows under a key-prefixed shard. Fix: filter by key inside
the cache enumeration (the `rowFilter` path already has keys) and hoist the owns-everything check.
- **F4, dispose drain edges.** An observer that removed its stash entry before the drain can publish after `Dispose`
returns (dropped); a throwing `_cacheJournal.Dispose()` skips the drain. Neither double-publishes or deadlocks.
- **F5, refusal naming.** A write whose schema is owned but whose key is outside the shard's prefix also answers
`unowned_schema` with details `{field:"schemaId"}`; the peer cannot tell a key refusal from a schema refusal.
- **Pre-existing, separate task.** Python emits error code `unsupported_schema_version`
(`cultnet-py/.../interop_peer.py:415`, `cultmesh-py/.../server.py:161`), which is not in the error-code enum; TS and
Rust refuse the whole message.

View file

@ -0,0 +1,307 @@
# CultCache Union Parity: Map
Campaign `cultlib-gaps`, follow-up `cultlib-gaps:follow_up:union-parity-and-unknown-arms` (from
`thing2:follow_up:cultcache-union-parity`), under operator ruling `thing2:ruling:contract-decode-tolerance`
("CultCache refuses unknown union arms in every runtime, the C# reference included"). This file holds body facts,
the model page and rationale; the cut specs and the question are documents in the mind and cite this file by
section and fact number.
Pinned base: `origin/main` at `aaf0d7f6` (2026-10-10). Imagination pass `imagination-cl-union-parity`, session
`self-2026-10-10c-eureka`. Probes live under the session scratchpad, `imagination-cl-union-parity/` (`cs` payload
probe, `cs2` store probe, `rs` payload probe, `py` store walkers, `il` decompiled MessagePack-CSharp). None of it is
committed. Earlier evidence is not repeated here: Eve `docs/thing2-map.md` P12-P13 (the cross-runtime probe) and
`docs/research/cultcache-union-decode-tolerance-prior-art.md` (prior art).
## Cut order
1. `union-refusal-cs`: the C# reference refuses an unknown union arm on read and a subtype the union does not name
on write. Ruled; nothing blocks it.
2. `union-golden`: the recursive-union golden and the arm refusal goldens in `tests/vectors/union-parity/`,
written by the C# reference and held by its tests.
3. `union-derive-rs` and `union-codec-ts`, in either order or together: cultcache-rs and cultcache-ts gain a
union (and, in TS, a slot record) codec that writes the reference shape and decodes the golden byte-identically.
`union-codec-py` follows the same golden for cultcache-py.
4. `slot-tolerance` (C# reference), then `slot-readonly-parity` (Rust, TypeScript, Python): a record carrying a
slot past its reader's declared slots, at any depth, is read-only (ruling
`cultlib-gaps:ruling:extra-trailing-slots`; the refinement is settled by section S). Both change a slot rule the
cuts in step 3 build, so they wait for 1-3 rather than reopening them.
5. `union-parity-release`: changelogs and tags, Breaking where `docs/semver-policy.md` says so.
Thing2's `cut-contract-rs` waits on step 3 (Rust) only: its first step pins cultcache-rs at the commit that lands
`union-derive-rs` and re-runs P12 or the `union-golden` vector both ways.
Why this order: the C# reference is the wire authority, so its refusal and its golden come first and the other
runtimes are held to bytes C# wrote, as `document-variants-c1` was (`docs/runtime-parity-scope.md`). Arms are
ruled and slots are not, so the slot rule is its own cut rather than a blocked step inside every runtime's cut.
## Model page (step 0b)
| Kind | What names it | Over time | Who decides |
|---|---|---|---|
| Union arm | An integer key, `[Union(k, typeof(Arm))]` on the abstract base (C#), assigned by the union's author. The wire carries `[k, armSlots]`; the arm's type name never travels. `nil` is a null reference, not an arm. | **Added:** a key never used before. **Retired:** the attribute is removed or commented out and the key is never reused (Aetheria `Behaviors.cs:160,173`; Aetheria-legacy `Behaviors.cs:37-38`). **Renamed type:** free, the key is the identity. **Unknown to a reader** (a newer writer's arm, or a retired arm in an old store): refused, naming the union and the key (ruling). | The union's author assigns keys. The C# reference defines the shape; every runtime keeps parity with it (doctrine, `runtime-parity-scope.md`). |
| Arm subtype on write | The value's runtime type must be one the union names. | A subtype the union does not name is today written as `nil` (U4). After cut 1 it is refused. | The union's author, by naming the arm. |
| Arm slots, record slots | `[Key(n)]` integers, base-class keys included (an arm is a flat array). Gaps are written `nil`. | **Added:** appended at the next free key. **Retired:** kept as a gap, never reused (Aetheria `ItemInstance.cs:35`, `EntitySerializer.cs` key 5). **Missing in an older store:** the reader's default (C# field initializer; Rust and Python need a declared default). **Extra past the reader's last key, at any depth:** the record is read and is read-only; every write or removal of it is refused, and it is persisted from its original bytes (ruling, S). | The type's author. The slot rule is CultCache's (`src/GameCult.Caching/Contracts/cultcache-schema-compatibility.md`). |
| Serde-enum member (Rust) | serde's external tag: the variant **name** (`{"Name": fields}`, a unit variant the bare string). | Persisted already by Rust consumers (section C). They keep their bytes: the union derive is opt-in and serde enums are untouched. | The Rust consumer. Not a cross-runtime shape. |
## U. Mechanism facts (C# reference, Rust, TS)
- **U1. The miss branch is `Skip` then `null`, in both union formatters MessagePack-CSharp 3.1.7 can hand out.**
`DynamicUnionResolver.BuildDeserialize` (decompiled from `MessagePack.dll` 3.1.7 net9.0 with ilspycmd): after
`ReadArrayHeader() == 2` (else `InvalidOperationException "Invalid Union data was detected"`) and `ReadInt32`, a
non-zero-start key set is mapped through `keyToJumpMap.TryGetValue`, a miss sets the index to `-1`, the `switch`
default calls `reader.Skip()`, and the result local stays `null`. The source generator's `UnionTemplate`
(`MessagePackAnalyzer` 3.1.7, `MessagePack.SourceGenerator.dll`) emits the same: `default: reader.Skip(); break;`
with `result = null`. Its serializer writes `nil` when `typeToKeyAndJumpMap` has no entry for `value.GetType()`.
- **U2. The C# reference decodes an unknown arm to null and does not re-encode to its bytes.** Probe `cs`
(payload built by hand with `MessagePackWriter`, decoded with `CultDocumentMessagePackSerialization.OptionsFor`):
unknown key 9 in a union field gives `Root=NULL`, in a list `Items=[TextNode,NULL]`; re-encode equal: False in
both. The formatter the reference composite hands out for the union is `MessagePack.Formatters.NodeFormatter1`
(DynamicUnionResolver's emitted type; the probe assembly carries no generated resolver).
- **U3. Missing and extra trailing slots, C# payload level.** Same probe: a payload one slot short decodes with
the field initializer (`Added=42`); a payload one slot long decodes with the extra slot skipped. Neither
re-encodes to its bytes.
- **U4. The reference writes an unnamed subtype as nil.** Same probe: `Root = new RogueNode()` (a subclass the
union does not list) serializes to `94 a2 64 32 c0 90 2a`: the root slot is `nil`, and it reads back `Root=NULL`.
Silent loss on the write side, the mirror of U2.
- **U5. Refusal can live at the CultCache layer; no MessagePack-CSharp fork.** Same probe, `strict` options: a
resolver placed ahead of `StandardResolver` returns, for any abstract or interface type with `[Union]`s, a
formatter that peeks (`CreatePeekReader`) the `[key, ...]` header, throws
`MessagePackSerializationException("union Node has no arm 9")` for a key the union does not list, refuses to
serialize an unnamed subtype, and otherwise delegates to the formatter `StandardResolver` would have handed out.
Baseline round-trips byte-identical; both unknown-arm cases and U4 are refused; slot behaviour is unchanged.
Every C# decode path reaches the composite through `CultDocumentMessagePackSerialization.OptionsFor`
(`CultCacheMessagePack.CreateCodec`, `DirectoryMessagePackBackingStore.cs:345`, `CultMesh.cs:2779,3493,3499`,
`CultMeshPrimitives.cs:1158,1230,2357`), so one resolver covers the cache, the directory store and CultMesh.
Consumer resolvers named by `[CultCacheFormatterResolver]` sit ahead of `CultDocumentResolver` today
(`CultDocumentMessagePackSerialization.cs:58-62`); the guard goes first and delegates to the rest of the
composite, so a consumer formatter still encodes the arm while the key check still runs.
- **U6. A top-level extra slot is dropped on write-back.** Probe `cs2`: a C#-written store, mutated as a newer
writer would (catalog member slot 2 added, payload slot 2 `"newer writer data"`), is pulled, the record edited
and flushed. The read succeeds, and the written payload is `['n1', 'edited by the older build']`: the newer
writer's slot is gone. (The schema id was left unchanged, so the migration report said `Kind=Exact`; with a
changed id the documented drift path reports `IgnoredExtraSlots`, `CultCache.cs:840-924`, but writes back the
same way.)
- **U7. Rust at `aaf0d7f6`, before the union cuts.** Probe `rs` (`cultcache-rs` at `aaf0d7f6`, payloads built with `rmpv`, decoded with
`rmp_serde::from_slice`): a serde enum's unknown variant is refused (`unknown variant 'Exploded'`); the C# arm
shape `[0, [...]]` is refused (`expected variant identifier`); a non-nil value in a gap slot is skipped; a missing
trailing slot is refused (`missing field 'added'`) unless the field is `#[cultcache(default)]`; an extra trailing
slot is skipped (`cultcache-rs-derive/src/lib.rs`, the `IgnoredAny` loop after the slot loop). The derive
accepts named structs only (`lib.rs:51-67`) and writes an empty member list into the catalog
(`src/lib.rs:2706`).
- **U8. TypeScript at `aaf0d7f6`, before the union cuts.** No typed codec: with no `formatter` a document encodes as a MessagePack **map** of the
zod-parsed object and decodes raw (`cult-cache.ts:679-692`; `@msgpack/msgpack` `encode({id:'d',version:7})` is
`82 a2 69 64 ...`, a fixmap, where the reference writes an array); positional slots exist only in hand-written
formatters (`test/cult-cache.test.ts:1263-1310`). P13 measured that a hand codec matches whichever encoding its
author chose.
- **U9. Python at `aaf0d7f6`, before the union cuts.** `define_database_entry_type` (`packages/cultcache-py/src/cultcache_py/documents.py:185-276`)
is a slot codec: gaps written `nil`, extra slots ignored, a missing slot defaulted or refused. No union codec.
- **U10. The written contracts.** `cultcache-schema-compatibility.md:75-91` classifies "persisted slot is missing
locally and can be ignored" as soft-migratable drift, reported as `IgnoredExtraSlots`;
`cultcache-persistence-format.md:215-218` says a store reader "never skips a slot it does not understand" for
record envelope slots, and every runtime refuses `extra-slot-full-payload.msgpack`
(`tests/vectors/document-variants-c0`). `docs/persisted-record-encoding.md` says consumers verify canonicality
by re-encoding and comparing bytes. The two contracts disagree about payload slots; the question below is
about that disagreement.
- **U11. Fixture home.** `tests/vectors/<name>/`: committed bytes every runtime's tests read
(`document-variants-c0` by Python `generate.py`, `document-variants-c1` written by C#), consumed by
`tests/GameCult.Caching.Tests/PreCut2StoreFormatTests.cs`, `packages/cultcache-rs/src/lib.rs`,
`packages/cultcache-ts/test/cult-cache.test.ts` and `packages/cultcache-py/tests/test_cultcache.py`. The CultNet
precedent for a reference-written vector is `CULTNET_WRITE_VECTORS=1` (`docs/cultnet-error-contract-cut.md:170`).
## S. Slot read-only probe (any depth)
Imagination pass `imagination-cl-slot-readonly` (session `self-2026-10-10c-eureka`), under ruling
`cultlib-gaps:ruling:extra-trailing-slots` ("b, and probe the any-depth refinement"). Probes live under the session
scratchpad, `imagination-cl-slot-readonly/` (`cs` C# synthetic, `aeth` C# on Aetheria's game database, `rs`, `py`,
`ts`, `il` decompiled MessagePack-CSharp 3.1.7). None is committed. The ruling's test: every runtime's decoder
carries a "slot skipped" mark to the record cheaply, on the one decode path, with no second pass and no per-type
consumer code.
- **S1. Where C# skips, and why a resolver sees it.**
`DynamicObjectTypeBuilder.BuildDeserializeInternalDeserializeLoopIntKey` emits the slot loop's `switch` default as
a direct `MessagePackReader.Skip()` call; the source generator's `FormatterTemplate` writes
`default: reader.Skip(); break;`. `MessagePackReader` is a struct, and neither path raises an event or reads an
option, so the skip itself cannot be observed without a fork. Both fetch every member formatter from
`options.Resolver` (the generated code's `formatterResolver = options.Resolver`), and the union formatters fetch arm
formatters the same way (U1). A resolver ahead of the composite is therefore handed every nested object, list
element and union arm, and can read each one's array header before delegating.
- **S2. The mark arrives in C#.** Probe `cs`: a guard resolver returns, for each concrete int-keyed
`[MessagePackObject]` type, a formatter that reads the array count (from `NextCode` for a fixarray, a peek for
array16 and array32), compares it with the highest `[Key]` plus one (base keys included, MessagePack's member
visibility rule), notes a skip into a `[ThreadStatic]` scope the decode's caller opened, and delegates to the
formatter the rest of the composite hands out. An extra slot at depth 0 (record), depth 1 (nested object), depth 2
(object in an object), in a union arm and in a list element each arrives as one mark naming the type and both
counts; the clean payload gives none. The guard needs `CompositeResolver.Create` around it so formatters are
cached (uncached, it was 40 times slower).
- **S3. C# cost.** Aetheria `GameData/Aetheria.cc` (12.1 MB; 237 of 238 records decodable with Aetheria.Shared's
types; decoded with `OptionsFor` that assembly; best of 9, three rounds): plain 165-209 ms, a second plain composite
(A/A) 174-193 ms, guarded 179-196 ms. That is 3.7-8.9% over the best plain run, inside the A/A spread. Worst case,
synthetic tiny objects (120,000 records, 23.1 MB, about 24 objects each): plain 618-637 ms with A/A within 4%,
guarded 704-705 ms: 11-19%, about 30 ns per decoded object.
- **S4. A live case in Aetheria.** The same probe, run with the AetherDb release binaries (built 2026-09-30) against
today's `Aetheria.cc`, marks 25 records: `WeaponItemData` whose `BehaviorData` arms (`AutoWeaponData`,
`ChargedWeaponData` and others) carry 34 slots where that build declares 21 or 33. The extra slot is
`WeaponData.Tracking` (key 33, Aetheria `8cd71ed6`, 2026-10-07): inside a union arm, inside a list. Today that tool
reads these records and drops `Tracking` from all 25 at its next landing (S5). Refusing nested extra slots would
stop it opening `Aetheria.cc` at all; read-only lets it open the store and refuses writes to those 25.
- **S5. A C# flush re-encodes every held record, not only the edited one.** `PushAll` and an unconditional
`CommitBatch` write `ToPersistedRecord(entry, SerializePayload)` for every entry (`CultCache.cs:3191-3201`, `3355`,
`3384`), under the local descriptor's schema id and catalog entry. So U6's loss reaches every held record with
unread slots at the next flush of anything. Rust (holds payload bytes and decodes on `get`, `lib.rs:2280-2293`),
Python (holds envelopes and decodes at load, `cache.py:257-285`) and TypeScript (holds payload bytes) carry an
untouched record's bytes. In C#, read-only therefore also means persisted from the original record (bytes, schema
id, catalog entry), never from the decoded document.
- **S6. Rust.** Probe `rs`: the derive's slot loop, with the trailing `IgnoredAny` loop counting into a
`thread_local`, marks depth 1, depth 2 and a list element. Depth 0 is the `DatabaseEntry` derive's own trailing
loop (`cultcache-rs-derive/src/lib.rs:258`), the same edit. A plain serde struct nested in a record already refuses
an extra element (`array had incorrect length, expected 2`), so only derived slot types skip. Cost (120,000
records, 21.4 MB): inside the A/A spread (A/A from -3% to +9%; marked against plain from -14% to +13% over six
runs). The added code is one branch per object, never taken on a clean payload. Rust decodes on `get`, so whether
a held record is writable is answered by decoding its held bytes through the same derive, on the write path only.
- **S7. Python and TypeScript.** Probes `py` and `ts`: a slot codec tree passing a context argument (the shape
`union-codec-py` and `union-codec-ts` build) marks all five positions. Python (40,000 records, 11.1 MB, msgpack
unpack included): +14% against an A/A spread of 8%. TypeScript (120,000 decoded records, codec walk only; no
msgpack in the scratch copy): +20% against an A/A spread of 18%. The check is one length comparison per object.
- **S8. Kotlin has no library slot codec.** `CultDocumentCodec<T>.decode` is written per type by the consumer
(`packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt:31-36`), and the package's own Eve
decoders skip extras by hand at 8 sites (`repeat((count - N).coerceAtLeast(0)) { reader.skip() }` in
`eve/EveDocuments.kt`). No slot rule, refusal or read-only, can be held by the library there until it owns slot
decoding. That is a follow-up, not a per-runtime rule.
- **S9. CultMesh decode paths** (`CultMesh.cs:2779,3493,3499`, `CultMeshPrimitives.cs:1158,1230,2357`) go through
`OptionsFor`, so through the guard, with no scope open: unchanged. Whether a Mesh replica that re-publishes a
decoded document drops slots is a follow-up.
- **S10. The refinement is in.** Every runtime with a library slot decoder (C#, Rust, Python, and the TypeScript
codec being built) carries the mark to the record on its one decode path, with no second pass and no consumer
code. The cost on Aetheria's real store is inside the A/A noise (S3); the worst synthetic case is 11-19% in C#.
A nested extra slot makes the record read-only; it is not refused.
## A. Aetheria (C#, the only consumer persisting `[Union]`s)
Aetheria consumes the reference through Unity packages pinned by tag (`Packages/manifest.json:55-57`:
`caching-unity-v1.4.0`, `cultlib-unity-v1.0.60`), so a CultLib change reaches it only when it bumps the pin.
Its only consumer resolver is `MathResolver` (`AssemblyInfo.cs:4`), which supplies no union formatter.
- **A1. Unions inside persisted documents** (Aetheria `aac2cf91`, `Assets/Scripts/ServerShared`):
`BehaviorData` (`Behaviors/Behaviors.cs:147-191`, keys 0-11, 15, 16, 18, 20-24, 28, 31-40; 12 and 26 retired,
14, 17, 19, 25, 27, 29, 30 commented out) in `List<BehaviorData> Behaviors` of the item data documents
(`ItemData.cs:346,368`); `WeaponData` (`Behaviors/Weapon.cs:15-18`); `EntityPack` (`EntitySerializer.cs:172-173`)
in `aetheria.savedzone` contents and its own `Children`; `ItemInstance` and `CraftedItemInstance`
(`ItemInstance.cs:18-31`, a union whose arms are also a union) in entity cargo and docking bays
(`EntitySerializer.cs:186-187`); `Provenance` (`Provenance.cs:96-98`) in `aetheria.provenanceledger`
(`Provenance.cs:11-15`, `Lot.Origin`); `SavedActionBarBinding` (`SavedGame.cs:221-223`) in `aetheria.savedgame`;
the input layout rows and keys (`InputLayout.cs:29-50`) in `aetheria.inputlayout`. `Brush`
(`Environment.cs:73`) was not traced to a persisted document.
- **A2. `GameData/Aetheria.cc` carries no unknown arm.** Walker `py/store_unions.py` over the git-tracked game
database (12.7 MB, `cultcache.store.v1`, 238 records, 13 catalog entries): `Behaviors` arm keys
{0:3, 1:2, 2:10, 3:5, 4:2, 5:2, 6:3, 8:7, 9:23, 11:2, 16:4, 23:7, 24:5, 31:1, 32:1, 34:1, 35:2, 37:4, 38:10, 39:2,
40:1}; none retired or commented out; no `nil` element.
- **A3. The run save carries no unknown arm, and does carry missing trailing slots.** Walker `py/run_unions.py`
over `GameData/run.cc` (2026-09-30): `EntityPack` arms 0 and 1 (3 each), `ItemInstance` arm 2 (12),
`Provenance` arm 0 (64), `SavedActionBarBinding` 9 `nil` (empty bar slots). The `EntityPack` arms carry 21 slots
(orbital) and 20 (ship) against today's 22: the `Boss` slot (key 21, added 2026-10-07 in `db4617d8`) is missing,
so today's Aetheria reads this save only because a missing trailing slot defaults. `py/gaps.py`: no non-nil
retired slot (ItemInstance 2, 9, 10; EntityPack 5). `GameData/stale-2026-09-22/run.cc` is the same shape;
`player.cc` holds only `aetheria.playersettings`. `py/slots.py`: every top-level record matches its own store's
catalog exactly.
- **A4. What changes for Aetheria.** After it adopts a CultLib with cut 1, its current stores load unchanged
(A2, A3). A store written later by a build that has added an arm is refused by a build that lacks it, naming the
union and key, where today the arm silently becomes `null` (a gear item quietly loses a behaviour, a lot loses
its provenance) and the next save writes the loss back. Builds already pinned to 1.0.60 keep the null path; a
shipped binary cannot change. Aetheria-legacy does not use CultCache and has no store.
- **A5. Other C# stores.** `%LOCALAPPDATA%Low/GameCult/Aetheria/aetheria-unity.cc` (2026-08-08, 1.2 KB) is a
`cultcache.store.v2.directory-indexed` header from an older layout, not a union carrier.
## C. Rust, TypeScript and other consumers
Eyes audit for this pass (session `self-2026-10-10c-eureka`), `git grep` at each repo's `origin/HEAD`, Rust reach
by a lexical script (it can over-count; it does not under-count named fields). Aetheria and CultLib's tests excluded.
- **C1. Rust: no `DatabaseEntry` record crosses runtimes as a union, and none uses the reference arm shape.**
Records that reach serde enums, each keeping its bytes because the derive is opt-in:
Huginn `epiphany.pipeline.resolution.v2` (`ResolutionOutcome`, external tag, `eureka-pipeline/src/lib.rs:746`,
live on Yggdrasil); Idunn `idunn.deployment_transaction` and `idunn.admitted_generation` (v4 and legacy forms;
internally tagged `#[serde(tag = ..., deny_unknown_fields)]` enums in `control_plane.rs`, `deployment_plan.rs`,
and two `#[serde(untagged)]` enums kept so older transactions decode, `drivers.rs:259,387`; live at
`/var/lib/gamecult/idunn/control.cc`); CodexConnector `gamecult.codex.replay_record.v1` (`CodexReplayState`,
`lib.rs:1017`); Epiphany's Atlas, model-request, persona and receipt records (internal, adjacent, external and
untagged representations; `.cc` files under `F:\Projects\.epiphany-runtime\` and on Yggdrasil). Unit-only serde
enums (a bare string) are widespread in Idunn, Huginn (`Faculty`, the pipeline's unit enums), CodexConnector and
Epiphany. No enum reaches any record in Odin, Muninn, Ghostlight, Huginn `mind-body`, or cultnet-rs and
cultmesh-rs production records. None of these is read by another runtime as a union today, so none is affected
by the arm refusal; the derive changes nothing for them.
- **C2. TypeScript: no consumer encodes a tagged union.** The only hand formatters are StreamPixels
`apps/service/src/realtime/overlay-state-documents.ts:190,232,277` (positional arrays, no union). Bifrost,
Heimdall, Hermodr, Odin scripts, Stonks, Vili, VoidBot (vendored `cultcache-ts`) and weksa have none.
- **C3. C# outside Aetheria: StreamPixels' overlay declares unions; its tracked store holds none of them.**
`apps/overlay-unity/Assets/Scripts/Assembly-CSharp/` (`8d40541`): `ActivationCondition` (10 arms),
`BattleEffect` (2), `StatusEffect` (4), `ItemInstance` (3), `EquippableItemInstance` (2), reachable from
`streampixels.status`, `streampixels.action_item`, `streampixels.skill` and gear documents. Walker
`py/sp_catalog.py` over `apps/overlay-unity/Data/catalog.cc` (`cultcache.store.v1`, 20 records): only
`outfit`, `skin_color`, `hair_style`, `emote` and `hair_color` records, and no `[int, array]` pair at any depth.
Its only consumer resolver is `MathResolver` (`Catalog/CatalogAssembly.cs:6`). CultLib's own
`GameCult.Networking` `Message` union (`Messages.cs:10`) is a CultNet wire message, decoded with
`MessageSerialization.Options` (`MessageSerialization.cs:7`, `DefaultOptions`), outside `OptionsFor`: the arm
guard does not reach it (follow-up).
- **C4. Python and Kotlin: no union-shaped record.** repixelizer uses `define_database_entry_type` with flat
slots; `cultmesh-kotlin` documents are `data class`es with no sealed type.
So the stores the arm refusal can affect today are Aetheria's (A2-A4, none carrying an unknown arm) and
StreamPixels' overlay catalog (C3, carrying no arm). Every other runtime's persisted enums are serde shapes that
the opt-in derive leaves alone.
## D. Payload depth bound (body at `c028c511`)
One bound, 256 nested MessagePack arrays and maps, on read and on write (ruling
`cultlib-gaps:ruling:payload-depth-limit-256`; prior art `docs/research/payload-depth-limit.md`). The C# reference
landed in `ead5260c`, Python in `5618d8dd`, Rust in `c028c511`. TypeScript is not landed (spec
`cultlib-gaps:cut_spec:cut-union-bounds-ts.r2`, blocked on question `cultlib-gaps:question:ts-payload-opaque-type`).
| Runtime | Owner of the decision (constant and walker) | Doors routed through it |
|---|---|---|
| C# | `CultPayloadDepth.Check` (`src/GameCult.Caching.MessagePack/CultPayloadDepth.cs`), limit `CultMessagePackSecurity.MaximumPayloadDepth`. MessagePack's own graph counter is set to the same value as a backstop only. | Write: `CultDocumentMessagePackSerialization.SerializeUntyped`. Read: `DeserializeUntyped`. The cache store (`SerializePayload`/`DeserializePayload`), the directory store, `CultMesh` and single-file documents all call those two; C# has no separate raw-envelope or prepared-batch door. |
| Rust | `cultcache_rs::check_payload_depth` and `MAX_PAYLOAD_DEPTH` (`packages/cultcache-rs/src/lib.rs`). | Read: `decode_payload` (get, list, `validate_envelope`/`put_envelope`, `finish_prepared_entry`). Write: `put`/`prepare_entry`/`prepare_entry_named` reach it through `finish_prepared_entry`, which decodes what it just encoded. Raw envelope: `put_raw_envelope`, the door CultMesh network puts use (`cultmesh-rs` `lib.rs`, two call sites). Prepared batch: `put_prepared_batch` via `require_payload_depth`. cultnet-rs: every free-form or skippable wire payload decodes through `cultcache_rs::decode_payload` (message decode in `contracts.rs`, `provider_session`, `media_stream_wire`, `replication`, `service_identity`). |
| Python | `_refuse_past_depth_limit` and `MAX_PAYLOAD_DEPTH` (`packages/cultcache-py/src/cultcache_py/documents.py`); a msgpack `StackError` on read maps to the same error. | `DocumentDefinition.encode_payload` (write) and `decode_payload` (read). `CultCache.put`, `put_envelope`, `put_envelopes` and load-time hydration go through them; there is no raw-envelope door. cultmesh-py round trips call the same pair. |
- **Doors deliberately not routed.** cultnet-rs `idunn_lifecycle_brake`, `provider_health_contracts` and
`runtime_authority_contracts` decode records whose positional array length is checked exactly before decoding. A
fixed shape has no undeclared slot to skip and no free-form content, so the derive's depth is its shape (commit
`84bb1e28`). The five media records have no length check and are held by the test instead. C# `CultCacheMessagePack.CreateCodec`
serialises member values and deserialises member types without the document guard, by design (codec comment); only
the document type goes through the guard.
- **Forbidden writers.** No second walker and no second constant: a runtime's decoders, stores and wire layers call
the owner above, never re-count depth or restate 256. An added payload door routes through the owner or names
itself in the exact-length list.
- **Shared path.** `tests/vectors/union-parity/` (`bounds-outcomes.msgpack`, `depth-*.msgpack`; table in its
`README.md`), written by the C# reference and held by `tests/GameCult.Caching.Tests/UnionParityVectorTests.cs` and
`PayloadDepthTests.cs`, `packages/cultcache-rs/tests/union_parity.rs`, `packages/cultcache-py/tests/test_union_parity.py`.
- **Cut line.** `packages/cultnet-rs/tests/payload_depth.rs` and the `service_identity`/`cultmesh-rs` tests pin the
Rust wire doors. cultnet-py and cultmesh-py message and replication decoders call `msgpack.unpackb` directly and
are outside the Python cut; they hold only msgpack's own stack limit.
- **Verification layer.** Vector held by each runtime's own suite (limit 256 accepted, 257 refused, skipped content
counted, non-recursive walk); Rust wire doors by the cultnet-rs test above.
## Rationale
- **Why a guard formatter, not a fork or a re-implementation.** U5 shows a guard that only checks the key and the
subtype and delegates the encoding is enough. It keeps MessagePack-CSharp's union encoding as the one encoder,
costs one small file, and covers every path that already goes through `OptionsFor`. Forking the generated
template or vendoring `DynamicUnionResolver` would put a second encoder in CultLib.
- **Why the Rust derive is opt-in.** Serde enums already persisted by Rust consumers keep their bytes
(`docs/persisted-record-encoding.md`); a changed default would make every such store unreadable or
non-canonical. A new derive with explicit arm keys is how a Rust type says "this crosses runtimes".
- **Why arms use newtype variants over slot structs.** A C# arm is a subclass whose slots include the base class's
keys. A Rust newtype variant over a struct with keyed slots is the same thing, and it needs the slot code the
`DatabaseEntry` derive already has, factored so both derives share one slot generator.
- **Why the slot rule is a separate, asked cut.** The ruling settles arms and asks for extra trailing slots to be
"settled against the same re-encode rule". Two written contracts disagree (U10), and the current behaviour drops
data silently (U6), so the choice is the operator's and it should change every runtime at once.
- **The recommendation on extra slots was refusal; the ruling chose read-only, and S settles its depth.** An extra trailing slot is the slot-level twin of an unknown
arm: data from a newer writer that the reader's types do not describe. The operator ruled refusal for arms
knowing it costs an older reader the newer record; the same reasoning gives refusal for slots, and record
envelope slots are already refused in every runtime (U10). The alternative with precedent is
`variants:ruling:undeclared-read-only` (a record a type can read but does not declare loads read-only): it would
let an older reader see a newer record without overwriting it, but only where a catalog shows the extra slot
(top level), while most evolution here is nested (A3's `Boss` slot sits inside a union arm).
- **Missing trailing slots stay tolerated.** The reader knows a missing slot and its default, so it carries no
bytes it does not understand, and Aetheria's live run save depends on it (A3). A store whose guard compares
re-encoded bytes (Idunn's) refuses such a record by its own stricter rule, which is that guard's purpose.

827
docs/cultlib-gaps-map.md Normal file
View file

@ -0,0 +1,827 @@
# CultLib Gaps: Map
Campaign `cultlib-gaps` (target: `docs/cultlib-gaps-target.md`). This file holds body facts, probes and
rationale for the cuts the mind's `cut_spec`s name; the specs cite it by section and fact number.
Pinned base: `origin/main` at `6bdc23c9` (2026-10-10). Imagination pass `imagination-cl-gaps-cultmath`, session
`self-2026-10-10-ag`. Probes live under the session scratchpad, `imagination-cl-gaps-cultmath/` (`probe1` traced
copy of the solver and the warm/cold sweep, `suite` a scratch copy of `packages/cultmath` with the candidate fix,
`probe2` the sweep against that copy, `probe3` the overload probe). None of it is committed.
## Cut order
1. `bls-release-stop`: BoundedLeastSquares stops on a release only when the release is not real.
2. `bls-input-field`: a validation helper names the invalid input (follow-up `bls-invalid-input-field`). Blocked on
question `cultlib-gaps:question:bls-input-error-shape`.
3. `cultmath-release-0-4-1`: release `org.gamecult.cultmath` with cuts 1 and 2. Version follows question
`cultlib-gaps:question:bls-fix-release-lane`.
4. `cultmath-rotation-algebra`: quaternion algebra, swing-twist and joint clamps (ruling
`aetheria-release:ruling:solver-in-gamecult-animation`). Ships in the CultMath release that the first
GameCult.Animation release pins; that release is mapped with GameCult.Animation.
5. `py-msgpack-default` revision 4 (section P, "Revision 4"): the fix batch (h4) on `hands/py-msgpack-default`
at `a59bbc7d`. `migrate_payloads` is replaced by `convert` (ruling `py-migration-shape-ruled`), and Soul s3's
open codec Lows are fixed. Its merge to main waits on the Brokkr cut (follow-up `brokkr-msgpack-payloads-r4`).
5a. Not mapped: the Python store takes the cross-process sidecar lock the C# and Rust stores take (follow-up
`py-ts-store-cross-process-lock`). It is a store parity cut of its own; `convert` does not need it (P15).
6. Not mapped: the C# leaf-vectors revision (`leaf-vectors-next`) gains the P2 map-key rows, so the Python
key tests read C#-written bytes instead of literals.
7. `decode-stack-safety` (section D): cultcache-rs's one decode door grows its stack at every container level
(ruling `decode-stack-mechanism-ruled`). Independent of 5 and 6; thing2's contract-rs merge waits on it.
8. `expect-persisted-schema` (section E): an observed entry keeps the schema id it was persisted under and
conditional commit compares that (ruling `aetheria-release:ruling:expect-fix-in-cultcache`). C# only. The
release that carries it to Aetheria follows question `cultlib-gaps:question:expect-fix-release-lane`; the
Aetheria pin bump is an Aetheria follow-on, not this cut.
9. `aot-record-ref-generator` (section A, AOT cut 1): a consumer with MessagePack's source generator on can declare
`CultRecordRef<T>` members and dictionary keys. Base: the C2a branch head `a768b821`; Hands starts after C2a merges
to main or rebases onto it. Serves `record-ref-breaks-msgpack-generator`, the first cut of
`cs-aot-safe-registration` (operator 2026-10-27: "I'm not a fan of requiring reflection. ... AoT is a nice thing to
support anyway.").
10. Not mapped (AOT cut 2): the generator reaches consumers that reference only `GameCult.Caching.MessagePack` (today its
analyzer is `PrivateAssets="all"`, so they run every document on Reflection.Emit).
11. Not mapped (AOT cut 3): registration descriptors are generated instead of reflected (`BuildDescriptor`,
`DiscoverMembers`, `CultDocumentResolver`, `CultMessagePackSecurity`), and C2a's element-id walk stops reflecting
(A6), so a NativeAOT consumer round-trips a ref-keyed dictionary.
## A. AOT cut 1: CultRecordRef under MessagePack's source generator (cut `aot-record-ref-generator`)
Imagination pass `imagination-aot-cut1`, session `self-2026-10-27-eureka`, pinned to `origin/hands/variants-c2a` at
`a768b821` (C2a's head; merge base with main `02cf04b5`). Starting evidence: `research/cultmesh-garden-path/cultcache-aot-probe-2026-10-27.md`
(main `eb6a8f2c`). Probes: the scratch console from that note, copied to the session scratchpad
`imagination-aot-cut1/aot-probe/` and committed only as dangling commits on top of `a768b821` (`a0f06b2c`, `6d382961`,
`dc6cf9dc`, `99cae039`, `07b89982`), run on Yggdrasil in `mcr.microsoft.com/dotnet/sdk:10.0` (MessagePack 3.1.7, .NET
10.0.12). Logs `job1.log` to `job5.log` beside the probe. Variants: G = `CultRecordRef<T>` member only, H = ref-keyed
`Dictionary<CultRecordRef<T>, float>` only, B = both. Every variant references `MessagePack` directly (generator on).
### Body facts
- A1. The generator does not discover `CultRecordRefFormatter<T>`. For a generic type it does not know, it collects
the type's definition as an object and names the formatter it would generate, `<Type>Formatter<T>`, nested in
`GeneratedMessagePackResolver`. For a type from another assembly it then generates nothing, so the resolver names
`GeneratedMessagePackResolver.GameCult.Caching.CultRecordRefFormatter<T>`, which does not exist (CS0426). The match
with our formatter's name is a coincidence. Evidence (job1): `[ExcludeFormatterFromSourceGeneratedResolver]` on the
formatter, and an `internal` formatter, both still fail G, H and B with the same line. Upstream issue
MessagePack-CSharp#2133 (open, no maintainer reply) reports the same failure for any open-generic type with a
formatter in another package.
- A2. Consumer MessagePack 3.1.12, the newest stable, fails G, H and B the same way (job1). Upgrading does not fix it.
- A3. `[MessagePackFormatter(typeof(CultRecordRefFormatter<>))]` on the `CultRecordRef<T>` struct stops the
generator naming a formatter for it (the mangled line is gone), but the generator then reports `MsgPack006` (error)
in every consumer: it checks `typeof(X<>)`'s interfaces, which an unbound generic does not list. Adding the
non-generic `IMessagePackFormatter` marker to the formatter does not help (job2 `ta2`). With `MsgPack006`
suppressed by the consumer, G, H and B build and run (job2). Rejected: it needs the formatter moved into
`GameCult.Caching`, which then needs the whole `MessagePack` package instead of `MessagePack.Annotations`, and every
generator consumer still has to suppress an error.
- A4. `[assembly: MessagePackAssumedFormattable(typeof(CultRecordRef<>))]` in the consumer assembly fixes G, H and B
with no library change (job2 `base@ASSUMED`). The generator stops collecting `CultRecordRef<T>`; the generated
document formatter asks the resolver, and `CultDocumentResolver` serves the ref. This is the attribute MessagePack
documents for "a formatter added through a custom resolver at runtime". The same attribute as an MSBuild
`AssemblyAttribute` item in the consumer project (`_Parameter1_IsLiteral`), which is what a package's
`buildTransitive` targets injects, does the same (job3 MG, MH, MB). The attribute declared in the library assembly
does not reach the consumer's generator (job4 LIBATTR: CS0426).
- A5. Generated and dynamic formatters write the same bytes. RefDoc (B) payload is `93A172A17081A170CA3F000000` from
the Reflection.Emit fallback with no generator (job3 BASE) and from the generated formatter (job3 MB). The document
formatter is `GeneratedMessagePackResolver+Probe+RefDocFormatter` (`dynamic=False`); the ref formatter is the
library's `CultRecordRefFormatter<PlainDoc>` through `CultDocumentResolver`.
- A6. NativeAOT (`PublishAot`, probe assembly rooted, linux-x64): G round-trips (payload, upsert, flush, reopen) with
payload `92A172A170`, identical to JIT (job4 G0). B fails, in C2a code, not in the generator path: upsert fails in
`CultElementIds.MapEntries`, whose `MakeGenericMethod(key, value)` has no native code for a struct key (job4 B0). With
that walk reading the map through non-generic `IDictionary` instead, upsert passes under AOT and JIT is unchanged
(job5 B1), but the payload and flush still throw `TargetInvocationException` from a second reflective call on the
serialize path. By source read the candidate is `ResolverClaim`'s `GetFormatterMethod.MakeGenericMethod(type)`
(`CultDocumentMessagePackSerialization.cs:84`), not proven. Both are runtime reflection over element-id paths, which is
AOT cut 3's ground, so cut 1 proves AOT on the member shape and leaves the key shape's AOT round trip to cut 3.
- A7. `GameCult.Geometry` already runs the generator (it references `MessagePack` directly) and carries the per-member
workaround on three members (`CultGeometryDocuments.cs:164,239,328`). Under A4 those attributes are redundant.
### Shape the cut lands, and why
`GameCult.Caching.MessagePack` ships `buildTransitive/GameCult.Caching.MessagePack.targets`, which adds the A4 assembly
attribute to every project that consumes the package. NuGet consumers need no line of their own, which is what a
consumer expects from a library whose type breaks the generator. Unity consumers and in-repo `ProjectReference`
consumers do not get `buildTransitive`: Unity consumers write the one attribute line (README), and in-repo consumers
import the targets file, so they exercise the file that ships. The attribute type comes from `MessagePack.Annotations`,
which `GameCult.Caching` already brings, so the injection compiles in a consumer with or without the generator. Wire
bytes do not change: no formatter changes, only which code path asks for it (A5).
Rejected: a type-level formatter attribute (A3); excluding or hiding the formatter (A1); upgrading MessagePack (A2);
declaring the attribute in the library (A4); keeping per-member attributes, which cannot reach dictionary keys.
## B. BoundedLeastSquares stopping
Finding `aetheria-release:finding:cut-thrust-allocator-core.s1.solver-converged-early` (Soul, Medium): a cold start
reports `Converged` at a non-optimal point, so Aetheria's thrust allocator (tolerance 1e-12) answers differently
warm and cold, by up to .078 throttle.
### Body facts
- **B1. One runtime ships the solver.** `BoundedLeastSquares` exists only in
`packages/cultmath/src/CultMath/BoundedLeastSquares.cs` (C#, netstandard2.1 and net8.0). The type doc says there is
no HLSL mirror; grep over `packages/*` finds no Rust, TypeScript, Python or Kotlin sibling, and no other CultLib
project calls it. Its consumer outside CultLib is Aetheria's `ThrustAllocator`.
- **B2. The defect reproduces against CultMath at base, outside Aetheria.** `probe1` rebuilds Soul's allocator
matrix (Soul's `ProbeAllocator2`, constants H=100, Wy=1000, W=.3, kappa=10, rho=.1, EMax=2) for hull
`Synthetic(8, 7005)` and the 8th intent of `System.Random(5)`, (-0.8505, 0.6739, 0.5587): 15 rows, 12 columns.
Real `BoundedLeastSquares.Solve` at tolerance 1e-12 from x = 0: `Converged`, 13 iterations, cost 36.0313737,
x7 = 1. Re-solving from that answer: 1 iteration, cost 36.0312608, x7 = 0.9222145, which is also the warm answer.
The same literals (dumped with `"R"` floats, `probe1 dump`) reproduce it at tolerance 1e-9 and 1e-12, and not at
the default 1e-6, where the cold solve stops at iteration 10 inside the tolerance and the re-solve takes 0
iterations.
- **B2a. The W5 problem as literals** (`probe1 dump`, float `"R"` round trip; m = 15, n = 12, row-major; the
regression fixture for `bls-release-stop`). Nonzero entries of A:
- row 0: a[0,2] = 100f, a[0,8] = -100f, a[0,9] = 100f
- row 1: 13.882518f, 20.86695f, 0, 14.8965845f, 12.38046f, 22.115599f, -13.706654f, 15.85789f, 0, 0, -100f, 100f
- row 2: -187.72859f, 239.31168f, 158.87828f, -270.31165f, 235.38467f, -14.704258f, 121.61232f, 244.81305f, 0, 0,
0, 0
- rows 3-6: a[3+k, 8+k] = 0.3f (k = 0..3); rows 7-14: a[7+i, i] = 0.1f (i = 0..7)
- b = {0, 67.385445f, 558.70917f, -3f, -3f, -3f, -3f, 0 x 8}; lo = 0; hi = 1f for columns 0-7, 2f for 8-11.
At base, tolerance 1e-12 from x = 0: `Converged`, 13 iterations, x[7] = 1, cost 36.031373704487095; a re-solve
from that answer: 1 iteration, x[7] = 0.92221445f, cost 36.031260771352414.
- **B3. The stopping rule that fires is the progress rule, after a release** (`BoundedLeastSquares.cs:273-278`). A
traced copy of the solver, bit-equal to the real one on this problem, shows the last two passes:
- iteration 12: full step lands; column 2 sits at its lower bound with gradient -0.00205 (violation 2.05e-3,
tolerance 1.5e-7); the solver releases it and records fMark = f.
- iteration 13: the re-solve moves x2 from 0 to 2.05e-7. The cost falls by about 2.1e-10, and the stall band
`1e-15 * fScale` is 2.11e-10, so `f >= fMark - band` holds and the solver reports `Converged`. Column 7 is then at
its upper bound with gradient +0.00145, ten thousand times the tolerance; releasing it is worth 1.1e-4 of cost.
- **B4. Why the progress rule cannot judge a release.** Near a minimiser, a cost difference resolves only about the
square root of what a gradient comparison resolves: a real step of size dx against gradient g lowers the cost by
about g*dx, while the cost itself is a sum of terms of size fScale. Here g*dx (2e-10) sat exactly at the cost's
rounding band. The gradient comparison is far from its own rounding: for column 7, the bound on the double
gradient's rounding error, `(n+1) * eps * (|A^T b|_7 + sum_k |(A^T A)_7k x_k|)` (Higham's inner-product bound,
gamma_(n+1)), is 9.3e-10, and the violation is 1.56e6 times that.
- **B5. What the progress rule is for** (commit 43035bc8, and the tests at `BoundedLeastSquaresTests.cs:774-942`).
It stops two kinds of pass that can never reach the tolerance: a free-set re-solve whose residual is rounding on a
near-singular free set, and a release whose violation is itself rounding (kktTol at or below the noise, including
kktTol = 0 when the box point nearest the origin is stationary). Deleting it outright breaks both: with a noise
gate on every test and no progress rule, 8 of 66 BLS tests fail (`wide`, `integer-tie`, `collinear`, `cond` spin to
the cap; `NearSingularFreeSetsConfirmOnceAndStop` sees 0 confirmations). Exempting releases from the progress rule
with no noise gate fails one: `StalledPassesConvergeInsteadOfSpinningToTheCap("plain")` seed 141, b = 0,
kktTol = 0, cycles release 3 (violation 1.7e-18) / stationarity miss / block on 3 until the cap.
- **B6. The candidate fix, measured.** In a scratch copy of the package (`suite`, diff `suite/clean.diff`, 44 lines):
(a) a bound column is a release candidate only if its violation exceeds both kktTol and the rounding bound of B4;
(b) the progress rule judges only passes that release nothing; a pass that releases sets fMark to +Inf, so a
release is never measured against a cost recorded before it. Results:
- all 66 tests in `BoundedLeastSquaresTests` pass, including `DefaultSolvesAreBitIdenticalToTheirGoldenBits` and
every stall and noise-floor test; the same holds with the bound's factor at 1, 16 and 256 in place of n+1;
- the W5 problem: cold `Converged` in 14 iterations at the warm answer (x7 = 0.92221, cost 36.03126077);
- 400 synthetic hulls x 60 random intents (24,000 calls, warm and cold) at tolerance 1e-12: every call `Converged`,
max iterations 45, warm and cold agree within 6e-8 on every call (the original: one call off by .078 in the first
2,400). At the default tolerance warm and cold still differ, by more than .1 in 10,273 of 24,000 calls: that is
the documented resolution of 1e-6 times the gradient scale, pinned by
`AnAllocatorShapedProblemIsStartIndependentAtATightTolerance` (spread >= 0.5), and not this cut's subject.
- **B7. Released columns the factor drops.** With the fix, 266 releases in the suite land on a column that
`SolveFreeSet` marks dependent (p = 0). The next pass is then a stationarity miss with no release, and the
progress rule stops it, as before. No release in the suite or the 24,000 allocator calls produced a step that moves
the released column outward. A Lawson-Hanson style rejection guard (re-bind the column, try the next candidate)
was prototyped and is not needed by any measured case; it is not in the cut.
### Prior art, and which one the solver follows
- The solver is a primal active-set method on the normal equations: solve the free set, step to the first bound
crossed, release the most violated bound only after a full step lands. That is the outer/inner loop of
Lawson-Hanson NNLS (1974) generalised to two-sided bounds, which is the shape of Stark-Parker BVLS (1995) and of
scipy's `lsq_linear(method='bvls')`. It differs in factorisation (Cholesky of `A^T A`, not QR of `A`) and in
stopping.
- Lawson-Hanson stop on the dual test only (netlib `NNLS`, label 350: `IF (WMAX .le. ZERO)`), and guard roundoff
per candidate: a column whose new norm is negligible against rounding (`DIFF(UNORM+...)`) or whose new
coefficient is not positive (`ZTEST .gt. ZERO`) is rejected with `W(J)=ZERO` and the next candidate tried. There is
no cost-based stop.
- scipy's BVLS (`scipy/optimize/_lsq/bvls.py`, main) measures KKT exactly as CultMath's status doc states
(`g * on_bound` for bound variables, `|g|` for free ones, max over components) and has two stops: optimality below
`tol` (status 1) and a relative cost change below `tol` (status 2), reported as different statuses.
- CultMath reported its cost stop as `Converged`, which its status doc defines as "the KKT conditions hold". The fix
follows Lawson-Hanson: a release is judged on the gradient against its rounding (the role of their `DIFF` test),
and the cost stop is kept only where no gradient test can decide (a free-set residual at rounding level), which is
where scipy's status 2 also lives.
### Rejected alternatives
- Remove the progress rule and stop on the gradient alone: B5, 8 failures.
- Report the cost stop as a new status (scipy's status 2): the W5 answer would still be wrong, just labelled, and a
new enum member is a breaking change for callers that switch on the status.
- Measure progress from the step (the exact model decrease `t(1-t/2)|L^-1 g|^2`) instead of differencing the cost:
exact, but a noise-level gradient still yields a tiny positive decrease, so it needs the same noise threshold, on a
quantity further from the KKT test than the gradient itself.
## I. Naming the invalid input
Follow-up `cultlib-gaps:follow_up:bls-invalid-input-field` (from Soul finding
`cut-bls-kkt-tolerance.s1.invalid-input-names-no-field`): every validation failure returns the same `InvalidInput`.
- **I1.** Validation today is three sites in `Solve`: tolerance (`:147-148`), bounds (`:149-151`), and finiteness of
A and b folded into the `A^T b` build (`:153-161`), which cannot tell A from b.
- **I2.** Precedent: LAPACK reports a bad argument as `INFO = -i`, naming the argument and never its value. .NET
throws `ArgumentException` with a parameter name, which the solver's no-throw, zero-allocation contract rules out.
- Recommended shape (question `bls-input-error-shape`, option `validate-helper`): a public
`BoundedLeastSquares.Validate(m, n, a, b, lo, hi, kktRelativeTolerance)` returning a new enum
`BoundedLeastSquaresInput { Valid, InvalidTolerance, InvalidBounds, NonFiniteMatrix, NonFiniteTarget }`, the first
failure in that order. `Solve` calls it and returns `InvalidInput` for anything but `Valid`, so validation has
one owner and the folded finiteness check in the `A^T b` loop is deleted. Additive: `InvalidInput` and every
existing result stay.
## R. Release 0.4.1
- **R1.** `org.gamecult.cultmath` is at 0.4.0 (`package.json:4`, `CultMath.csproj:13`, tag
`cultmath-unity-v0.4.0`). `scripts/check-changelog-semver.mjs:97-101`: in 0.y.z a minor bump is the breaking lane;
additions and fixes take the patch lane (0.2.4 added `erf` as a patch).
- **R2.** Cut 1 changes results only where the old solver stopped after a release whose violation was above both the
caller's tolerance and the gradient's rounding, that is, where it reported `Converged` in breach of its status doc.
The default path's golden bits are unchanged (B6). The 0.4.0 precedent (ruling `cultmath-0-4-0-breaking-r2`)
declared the snoise tie fix Breaking because two valid functions gave different values; here the old value broke
the documented contract. Question `bls-fix-release-lane` asks which lane; the recommendation is `fixed-patch`
(0.4.1, `### Fixed`).
- The release procedure is the 0.4.0 cut's (`cultlib-gaps:cut_spec:cut-cultmath-release.r1`): Windows-only DLL rebuild
on Starfire, tests on Yggdrasil, public-surface diff, Unity import smoke, Soul before the tag.
## Q. Rotation algebra
Ruling `aetheria-release:ruling:solver-in-gamecult-animation`: CultMath gains quaternion algebra, swing-twist and
joint clamps; the aim-chain solve lives in a new package GameCult.Animation. The interface the consumer needs is the
Aetheria map's section "The rig's interface to GameCult.Animation and CultMath" (Aetheria `origin/master`
`30d02719`, `docs/aetheria-release-map.md:7559-7601`).
### Body facts
- **Q1. What exists.** `quaternion` (`src/CultMath/quaternion.cs`, 95 lines): public float fields x, y, z, w;
`identity`; `LookRotation(float3 forward, float3 up)`; implicit conversions to and from `float4`; scalar `==`.
`math.normalize(quaternion)` (`math.cs:225-235`) returns identity below length 1e-20. No mul, rotate, conjugate,
inverse, axis-angle, slerp, swing-twist or clamp exists. `math` is a partial class (`math.cs`,
`math.Interval.cs`, `math.Affine.cs`, `math.Phacelle.cs`); `math.rotate(float2, float)` exists (`math.cs:240`).
- **Q2. Serialization.** `GameCult.Caching.MessagePack` serialises `quaternion` as the float array [x, y, z, w]
(`CultMathResolver.cs:170-171`) and as an xyzw object in JSON (`CultMathJson.cs:468`). No other runtime
(`cultcache-rs`, `-ts`, `-py`) has a quaternion type. The cut adds static functions only: no field, layout or
formatter changes, so no wire change and no cross-runtime fixture.
- **Q3. Runtimes.** The algebra is C#-only, like `LookRotation`: shaders carry rotations as `float4` (the type's doc),
the mirror tests map shader functions to C# and never the reverse (`HlslSourceCompatibilityTests.cs:114-117`), and
the native core (`native/cultmath-core`) is a Voronoi tone helper. The Unity bridge already converts `quaternion`
(`UnityConversions.cs:16,23`); it needs no change.
- **Q4. An overload trap, probed.** `quaternion` converts implicitly from `float4`, and CultMath has no
`mul(float4, float4)`. `probe3` declares `mul(quaternion, quaternion)` beside scalar and matrix `mul` overloads and
calls it with two `float4`: it compiles and returns the quaternion product. HLSL's `mul(vector, vector)` is the
inner product, and Unity.Mathematics 1.3.3 (the copy in Aetheria's PackageCache) defines `mul(float4 a, float4 b)`
as such (`matrix.gen.cs:129`), so the same call there is a dot product. The cut therefore adds HLSL's vector
`mul(float2|3|4, float2|3|4)` (dot) beside the quaternion `mul`, so a `float4` pair keeps its shader meaning.
- **Q5. Unity.Mathematics as reference, not template.** Its signatures for `mul`, `rotate`, `conjugate`, `inverse`,
`AxisAngle` and `slerp` (`quaternion.cs:99,493,502,630,651,678`) are the consumer's stated convention ("`mul(a, b)`
applies `b` first"). CultMath's design rule is to state semantics in code and not copy Unity.Mathematics
(`docs/design.md:465-469`), so the formulas are written from the definitions below.
### Semantics the cut lands
Quaternions are Hamilton, stored xyzw, unit for rotation; angles are radians, right-handed about the axis.
- `math.mul(quaternion a, quaternion b)`: the Hamilton product; `rotate(mul(a, b), v) == rotate(a, rotate(b, v))`.
- `math.rotate(quaternion q, float3 v)`: `t = 2 cross(q.xyz, v); v + q.w t + cross(q.xyz, t)`; q assumed unit.
- `math.conjugate(q)`: (-x, -y, -z, w). `math.inverse(q)`: `conjugate(q) / dot(q, q)`, no guard (zero in, non-finite
out, as HLSL division).
- `quaternion.AxisAngle(float3 unitAxis, float radians)`: (axis sin(r/2), cos(r/2)); the axis is not normalised.
- `math.slerp(a, b, t)`: shortest arc (negate b when `dot(a, b) < 0`); normalised lerp when the quaternions are
nearly parallel; t is not clamped.
- `math.swing_twist(q, unitTwistAxis, out swing, out twist)`: twist is q's projection onto the axis, normalised
(identity when that projection vanishes, a half-turn swing); `swing = mul(q, conjugate(twist))`, so
`q == mul(swing, twist)` (twist applied first).
- `math.clamp_twist(twist, unitAxis, min, max)`: angle `2 atan2(dot(twist.xyz, axis), twist.w)` wrapped to (-pi, pi];
inside [min, max] the input is returned unchanged, otherwise `AxisAngle(axis, clamp(angle, min, max))`.
- `math.clamp_hinge(q, unitAxis, min, max)`: `clamp_twist` of q's twist about the axis; the swing is discarded.
- `math.clamp_cone(swing, unitAxis, maxAngle)`: the angle between `rotate(swing, axis)` and `axis`; inside the cone
the input is returned unchanged, otherwise the rotation by `maxAngle` about `normalize(cross(axis,
rotate(swing, axis)))` (at a half turn, where that cross product vanishes, about the swing's own axis).
- Preconditions (`min <= max`, unit axes) are documented, not checked, as everywhere in `math`.
### What GameCult.Animation will need from this cut (not mapped here)
- From cut 4: `mul`, `rotate`, `AxisAngle` for `AimChain.Pose` and `EffectorDirection`; `conjugate` or `inverse` for
frame changes; `swing_twist` and the clamps if its joints ever carry cone or twist limits (the presenter's per-axis
boxes do not).
- From cut 1: start-independent `BoundedLeastSquares` at a tight tolerance. `AimChainSolver.Solve` is warm-started
from `current`; without cut 1 its answer would depend on the previous frame exactly as the allocator's did.
- A CultMath release that carries cut 4, which `org.gamecult.animation`'s `package.json` pins. That release is
mapped with GameCult.Animation, so one Soul-verified release pair ships both.
## P. cultcache-py payload format (py-msgpack-default, revisions 2 to 4)
Imagination pass `imagination-py-msgpack-r2`, session `self-2026-10-12-eureka`, against `hands/py-msgpack-default`
at `7f2c7477`. Probes ran from the session scratchpad and were removed afterwards; their results are recorded here (`jobK` ran
the C# reference on Yggdrasil at `7f2c7477`; `p1`-`p3` ran Python 3.13.15 with msgpack 1.2.2 on Starfire). Revision 1's facts
are in `docs/research/py-msgpack-default/README.md`, and Soul's are in verdict `cut-py-msgpack-default.s1`.
### Body facts
P1. **What decides a payload's format at 7f2c7477.** A search of every `packb`, `unpackb`, `json.loads` and
`json.dumps` call in cultcache-py, cultnet-py and cultmesh-py found five places:
- (a) `PayloadCodec.encode`/`decode`, through `DocumentDefinition.encode_payload`/`decode_payload`.
- (b) `_check_payload_tree` (documents.py:75), which holds the map-key rule. `MESSAGEPACK_PAYLOAD_CODEC.encode`
does not apply that rule, so a direct call writes `{1: 'a'}` and its own `decode` refuses the bytes.
- (c) `stores.py:217`, `_infer_schema_version_from_payload`. A v1 record whose schema id is missing from the
catalog is read by unpacking its payload as MessagePack to find a schema stamp.
- (d) `replication.py:186`, the same stamp reader copied into cultnet-py behind a lazy `import msgpack`.
- (e) `stores.py:257`. A legacy envelope whose payload is an inline map or array is repacked as MessagePack.
The other JSON sites are wire messages, schema JSON, CLI output and the cultmesh ready file. None of them is a
document payload.
P2. **The C# reference's object slot, map keys** (`jobK`, `NumericLeafVectorTests.AnySlot` under
`CultDocumentMessagePackSerialization.OptionsFor`, MessagePack `UntrustedData` security):
- Accepted: str, int at every width (negative fixint, u64 max, i64 min), float32, float64, bool, and timestamp
ext. The reference rewrites each one at its own width, for example `9181ff01` becomes `9181d0ffcc01`.
- Refused: nil (inner `ArgumentNullException`); bin, array and map keys (inner `TypeAccessException`, because
UntrustedData has no collision-resistant comparer for them).
- Refused as duplicates (inner `ArgumentException`): `{1:1,1:2}`, `{"a":1,"a":2}`, and `{01:1, cc01:2}`, which
is the same value written at two widths.
- Accepted as two keys: `{1:1, 1.0:2}` and `{1:1, true:2}`. The CLR types differ, so the keys are distinct.
- An `object` value of u64 max or i64 min is accepted and rewritten byte-identical.
P3. **msgpack-python with `strict_map_key=False`** (`p2`, `p3`). Every key kind decodes: nil becomes None, bin
becomes bytes, and a timestamp ext becomes `msgpack.Timestamp`. An array or map key raises TypeError
(unhashable) without a hook. Duplicates are silently last-wins: `{1:1,1:2}`, `{"a":1,"a":2}`, `{01, cc01}`,
`{1, 1.0}` and `{1, true}` each decode to a single key. An `object_pairs_hook` sees every key kind, lists and
dicts included. An exception it raises propagates with `__context__` None. On 100k small maps the hook costs
2.2x the decode time (0.087 s becomes 0.200 s). `packb` raises an untyped `OverflowError` for 2**64 and
-(2**63)-1.
P4. **Hash flooding with non-str keys** (`p1`). This is msgpack-python's reason for `strict_map_key=True`.
Within the 64-bit MessagePack integer range, at most 13 integers share one CPython hash. The full-hash collision
attack on int keys therefore needs unbounded integers, and MessagePack cannot carry them. Keys crafted to share
low hash bits (`k << 43`) took 0.44 s for 200k keys and 1.34 s for 400k. 200k str keys take 0.25 s. The cost is
mildly superlinear, not quadratic. The reference defends itself with UntrustedData's collision-resistant
comparers. Python's dict has no such comparer, and the measured bound makes one unnecessary.
P5. **Schema-stamp recovery.** The C# store deleted its stamp recovery in `a3cc3942` (2026-09-13), so a persisted
schema id that is absent from the embedded catalog is refused ("Persisted schema ... is not present in the
embedded catalog", `CultCache.cs:504-507`). The C# CultNet registry keeps payload-stamp routing for raw wire
records (`CultNetDocumentRegistry.ResolveDescriptorForRawDocument`, `TryResolveDescriptorByPayloadSchema`).
cultcache-py's store copy (P1c) is therefore a divergence from the reference. cultnet-py's copy (P1d) mirrors
the reference. The TS store (`single-file-messagepack-backing-store.ts:336`) also keeps the store copy.
P6. **Legacy envelope arrays.** The C# reference does not read them. TS (`normalizePayload`, line 490) and Python
both repack an inline payload with MessagePack `encode`. A legacy inline payload is a subtree of a MessagePack
document, so it is MessagePack by construction. No JSON-payload writer ever produced one: the 0.3 JSON default
wrote bytes, which land as bin.
P7. **Brokkr** (read-only, `f97d4dc`). The bridge imports cultcache-py from `F:/Projects/CultLib/packages`
(`blender_target.py:19`) and defines 8 types with `define_document_type` (630-637). Merging to that checkout's
main is therefore the switch itself: Soul s1 measured that the next `_open_node` pull refuses the whole Blender
store. The daemon writes JSON into the Blender store and the Unity store with `put_json_document`
(`main.rs:1424`, called at 790-1392 and in tests from 2710). It also has `put_messagepack_document` (1435) and
reads JSON first, then MessagePack (`decode_payload`, 1447). The daemon is Rust and cannot call cultcache-py.
### Authority, and why
- **Payload format.** The definition's `PayloadCodec` owns its bytes: the encoding, and which maps and integers
the format can carry. On read, `MESSAGEPACK_PAYLOAD_CODEC` refuses key kinds and duplicates through its
`object_pairs_hook`; that is the only place they are visible, because a dict cannot show a collapsed duplicate
afterwards. On write, its encode walks the tree first, so a direct `codec.encode` call cannot write bytes
its own `decode` refuses (Soul's single path).
- **What the definition owns.** `DocumentDefinition.encode_payload`/`decode_payload` own the cross-codec rules
(depth 256) and the translation of codec faults into the typed refusals, naming the type. `PayloadCodec.map_keys`
is deleted, and `_check_payload_tree` goes back to checking depth only.
- **The key rule is P2's.** It is not `strict_map_key`'s. Duplicates are refused as the reference refuses them.
- **Residual.** Keys Python holds as one but the reference holds as two (`1` and `1.0`, `1` and `true`) are
refused, because a dict cannot hold both. The refusal is typed (`duplicate_map_key`) and pinned so a change is
seen. Matching the reference here would need a decoded map type that is not a dict. No consumer is known to
write such maps.
- **Prior art for refusing duplicates** (from knowledge, not fetched this pass): CBOR's RFC 8949 section 5.6
expects decoders to reject duplicate keys; I-JSON (RFC 7493) requires the same of JSON; MessagePack-CSharp's
`UntrustedData` refuses duplicates and keys it cannot hash safely (P2). msgpack-python's `strict_map_key` exists
for hash flooding, which P4 bounds.
- **Integer range.** Beyond 64 bits is a format limit, so the MessagePack codec's write walk refuses it with the
code `integer_out_of_range`. The value is never named, and neither is a map key on its path, because in a
free-form tree a key is payload data. Entry slots keep their leaf refusals, which name the slot and the field.
A bigint was never wire-parity data: MessagePack has no integer wider than 64 bits (P3), and the 0.3 JSON
payloads that could hold one were unreadable by the reference (`py-default-payload-json`).
- **The stamp readers.** P1c is deleted and its record is refused as the reference refuses it (P5). P1d stays as
cultnet-py's raw-record router, mirroring C#. It reads a stamp and decides no format: the resolved definition's
codec decodes the payload or refuses it. It loses its lazy import.
- **Legacy envelopes.** P1e is demoted and stays (P6). It extracts the MessagePack bytes the inline subtree
already is. A definition whose codec is not MessagePack refuses the record like any other foreign record.
- **The default codec.** The `DocumentDefinition` field is its only author. `define_document_type`'s parameter
defaults to None and forwards only a codec it was given.
- **Refusal text.** The JSON remedy sentence is dropped from the read refusal: it was false for every entry
definition. Each refusal carries a `code` (`not_one_value`, `map_key_kind`, `duplicate_map_key`,
`integer_out_of_range`) and names the kind or the fault class. It is raised after the `except` block, so
`__context__` holds no decoder exception and no payload.
### Migration: what CultLib owns
Revision 1's README procedure (read under JSON, then put each value) was per record and not atomic. Soul s1
stranded a store with it. Revision 2 replaces the procedure with
`migrate_payloads(store, definitions, *, source_codec=JSON_PAYLOAD_CODEC) -> int` in cultcache-py:
1. It pulls every envelope from the store.
2. For each record whose type is among `definitions`, it decodes under that definition with `source_codec` and
encodes under the definition itself. That is the whole definition path, validation and every refusal included.
3. Any refusal stops the migration before the store is touched. The error names the type, the record key and
the refusal's code, never the value.
4. Otherwise one `push_all` writes the store whole. `SingleFileMessagePackBackingStore` and
`JsonLinesBackingStore` each do that as a temp file and a rename.
5. Records of other types keep their bytes and `stored_at`. The return value is the count rewritten.
It never sniffs, and it cannot leave a store half-migrated. Revision 2 also claimed that a second run is refused;
Soul s2 falsified that (P10), and a concurrent writer strands the store (P11). Revision 3 replaced this shape with a
backup-and-swap (P12), which Soul s3 broke three more ways. Revision 4 deletes the in-place migration: CultLib owns
only `convert`, which writes a new file and never writes its source (P14).
Rejected alternatives:
- A sniffing reader that accepts both formats: P1's one-digit payloads are valid in both formats, and r1's map
forbade readers that try one format and then another.
- A consumer-run loop over `put`: that is the procedure that stranded the store.
### Brokkr landing contract (follow-up `brokkr-msgpack-payloads-r4`)
The merge of `hands/py-msgpack-default` to main waits on three things:
1. Soul holds on the fix batch.
2. A Brokkr branch, Soul-held, is ready in its own campaign. On it, the daemon writes MessagePack
(`put_messagepack_document`) into both stores. The JSON-first read becomes a read of exactly one MessagePack
value with a trailing-bytes check, and the bridge's 8 definitions are unchanged. The Blender store is converted
by `cultcache_py.convert` over those 8 definitions into a new file beside it, because those payloads came from
cultcache-py's old default (P14). The daemon-written JSON receipts in the Unity store are the daemon's own, and
it rewrites them, also into a new file.
3. An operator window: close Blender and stop sync, Self merges CultLib and Brokkr together, build the daemon,
run both conversions, open each new file, swap it into place, then restart. The JSON originals are kept under
another name for as long as the operator wants them.
### Revision 3
Imagination pass `imagination-py-msgpack-r3`, session `self-2026-10-12-eureka`, against `hands/py-msgpack-default`
at `57f85acd`. Python probes ran on Starfire (Python 3.13, msgpack 1.2.2) over a `git archive` of the cultcache-py
and cultnet-py sources at that head, and were removed afterwards. The C# facts are Soul s2's (`jobB`, Yggdrasil),
not rerun here.
P8. **What still decides map keys at 57f85acd.** One search found three MessagePack readers that see document
payload maps. (a) The codec's `_unpack_tree` applies the reference rule through `_checked_map`. (b) The store's
whole-file `msgpack.unpackb(data, raw=False)` (`stores.py:98`) applies msgpack's `strict_map_key=True`. A legacy
envelope file whose inline payload is `{1: 'a'}` is refused with "int is not allowed for map key". Read through
`_unpack_tree` instead, the same file yields the payload `8101a161`, and a v1 snapshot's framing decodes
identically either way. (c) cultnet-py's stamp router (`replication.py:192`) applies `strict_map_key=True`. Read
through the codec, `['x.v1', {1: 'a'}]` and `{'schemaVersion': 'x.v1', 'v': {1: 'a'}}` both route to `x.v1` (C#
routes both, Soul s2 STAMPPROBE). A nil-keyed payload gives `None`, as it does today and in C#. Every other
`unpackb` in cultnet-py and cultmesh-py reads wire messages, cursor files or fixed-slot protocol records, whose
maps are frames rather than payload data. Their bound is follow-up `py-wire-decoders-unbounded`. Unmeasured: C#
reads a map stamp as `IReadOnlyDictionary<string, object>`, so a top-level non-str key may stop C# routing where
Python routes. The resolved definition decides the record either way.
P9. **The two depth counters.** `_check_writable` (documents.py:184) counts every node, scalars included, to
`_PACK_DEPTH_LIMIT = 511`. `_check_payload_tree` counts containers to 256. Commit `eef1cb15` moved the codec
from 256 to 511 so that `test_union_parity.py:650` could build its 257-deep read fixture with
`document.payload_codec.encode(past)`. The fixture needs bytes that are 257 deep, not a codec that writes them:
`msgpack.packb(past, use_bin_type=True)` and `json.dumps(past).encode()` build them, and both writers nest to 511
or further. One container walk, bounded at `MAX_PAYLOAD_DEPTH`, serves both the definition and the codec. It also
ends a cycle, because a cycle reaches 257.
P10. **Rerun.** No byte in a CultCache store records a payload format, and no wire change is wanted for one. A
migration that rewrites the live store in place therefore cannot tell its own output from its input when a
payload is valid in both formats. `b'5'` is the JSON 5 or the MessagePack 53, and Soul s2 saw 53 become 5. No
check on the payload content can separate the two, so the rerun guard has to come from outside the payload.
P11. **Concurrency in the store.** cultcache-py's file stores hold a per-object `threading.RLock` only, so every
`push` is a read-modify-write that loses another process's write. The C# store opens a `.lock` sidecar with
`FileShare.None` (`CultCache.cs:3489-3509`), and cultcache-rs takes an `fs2` lock on its sidecar (`lib.rs:853-859`).
cultcache-ts has none (a search of `packages/cultcache-ts/src` finds no lock). The migration race is one case of a
store that lacks the reference's lock.
P12. **Prior art and the shape chosen** (primary docs fetched 2026-10-12). SQLite `VACUUM INTO`: "the original
database file is unchanged", and the target "must not previously exist, or else it must be an empty file". LMDB
`mdb_env_copy`: the target directory "must already exist and be writable but must otherwise be empty". Realm is
the counter-example to Self's hypothesis. It migrates in place on open, and a rerun is safe because the file
records its schema version, which CultCache's payload format does not (P10). The chosen shape follows `VACUUM
INTO`, with the destination role taken by a mandatory backup:
`migrate_payloads(store, definitions, *, backup, source_codec=JSON_PAYLOAD_CODEC) -> int`
1. Copy the live store's bytes to `backup` with exclusive create (`open(backup, "xb")`). If the backup exists,
the call is refused before anything is read. Every rerun is refused this way, whatever the payloads hold.
2. Read the records from the backup, the frozen copy, and rewrite the listed ones. Any refusal of any exception
class raises `CultCacheError` naming the type, the record key and the code or class.
3. Compare the live bytes with the copied bytes. A difference means a writer ran during the migration, and the
call refuses and says to stop every writer.
4. Replace the live file in one `_replace_all` (temp file and rename).
5. On any failure after step 1, the backup is removed and the live store is unchanged. A count of zero also
removes it.
Probe (`probe2`, this shape over the 57f85acd sources). The first run over `b'53'`, `b'5'` and an unlisted type
gives 53 and 5, keeps the unlisted bytes, and keeps the JSON in the backup. A second identical call raises
`FileExistsError` and leaves the live bytes unchanged. A push between the read and the swap (the s2 interleave)
is refused, with the backup removed and the live store holding the writer's JSON record. A validate that raises
`AssertionError` is named by class and the backup is removed.
What remains and is stated: the window between the compare and the rename, closed by the lock in `5a`. The
precondition is to stop every writer, as it is for SQLite's offline tools. If someone deletes the backup and then
reruns the migration on its own output, P10 recurs. The README says to delete the backup only after the
migrated store has been opened, and never to rerun after that.
P13. **Float32 keys.** `Float32(0.1).value` is `0.1`, so a `Float32` key is not rounded until it is packed.
Decoded, it is the float32-rounded value. Writing `{1, F32(1.0)}`, `{True, F32(1.0)}`, `{1.5, F32(1.5)}` or
`{0, F32(-0.0)}` produces bytes that the codec's own decode refuses as `duplicate_map_key`. `{0.1, F32(0.1)}` and
two NaN keys read back as two keys. The rule that matches decode exactly compares each key's decoded image (a
`Float32` key's image is `struct.unpack('>f', struct.pack('>f', v))[0]`) and refuses a collision on write as
`duplicate_map_key`. The reference reads all four collision maps as two keys (Soul s2 KEYPROBE). Python cannot,
for the same reason it cannot read `{1, 1.0}`, which is the residual already recorded. Refusing on write keeps
Python inside what it can read back, and it is the write half of that residual.
### Revision 4
Imagination pass `imagination-py-msgpack-r4`, session `self-2026-10-14-eureka`, against `hands/py-msgpack-default`
at `a59bbc7d`, under ruling `py-migration-shape-ruled` (convert-to-new-file). Source reads only; nothing was run.
P14. **The shape ruled.** `convert(source, destination, definitions, *, source_codec=JSON_PAYLOAD_CODEC) -> int`
replaces `migrate_payloads`. It creates `destination` with exclusive create (`open(path, "xb")`) before it reads
anything, so an existing file (the source itself, an earlier output, anything) is refused and left alone. It reads
the source once through the source store's `pull_all`, rewrites the listed records in memory, writes the store
bytes into the file it created, and fsyncs it. Any failure after the create removes that file. It never opens the
source for writing. The destination is the same store kind as the source, and its bytes come from the same
per-kind encoder `_replace_all` uses, so the two writers cannot drift. The caller stops writers, converts, opens
the new file and swaps it in. This follows SQLite `VACUUM INTO` and LMDB `mdb_env_copy` (P12) without the backup
role that r3 added to them.
P15. **What the shape makes impossible, and what stays.** The library no longer writes a live store during a
format change. That leaves no code to fix for s2's `migrate-races-concurrent-writer` or s3's
`inflight-writer-strands-migration`, `rerun-guard-is-per-backup-path` and `stopped-migration-has-no-recovery`,
because the backup, the compare and the swap are deleted. The library does not lose a write made during
`convert`. The write lands in the source, and only the caller's swap can drop it. The README makes stopping
writers the caller's precondition, as SQLite's offline tools do. A killed run leaves the source intact and possibly
a partial destination; recovery is to delete the destination and run again. A partial JSON Lines destination can
parse as a smaller store, so the README says the destination is complete only once `convert` has returned.
Converting a store that is already MessagePack can still produce wrong values in the destination (P10: `b'5'`),
but the source is untouched. The cross-process store lock (follow-up `py-ts-store-cross-process-lock`) is still a
store parity gap, but nothing in this cut waits on it.
P16. **The codec decodes only what it writes.** At `a59bbc7d`, `MESSAGEPACK_PAYLOAD_CODEC.decode` is `_unpack_tree`
alone. It returns trees 257 or more deep, `ExtType` values, and timestamps outside .NET `DateTime` (Soul s3); the
definition or the reference refuses every one of them. Revision 4 also runs the write check over the decoded tree
(`_check_carried`, renamed from `_check_writable`). That gives one rule in both directions. The depth walk, the
integer range and the key kinds apply as before. No `ExtType` node is carried (code `value_kind`, kind `ext`).
A timestamp's seconds must lie within `-62135596800..253402300799` (code `timestamp_out_of_range`). That interval
is `DateTime.MinValue` to `DateTime.MaxValue`. It is derived from MessagePack-CSharp's timestamp reader, which adds
the seconds to the Unix epoch and then the nanoseconds as ticks. It was not measured at the seam here, and a
C#-written seam row belongs to follow-up `leaf-vectors-next`. The store's whole-file read keeps `_unpack_tree`
alone, because a store frame is not a payload. The `any` leaf keeps its own ext refusal, which is the slot's rule
for a value handed to it directly.
P17. **NaN keys.** The reference holds a map's keys in a `Dictionary` keyed by CLR type and value, where a NaN
equals a NaN of the same width. It therefore refuses two float64 NaN keys and accepts a float64 NaN beside a
float32 NaN (Soul s3). Python reads both widths as one `float` and cannot tell them apart. Revision 4 gives
`_key_image` one NaN image, used by `_checked_map` on read and by `_check_carried` on write. Any two NaN keys are
refused both ways as `duplicate_map_key`. On the mixed-width pair this refuses something the reference accepts,
the same kind of residual as `1` beside `1.0`. The other choice would read two float64 NaN keys that the
reference refuses.
Revision 5 records this as a deliberate exception to ruling `leaf-acceptance-follows-reference`, because Python
cannot tell the two widths apart (msgpack-python decodes both to one `float`) and no ruling lets the codec read a
pair the reference refuses. Soul s4 probed the C# behaviour: `Dictionary<object,object>{float.NaN, double.NaN}`
serialises as `82 ca ffc00000 01 cb fff8000000000000 02`, the reference reads it back as two keys, and cultcache-py
refuses it on read as `duplicate_map_key` (base `a59bbc7d` accepted that read). Every same-width NaN pair, with
equal or different payload bits and with +NaN beside -NaN, is refused by both runtimes. The exception lives where a
consumer reads it: the cultcache-py README's Wire Parity section, pinned by a test vector (the bytes above).
## D. cultcache-rs decode stack safety (cut `decode-stack-safety`)
Pinned base: `origin/main` at `ae6ef5a8` (2026-10-14). Imagination pass `imagination-decode-stack`, session
`self-2026-10-14-eureka`. Ruling `decode-stack-mechanism-ruled` (grow-stack); prior art
`docs/research/decode-stack-safety.md`. Probes live under the session scratchpad, `imagination-decode-stack/`:
`probe` decodes through a scratch copy of cultcache-rs at the base whose derive calls a hook per derived level
(off, observe the stack used between levels, or `stacker::maybe_grow`), against Eve's `thing-contract` at
`2467e1b`; `wasmprobe` is the wasm run. Yggdrasil rust image (rustc 1.95.0, rmp-serde 1.3.1, serde_stacker
0.1.14, stacker 0.1.25, psm 0.1.32), jobs `job1` to `job5`. None of it is committed.
Shapes: `box` is a thing-contract Box chain (3 containers a level, the bound admits 83), `repeat` a Repeat chain
(2 a level, admits 125), `fat` a 32-slot record of `[u64; 32]` with an `Option<Box<Self>>` slot (1 a level,
admits 255, so the leaf arrays sit at depth 256), `json` a one-slot record holding a `serde_json::Value` nested
255 arrays, `ignored` a one-slot record whose extra trailing slot nests 255 arrays (skipped as `IgnoredAny`).
### Body facts
D1. **Today's abort, reproduced.** `decode_payload` at the bound, first level that aborts (process abort, no
exit code, every time):
| Profile, thread | box (83) | repeat (125) | fat (255) | json, 64 KiB | ignored, 32 KiB |
|---|---|---|---|---|---|
| release, 1 MiB | 44 | 56 | 13 | 115 | 24 |
| release, 2 MiB | ok | 114 | 25 | | |
| debug, 1 MiB | 11 | 14 | 5 | 15 | 8 |
| debug, 2 MiB | 23 | 30 | 10 | | |
One level past the bound is refused typed (`PayloadTooDeep { depth: 257, limit: 256 }`) for every shape and
every variant, before any recursion.
D2. **Recursion points.** Per container level a typed decode passes through: the derive's generated
`Deserialize::deserialize` (`cultcache-rs-derive/src/lib.rs:294-330` for slots, `534-684` for unions), which
calls `deserialize_tuple`; rmp-serde's `Deserializer::any_inner` (`decode.rs:530`), which calls
`visitor.visit_seq`/`visit_map`/`visit_newtype_struct` under `depth_count!` (566, 583, 616); the derive's
`visit_seq`, whose frame holds one `Option<T>` per slot; and `next_element`, which re-enters either a derived
type, serde's own impls (`Vec`, `Option`, `Box`) or another crate's (`serde_json::Value`, `rmpv::Value`).
rmp-serde 1.3.1 has no skip of its own: `IgnoredAny` goes through `any_inner` and a visitor like any value.
`check_payload_depth` is iterative.
D3. **Where the grow must sit.** A `maybe_grow` in the derive's `deserialize` covers derived levels only: `json`
on a 64 KiB debug thread still aborts at 15 with a 32 KiB red zone, and passes only when the first derived
level happens to hand it a whole new segment. A `serde_stacker::Deserializer` over rmp-serde at the door checks
at every `deserialize_*` call it forwards (`maybe_grow` at `de.rs:476, 489, 502, 511, 520`), so
rmp-serde's recursion, serde's impls, foreign `Value` types and `IgnoredAny` skips all re-enter through a check:
every shape passes in both profiles (D4). Consumers decode non-derived recursive types through the door today:
cultnet-rs `replication.rs:525` (`serde_json::Value`) and `contracts.rs:607` (`rmpv::Value`).
D4. **The frame between checks, measured.** Stack used from one derived `deserialize` to the next nested one,
at the bound: release box 13,168 B, repeat 12,192 B, fat 84,944 B; debug box 52,064 B, repeat 49,760 B, fat
207,504 B. The door wrapper checks more often, so its gap is smaller: release, every shape passes at a red zone
of 32 KiB on 128 KiB, 1 MiB and 2 MiB threads; debug, box and repeat pass at 32 KiB, fat fails at 128 KiB and
passes at 144 KiB through 256 KiB (1 MiB thread). So the worst gap constructed is between 128 and 144 KiB,
the debug `visit_seq` of the 32-slot record. serde_stacker's defaults (red zone 64 KiB, segment 2 MiB,
`param.rs:10-11`) fail debug fat. The cut sets 256 KiB and 2 MiB: 1.8x the worst measured gap. A type whose
single `visit_seq` frame exceeds the red zone can still overflow; that is documented, not prevented.
D5. **Same values, same errors.** Through the wrapper, box 83, repeat 125 and a 1,000-node surface decode equal
to the plain decode. The wrapper forwards `is_human_readable` (`de.rs:313`) and returns rmp-serde's own error
type, so `opaque_decode_error` and the derive's refusal texts are unchanged.
D6. **Cost.** No growth (release, repeat 125 on an 8 MiB thread): 322-339 us through the wrapper against 320 us
plain. A shallow 27 KB, 1,000-node surface on 1 MiB: 1.57 ms plain, 1.61-1.80 ms wrapped (run-to-run noise is
about that wide). When the decode does grow, it pays for faulting in fresh segment pages: repeat 125 on a 1 MiB
thread 1.5-1.7 ms; fat 255 on 1 MiB 25 ms against 9.3 ms on a 64 MiB thread. A 1 MiB and a 2 MiB segment cost
the same.
D7. **Windows x86_64: source read, not run.** stacker 0.1.25 does not use psm on Windows: `backends/windows.rs`
grows by fibers (`ConvertThreadToFiber`, `CreateFiber`, `SwitchToFiber`, `SetThreadStackGuarantee`), reads the
limit with `VirtualQuery`, and links a C helper built by `cc`. psm's README marks its own Windows stack switching
disabled; that is the other layer, so the research's conflict (C) is two layers, not a contradiction. Yggdrasil
has no Windows: the rust image carries `x86_64-pc-windows-gnu` (cross-compile, no run). Whether it actually grows
on Starfire is question `decode-stack-windows-check`.
D8. **wasm32: stacker is not a no-op.** psm's `build.rs:53` ships a precompiled `wasm32.o` and sets
`switchable_stack`; stacker then allocates segments from the global allocator with no guard page
(`alloc_stack_restore_guard.rs`). `remaining_stack()` is `None` on wasm, so the first check always grows and
the limit is known from then on. Under wasmtime 48.0.5 (wasm32-wasip1, release), a nested 9-slot record: plain
decodes 48 levels and traps at 64 ("out of bounds memory access" at `0xffffe510`: the stack-first shadow stack
wrapped below zero); wrapped decodes 64, 255, 400 and 600 levels (linear memory 1,088 KiB before, 8,896 KiB
after 255) and traps at 1,000 with "call stack exhausted", the engine's own stack, which stacker cannot grow.
`wasm32-unknown-unknown` builds. This corrects the ruling's premise that wasm keeps the abort: the shadow stack
grows and the engine stack is the host's setting, so the documentation says that instead.
D9. **Dependency.** `serde_stacker` 0.1.14 (dtolnay), pulling `stacker` 0.1.25 and `psm` 0.1.32 (rust-lang),
all `MIT OR Apache-2.0`; plus `cfg-if`, `libc`, `cc` (build) and `windows-sys` on Windows. psm assembles and
stacker compiles C at build time, so a C toolchain becomes a build requirement (gcc in the rust image, MSVC on
Starfire). cultcache-rs is MIT. Written by hand, the wrapper would be serde_stacker's ~650 lines of forwarding.
### Body facts, revision 2 (verdict `cut-decode-stack-safety.s1`)
Imagination pass `imagination-decode-stack-r2`, session `self-2026-10-14-eureka`, on branch
`hands/decode-stack-safety` at `2505c0c4`. Probes live under the session scratchpad,
`imagination-decode-stack-r2/` (`gen.py`, `gen2.py`, `driver*.rs.in`, jobs `jobP1` to `jobP4`), run on Yggdrasil
in the rust image (rustc 1.95.0). None of it is committed. `jobP1` patches a copy of serde_stacker 0.1.14 so
each of its five `maybe_grow` calls (`de.rs:476-520`, the wrapper visitor's container entries) records the
stack pointer, and reads the door's red zone and segment from the environment. Shapes: `wide` is the committed
`Wide32` (32 slots of `[u64; 32]`, 8 KiB inline); `bigN` is 32 slots of an N-array record (32·N KiB inline:
32, 64, 128, 256 KiB), each with an `Option<Box<Self>>` slot; `lim` is 31 slots of a 2-array record (15,880 B).
A caller is one `#[inline(never)]` function holding the `Result<T>`.
D10. **The gap between checks is linear in the record's inline size.** Stack used between two consecutive
checks, the worst over a 6-level decode on a 1 GiB thread (every gap in a shape is the same value):
| Shape (inline) | wide (8 KiB) | big4 (32) | big8 (64) | big16 (128) | big32 (256) |
|---|---|---|---|---|---|
| debug gap | 115 KiB | 451 | 899 | 1,795 | 3,587 |
| debug frames before the first check | 131 KiB | 515 | 1,027 | 2,051 | 4,099 |
| release gap | 48 KiB | 192 | 384 | 768 | 1,536 |
| release frames before the first check | 48 KiB | 192 | 384 | 768 | 1,536 |
So the gap is 14·S + 3 KiB in debug and 6·S in release, for S = `size_of` of the record entered: the frames
between a container check and the next one each hold the record being built or a `Result` of it (rmp-serde's
`any_inner`, the wrapper visitor, the derive's `visit_seq` with one `Option` per slot, the seed). D4's 128-144
KiB debug gap for `fat` is this line at 8 KiB plus a margin. The stack touched below the deepest check is 6 KiB
in debug and 0 in release, so the excursions between checks are the gaps above.
D11. **Why the thread size decided, in two ways.** (a) Before the first check the decode has already used the
second row of D10: `rmp-serde`'s `deserialize_tuple`, `any_inner` and the wrapper's frames sit between
`decode_payload` and the wrapper's first `maybe_grow`, and each holds `Result<T>`. A thread smaller than that
row aborts whatever the red zone: `wide` aborts on every thread up to 144 KiB in debug. (b) Mid-decode, a check
with between R and the gap remaining lets the next gap overrun; after the first grow the positions in each
segment are fixed, so which check first grows (set by the thread size) decides, which is the non-monotonic
pattern Soul measured. Thread sweeps at the bound (threads from 64 KiB in steps of a 64th of the span, plus
1, 1.5, 2, 3, 4 and 64 MiB):
| Profile, shape | R = 256 KiB | R = gap - 48 KiB | R = gap rounded up + 64 KiB |
|---|---|---|---|
| debug wide | 6 of 65 sweep threads abort (64-144 KiB); none at 1-64 MiB | all 65; and at 1-4 MiB | 6 (64-144 KiB); none at 1-64 MiB |
| debug big4 | 39 of 65; and at 1.5, 2, 3, 4, 64 MiB | 29; and at 2, 64 MiB | 24, the lowest from 64 KiB; none at 1-64 MiB |
| release wide | none | 14, spread from 112 to 864 KiB; and at 1, 4 MiB | none |
| release big4 | 9 (64-192 KiB) | 18, including 448-480 KiB | 9 (64-192 KiB) |
| release big8 | 40; and at 1.5, 2, 3, 64 MiB | 20; and at 1.5 MiB | 17, the lowest from 64 KiB; none at 1-64 MiB |
With R at least the gap, the abort counts match the threads smaller than the frames before the first check
(D10's second row plus the harness) in every shape and profile, big16 and big32 included (segments 4·R); with
R below the gap, threads well above those frames abort mid-decode.
D12. **Checking at the top of the door** (`jobP4`). `decode_payload` patched so its whole body runs inside
`stacker::maybe_grow(R, segment, ..)` before the walk and the wrapper, R = 512 KiB, segment 2 MiB. Every
thread from 24 KiB to 1 MiB in 8 KiB steps, then 1.5, 2, 3, 4, 8 and 64 MiB:
| Profile | wide (8 KiB), 1 level | wide, 255 levels | lim (15,880 B), 1 level | lim, 254 levels |
|---|---|---|---|---|
| debug: smallest thread that decodes | 40 KiB | 40 KiB | 72 KiB | 72 KiB |
| release: smallest thread that decodes | 24 KiB | 24 KiB | 40 KiB | 40 KiB |
No thread at or above the smallest one aborts, in any of the four columns, in either profile. lim's gap is
220 KiB in debug and 93 KiB in release (D10's lines give 220 and 93), so R = 512 KiB is 2.3 times the debug
gap of a record at 16 KiB. What a thread needs is now the caller's own frames, which hold the returned value
and do not depend on the payload's depth.
D13. **No red zone or placement removes the bound.** Every frame that holds a value of type T sits between the
check before T's container and the check inside it, and serde's own impls add frames that hold T before T's
first check: `Box<T>`'s `Deserialize` holds the `Result<T>` it boxes, and the `Option` check before it sized
nothing larger than a pointer. A per-type red zone (a forked wrapper sized by `size_of::<V::Value>()`, or a
grow in the derive sized by `size_of::<Self>()`) still leaves that frame of about one to three T outside any
sized check. So the stack a decode needs between checks is bounded only by bounding the inline size of the
records it builds. The derive is the one place that sees every record type it produces: a `const` assertion on
`size_of::<Self>()` refuses an oversized record at compile time. A field of a type the derive did not produce
(a fixed array of arrays, a large tuple, a plain serde struct) behind a `Box` or `Vec` is not reached by it.
D14. **Build closure and wasm, measured by Soul (`soul-decode-stack-s1` jobD, `cargo tree -e normal,build`
at `2505c0c4` against `ae6ef5a8`).** Added on `x86_64-unknown-linux-gnu`: serde_stacker 0.1.14, stacker 0.1.25,
psm 0.1.32, cc 1.8.0, find-msvc-tools 0.1.14, shlex 2.0.1, object 0.39.1 (`Apache-2.0 OR MIT`), all
`MIT OR Apache-2.0` except ar_archive_writer 0.5.3 (`Apache-2.0 WITH LLVM-exception`, build-only, via psm).
On `x86_64-pc-windows-msvc` the same plus libc 0.2.190 and windows-sys 0.61.2 (`MIT OR Apache-2.0`, beside the
crate's own windows-sys 0.59). `fs2 0.4` (8 call sites in `lib.rs`, the store's file locks) has no wasi
backend, so cultcache-rs does not build for `wasm32-wasip1`; D8 measured serde_stacker in a separate crate.
### Authority, and why
The door owns stack safety as it owns the bound: `decode_payload` runs the bound walk, then rmp-serde through
`serde_stacker::Deserializer` with two private constants. The ruling's "every container level the derive and the
door descend through" is met there, because every derived level is entered through the wrapper (D3); a
`maybe_grow` in the derive would be a second owner of the same check, needs a red zone of 128 KiB in release
and 256 KiB in debug for fat where the wrapper needs 32 and 144 KiB (D4), and still misses foreign recursive types. The derive is unchanged.
Revision 2 (D10-D13): the door's first act is a check, so no frame holding T precedes it, and the red zone is
set against the gap of the largest record the derive may produce. That size is a public constant the derive
asserts at compile time, because nothing at decode time can bound a frame that holds a T before T's own check
(D13). The thread's size then decides nothing beyond the caller's own frames, which hold the returned value
whatever the payload's depth (D12). Envelope and store decodes enter through the same grown decoder, without
the payload walk, so what they accept is unchanged. rmp-serde's 1024 counter stays a backstop that never decides. Callers that decode a payload with
`rmp_serde::from_slice` directly bypass the door, the bound and the grow alike; cultnet-rs re-decodes
door-checked `rmpv::Value`s that way (follow-up `rs-redecode-outside-door-r2`).
Rejected inside the ruling: the derive placement (above), and writing the wrapper by hand (D9: established
tooling, one owner upstream).
## E. Expect under compatible schema drift (cut `expect-persisted-schema`)
Pinned base: `origin/main` at `b549d731` (2026-10-21). Imagination pass `imagination-expect`, session
`self-2026-10-19-ag7`. Probe `probe1` (`ProbeExpectDrift.cs`, written into the clone by the job command, never
committed) ran on Yggdrasil, dotnet sdk 10.0, against `b549d731`. Fixture: a record written under
`DriftWide` (`tests.variant_drift.v1`) and read by a cache whose registry holds only `DriftNarrow` (v2) loads
as CompatibleDrift.
### Body facts
- **E1. The defect.** `CultCommitRequest.ConditionsHold` (`CultCache.cs:1677-1696`) compares the durable
record's `SchemaId` with the observed entry's `Descriptor.SchemaId` (`Expect` captures it at `:1772`;
`ExpectUnchanged` reads it at `:1694`). `ToStoredDocument` (`:3212-3268`) resolves a persisted record to the
local descriptor and drops `record.SchemaId`. So the observed side holds the local id and the durable side
the persisted id. Probe: unchanged drifted record, `Expect` + `Upsert` commits **False** on the single-file
store (P1) and on the directory store (P7). Same code at Aetheria's pin `45c2f40` (`CultCache.cs:1147-1207`).
- **E2. What must still refuse, at base.** Another cache commits a changed value (new minted `storedAt`):
Expect False (P4). Another cache reading under v3 (`DriftReused`) flushes the file, rewriting the record under
v3 with the **same** `storedAt`: Expect False (P5).
- **E3. A whole-file write rewrites every record under its local schema and keeps `storedAt`.**
`ToPersistedRecord` (`:3194-3204`) writes `entry.Descriptor.SchemaId` with `entry.StoredAt`, and both the
single-file `PushAll` (`:3361-3375`) and an unconditional or dirty `CommitBatchCore` (`:3393-3399`) write the
whole view. So the contract sentence "every write to a key mints a later storedAt"
(`Contracts/cultcache-store-composition.md:229-231`) is false for a flush: the schema id is the half of the
identity that sees it. The directory store writes only staged keys, each freshly stamped
(`DirectoryMessagePackBackingStore.cs:240-293`), so it never normalises a clean record.
- **E4. Lifetime at base.** After the cache's own `PushAll`, Expect commits True (P2): disk and descriptor now
agree. After *another* cache's normalising flush to the observer's own local schema, Expect commits True (P3);
after a re-pull, True (P6).
- **E5. Re-reads.** Both pull filters skip a record whose `storedAt` and local descriptor id are unchanged
(`CultCache.cs:3323-3327`, `DirectoryMessagePackBackingStore.cs:110-114`). A record normalised P to L by
another writer keeps `storedAt` and the local id, so with the persisted id on the entry the filter must
compare the persisted id, or a re-read never refreshes it and Expect stays a Mismatch forever.
- **E6. Other runtimes.** cultcache-rs `SingleFileMessagePackBackingStore::compare_exchange` and the snapshot
form (`packages/cultcache-rs/src/lib.rs:1352-1393`, `:1427-1450`) compare whole raw envelopes as read from
disk (`schema_id: Some(record.1)`, `:3189`); the expectation *is* the persisted envelope, and the crate has
no compatible-drift resolution, so the split cannot occur. Unchanged. cultcache-ts and cultcache-py have no
conditional commit (`cultcache-persistence-format.md:67-69`; grep finds none); nor do the cultnet and
cultmesh packages or cultmesh-kotlin. Not added here.
- **E7. Release path.** Aetheria pins `org.gamecult.cultlib#cultlib-unity-v1.0.60` and
`org.gamecult.caching.unity#caching-unity-v1.4.0`, both locked at `45c2f400` (2026-09-17). The CultCache
runtime ships as DLLs in `unity/org.gamecult.cultlib` (built by `scripts/build-unity-package.ps1`). Main is
1363 commits past the pin with a `### Breaking` section under `[Unreleased]`
(`unity/org.gamecult.cultlib/CHANGELOG.md`). `release/cultlib-unity-1.0.x` is the pin plus one commit
(1.0.61, Win64 QUIC plugins).
### Authority, and why
The backing store that read or wrote a record's durable bytes owns the schema id it is persisted under. It is set
when the store builds the entry from a persisted record (`ToStoredDocument`), defaults to the descriptor's id
for an entry this cache stamps (it is about to be written under that id), and is reset to the descriptor's id,
under the hold, after a whole-file write the store completed. The commit check, `Expect` and the pull filters
only read it. `Descriptor.SchemaId` stays what the entry decodes as; it is no longer the record's durable identity.
Consequence, recorded rather than ruled: after another writer's whole-file flush rewrites a drifted record
under a schema id this cache did not read it under, an `Expect` captured before is a Mismatch until the cache
re-reads (E4's P3 flips). That is the ruling's "compares that persisted id" applied to a rewrite that kept
`storedAt`; it is fail-closed, the standard retry (re-read, retry) recovers, and it is what refuses E2's P5.
## Observed, not owned by these cuts
- At the default tolerance the solver's answer on allocator-shaped problems depends on the start by up to the whole
box (B6). That is documented and pinned (`AnAllocatorShapedProblemIsStartIndependentAtATightTolerance`); a
consumer that needs one answer passes a tight tolerance, as Aetheria does.

View file

@ -0,0 +1,32 @@
# CultLib Gaps: Target
Date: 2026-10-09. Campaign `cultlib-gaps` in the Eureka mind.
## Why
Consumers of CultLib keep finding capabilities a reasonable consumer of a general
library would expect and that CultLib lacks. Doctrine says a gap is filled in its
owner, never worked around in the consumer. This campaign is the standing home for
those gaps, so each one lands in CultLib as a small cut instead of a local helper in
whichever project hit it.
Known gaps at opening (each a follow-up in the mind, owned by the consumer campaign
that found it):
- CultMath `BoundedLeastSquares.Solve` has a fixed KKT tolerance (1e-6); Aetheria's
thrust allocator needs a tighter one (`aetheria-release:follow_up:cultmath-bls-kkt-tolerance`).
- `cultcache-rs` has no decimal type, its snapshot reads follow symlinks, and its
single-file store stages files with the process umask and no mode option
(`eureka-body:follow_up:cultcache-rs-decimal-and-nofollow`).
## Invariants
- `owner-fills-gap`: a capability a consumer needs from CultLib is added to CultLib, with the consumer adopting it; no consumer-local copy survives the cut that adds it.
- `defaults-unchanged`: a new option defaults to today's behaviour; existing callers and stored bytes do not change.
- `wire-parity`: a CultCache wire change keeps byte parity across runtimes against the C# reference, with a cross-runtime fixture.
- `semver-measured`: a public API change bumps the package version by measured semver and ships as a tagged release consumers can pin.
## Not in scope
- Growing CultCache into a general database (memory `cultcache-scope-restraint`).
- Any consumer's adoption code beyond the pin bump; that stays in the consumer's campaign.

View file

@ -0,0 +1,201 @@
{
"campaign": "cultmath-tapes",
"cut": "glsl-lowering",
"revision": 2,
"title": "GLSL lowering r2: Phacelle as its own MPL-2.0 file so CultMath.glsl is MIT, notices corrected, iv_exp in the fixture, fixture regenerated, glslang and dxc run",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-glsl-lowering",
"base": "3ac7250a",
"depends_on": [
"interval-ops"
],
"first": [
"Follow-up on cut report cultmath-tapes:cut_report:cut-glsl-lowering.h1 (range c9bd003b..5ae207e8, no verdict yet; worktree F:\\Projects\\CultLib-glsl). Rulings in force: operator-split-phacelle, operator-compiler-downloads, self-exp-measure-in-site. Map: docs/cultmath-interval-ground-cut.md, 'Pass 3'. First commit: MERGE 3ac7250a (interval-ops r3's head, which Soul s2 found holds for everything this cut lowers) into hands/cultmath-glsl-lowering. Do not rebase: the branch is pushed, and the git rules forbid force-pushing a shared branch. Probed by Imagination in a scratch clone: the merge conflicts only in packages/cultmath/README.md. Resolve it by keeping r3's Intervals bullet (iv_frustum_ball, its rounding bound, the envelope), then r1's GLSL bullet, so the GLSL bullet sits at :48-53. design.md and HlslSourceCompatibilityTests.cs merge clean.",
"Second commit: regeneration alone (CULTMATH_WRITE_GLSL=1). Probed on the merge: CultMath.glsl gains 15 lines (cultmath_iv_frustum_ball with the r3 widening); the fixture does not change until this revision adds its families; GlslMirrorTests 4/4 and HlslSourceCompatibilityTests green (16 tests). Soul's range is 3ac7250a..HEAD, which carries r1's four commits through the merge, so the whole lowering gets its first verdict. Runs in parallel with interval-ops r4: r4 changes no src, HLSL or README, only NoiseBoundTests.cs and design.md's saving section (:475-512 at 3ac7250a, :533-570 after this merge), disjoint from this cut's :136-193. Do not merge r4's head into this branch.",
"The split (operator-split-phacelle). Transformation step 1 becomes: each #include is inlined in place, except an include whose file is under its own licence, which is lowered on its own into its own output file. The list of such includes is one table in GlslLowering, SeparateFiles = { \"CultMath.Phacelle.hlsl\" -> \"CultMath.Phacelle.glsl\" }, with the reason beside the entry (file-level MPL-2.0). That is an enumerated rule of the lowering, documented as step 1 in design.md; it is not a per-function special case, and nothing else is keyed by name. Lower(root) returns the two texts; Lower(string) stays for the inlined body if that is simpler, but there is one resolver (ReadShaderSource, generalised to accept the include predicate), never a second.",
"shaders/CultMath.glsl: the provenance line says it is generated from CultMath.hlsl and CultMath.Interval.hlsl, MIT, and that cultmath_phacelle is in CultMath.Phacelle.glsl, to be concatenated after this file by consumers that call it. The dropped include line leaves nothing behind. shaders/CultMath.Phacelle.glsl: its own provenance line (generated from CultMath.Phacelle.hlsl, MPL-2.0, requires CultMath.glsl before it), the MPL-2.0 header and attribution exactly as the HLSL carries them, the CultPhasor struct and cultmath_phacelle, guard CULTMATH_PHACELLE_GLSL. Both files pinned by CommittedGlslEqualsLowering; CULTMATH_WRITE_GLSL=1 writes both.",
"Pin tests over both files: NoHlslOnlyTokensSurvive runs on each; EveryPublicFunctionIsLowered takes the union of both files' definitions against the HLSL (minus texture samplers) and additionally asserts cultmath_phacelle and CultPhasor appear only in the Phacelle file; a new PhacelleFileCompilesOnlyAfterTheLibrary is not needed, the compile check below proves it.",
"The fixture (self-exp-measure-in-site). The site measures WebGL2 exp against the fixture, so iv_exp joins it; its C# bits come from the platform exp and differ by OS, so its entry carries tolerance \"ulp-bounded, platform exp\" and names the OS that generated it. GoldenFixtureMatchesCSharp parses the committed JSON (System.Text.Json, BCL) and compares family by family: text-equal for every family except those whose tolerance names the platform, which compare case by case within 1 ulp and print the maximum observed. One fixture, one writer, one tolerance field already in the format; no second fixture. The phacelle family stays in the fixture; the site's parity harness concatenates both GLSL files for that run (the site ships only CultMath.glsl), say so in design.md.",
"Notices: packages/cultmath/THIRD-PARTY-NOTICES.md :35-43 lists the MPL-2.0 files as math.Phacelle.cs, CultPhasor.cs, CultMath.Phacelle.hlsl with its Unity copy, and shaders/CultMath.Phacelle.glsl; CultMath.glsl is MIT and says so in its header. The package licence expressions (MIT AND MPL-2.0) do not change: the NuGet still ships the HLSL include. README :36-37 and :48-53 (after the merge) name both GLSL files and when a consumer needs the second. design.md :136-192: step 1 rewritten, the second file, the wrapper runs, the fixture's platform tolerance. Unity ships neither GLSL file.",
"Compile checks on Starfire (operator-compiler-downloads), one job each, detached with a log, polled in the foreground: tools/get-glslang.ps1 (16.6.0, the pinned SHA already in the script; if the published digest differs, stop and report, do not edit the pin to match); tools/compile-glsl.ps1 gains a -Phacelle switch and runs twice: the MIT-only wrapper over CultMath.glsl calling every function in it, and the full wrapper over both files calling every function including cultmath_phacelle. Negative controls, both must fail: -Body 'float x = lerp(0.0, 1.0, 0.5);' and the MIT-only wrapper with -Body calling cultmath_phacelle (proves Phacelle is not in CultMath.glsl). Then tools/get-dxc.ps1 and dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0. The browser-pane WebGL2 probe of r1 is repeated on the two-file wrapper.",
"Carried from r1 and still out of scope, name them in the report: the fixture's ulp bounds on WebGL2 are measured by the site cut; Stryker is not re-run unless packages/cultmath/src changes in 3ac7250a..HEAD (it should not; the lowering lives in tests; interval-ops r3's Stryker evidence, Soul s2, stands). Deviations r1 reported that stand: the anchored token regex; the iv_exp absence is now resolved by this revision; the HLSL edits (damp splat, pcg3d order) stay."
],
"deletes": [
{
"path": "packages/cultmath/shaders/CultMath.glsl",
"lines": 100,
"note": "the inlined Phacelle block (:510-612 at 5ae207e8: MPL header, CultPhasor, cultmath_phacelle) leaves this file; it is regenerated, not edited."
},
{
"path": "packages/cultmath/THIRD-PARTY-NOTICES.md",
"lines": 3,
"note": ":39-41 the sentence making CultMath.glsl MPL-2.0 as a whole."
}
],
"keeps_moves": [
"Transformation steps 2-9 and their order: unchanged; the cast scanner, saturate lowering, the guard and provenance mechanism stay.",
"Both tracked CultMath.hlsl copies stay one blob; no HLSL edit is expected in this revision (a dialect gap found by the two-file compile is fixed in the HLSL in the common subset, bit-identical, listed in the report).",
"HlslSourceCompatibilityTests' mirror and FindCultMathRoot: the resolver generalises in place; no second include resolver.",
"tests/CultMath.Tests/fixtures/glsl-parity.json: the same file, regenerated; families keep their order so the diff shows only the two additions."
],
"adds": [
"packages/cultmath/shaders/CultMath.Phacelle.glsl: generated, MPL-2.0, guard CULTMATH_PHACELLE_GLSL.",
"GlslLowering: the SeparateFiles table and the two-text Lower; GlslMirrorTests: the second file in CommittedGlslEqualsLowering, NoHlslOnlyTokensSurvive and EveryPublicFunctionIsLowered; GoldenFixtureMatchesCSharp's per-family comparison with the platform tolerance.",
"fixture families: cultmath_iv_frustum_ball (exact) and cultmath_iv_exp (ulp-bounded, platform exp).",
"tools/compile-glsl.ps1: -Phacelle switch (appends CultMath.Phacelle.glsl after the library and includes cultmath_phacelle in the calls).",
"design.md and README: the two files, the concatenation order, the fixture tolerance."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/GlslLowering.cs",
"line": 24
},
"change": "Lower takes the root (or the resolver's two texts) and returns both outputs; SeparateFiles table with the MPL reason; ProvenanceLine per file."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs",
"line": 320
},
"change": "ReadShaderSource accepts a predicate (or returns the separated include's text beside the inlined body); the C# mirror keeps inlining everything."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs",
"line": 30
},
"change": "CommittedGlslEqualsLowering over both files; :45 NoHlslOnlyTokensSurvive over both; :54 EveryPublicFunctionIsLowered union plus the phacelle-only-in-its-file assertion; :64 GoldenFixtureMatchesCSharp per family with the platform tolerance."
},
{
"location": {
"path": "packages/cultmath/tools/compile-glsl.ps1",
"line": 1
},
"change": "-Phacelle switch; the library text is CultMath.glsl, plus CultMath.Phacelle.glsl when set; the call list covers whichever functions are present."
},
{
"location": {
"path": "packages/cultmath/THIRD-PARTY-NOTICES.md",
"line": 35
},
"change": "MPL-2.0 file list: the two C# files, the HLSL include and its Unity copy, shaders/CultMath.Phacelle.glsl. CultMath.glsl is MIT."
},
{
"location": {
"path": "packages/cultmath/README.md",
"line": 48
},
"change": "both GLSL files; a consumer concatenates CultMath.Phacelle.glsl after CultMath.glsl only when it calls cultmath_phacelle."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 150
},
"change": "step 1 rewritten for the separated include; :182-192 the two wrapper runs and the fixture's platform tolerance; the site's parity run concatenates both files."
}
],
"authority_map": {
"owner": "GlslLowering owns the HLSL -> GLSL transformation and now emits two derived files, one per licence; both are pinned by GlslMirrorTests and never edited. The fixture stays owned by GoldenFixtureMatchesCSharp with one tolerance field per family. THIRD-PARTY-NOTICES.md states which files are MPL-2.0 and is the one place that does.",
"inputs": [
"shaders/CultMath.hlsl, CultMath.Interval.hlsl and CultMath.Phacelle.hlsl text at the merged head (3ac7250a merged into the branch)",
"C# math for the fixture values, including the platform exp for iv_exp"
],
"outputs": [
"shaders/CultMath.glsl (MIT) and shaders/CultMath.Phacelle.glsl (MPL-2.0)",
"tests/CultMath.Tests/fixtures/glsl-parity.json with cultmath_iv_frustum_ball and cultmath_iv_exp families"
],
"derived_state": [
"both GLSL files are derived; a hand edit to either fails CommittedGlslEqualsLowering",
"the fixture is derived from C#; the iv_exp family is derived from the generating platform's exp and compared within 1 ulp, every other family bit for bit"
],
"forbidden_writers": [
"no per-function special case in the lowering; the only name-keyed rule is the SeparateFiles table, one entry, licence-justified",
"no second include resolver, no PowerShell or JS copy of the transformation",
"no GLSL in the Unity package; no precision statement in either library file",
"no edit to the pinned glslang or dxc hashes to make a download pass"
],
"shared_paths": [
"the same dotnet test run pins the C# mirror, both GLSL files and the fixture",
"compile-glsl.ps1 is the one wrapper writer for the MIT-only run, the two-file run and both negative controls"
],
"deletion_line": "The Phacelle block leaves CultMath.glsl by regeneration and the MPL sentence leaves the notices before the new file is added. Net: one more generated file and one switch; what it buys is an MIT CultMath.glsl the site can vendor without carrying MPL text, and the compile checks r1 could not run."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green on the merged branch; paste counts; GlslMirrorTests 4/4 with the per-family fixture line printed",
"Starfire: tools/get-glslang.ps1 installs 16.6.0 with the pinned SHA matching; tools/compile-glsl.ps1 exits 0 on the MIT-only wrapper and on -Phacelle; both negative controls fail with the error quoted",
"Starfire: tools/get-dxc.ps1 then dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0; FXC cs_5_0 still green",
"Browser pane WebGL2: the two-file wrapper compiles with an empty log"
],
"tests": [
{
"name": "GlslMirrorTests.CommittedGlslEqualsLowering",
"pins": "CultMath.glsl and CultMath.Phacelle.glsl are exactly the lowering of the committed HLSL; the regeneration path is the same code."
},
{
"name": "GlslMirrorTests.EveryPublicFunctionIsLowered",
"pins": "the union of both files equals the HLSL's cultmath_* set minus texture samplers; cultmath_phacelle and CultPhasor live only in the Phacelle file."
},
{
"name": "GlslMirrorTests.NoHlslOnlyTokensSurvive",
"pins": "no HLSL-only token in either file."
},
{
"name": "GlslMirrorTests.GoldenFixtureMatchesCSharp",
"pins": "every family is C#'s current output bit for bit, except iv_exp within 1 ulp of the platform exp; iv_frustum_ball is present and exact."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "no HLSL edit in this range changed a bit."
}
],
"negative": [
{
"pattern": "Mozilla|MPL|Johansen|CultPhasor|cultmath_phacelle\\(",
"scope": "packages/cultmath/shaders/CultMath.glsl: 0 matches; the header's file-name pointer to CultMath.Phacelle.glsl is the only mention of Phacelle."
},
{
"pattern": "^CultPhasor cultmath_phacelle\\(",
"scope": "packages/cultmath/shaders/CultMath.Phacelle.glsl: exactly 1 match."
},
{
"pattern": "CultMath\\.glsl|Phacelle\\.glsl",
"scope": "packages/cultmath/unity/**: 0 matches."
},
{
"pattern": "\\b(float|u?int|bool)[234]\\b|\\blerp\\(|\\bfrac\\(|\\bsaturate\\(|\\brsqrt\\(|\\basuint\\(|\\basfloat\\(|static const|#include|precision (high|medium|low)p",
"scope": "both GLSL files: 0 matches (the anchored regex r1 settled on)."
},
{
"pattern": "CultMath\\.glsl",
"scope": "packages/cultmath/THIRD-PARTY-NOTICES.md: 0 matches in the MPL-2.0 file list; CultMath.Phacelle.glsl appears there once."
},
{
"pattern": "git diff 3ac7250a..HEAD -- packages/cultmath/src",
"scope": "empty; the lowering and the fixture live in tests, so interval-ops r3's Stryker evidence (verdict s2) stands for this range."
}
],
"operator": []
},
"estimate": {
"lines_added": 320,
"lines_removed": 130,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [
"GLSL ES 3.00 library file (generated, MPL-2.0): shaders/CultMath.Phacelle.glsl"
],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-split-phacelle",
"cultmath-tapes:ruling:operator-compiler-downloads",
"cultmath-tapes:ruling:self-exp-measure-in-site",
"cultmath-tapes:ruling:operator-campaign-home"
],
"questions": []
}

View file

@ -0,0 +1,141 @@
{
"campaign": "cultmath-tapes",
"cut": "glsl-lowering",
"revision": 1,
"title": "GLSL lowering: shaders/CultMath.glsl is generated from CultMath.hlsl by one enumerated textual transformation, pinned by a test, compiled by glslang, with a committed golden fixture for WebGL2 parity",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-glsl-lowering",
"base": "c49c76ee",
"depends_on": [],
"first": [
"Map: docs/cultmath-interval-ground-cut.md, sections 'Where the interval math lives' (route C) and 'The authority map'. Model: the HLSL -> C# mirror in tests/CultMath.Tests/HlslSourceCompatibilityTests.cs (wrapper at :348) and its complete transformation list in packages/cultmath/docs/design.md:92-134. The GLSL lowering follows the same discipline: a complete, enumerated list; anything outside it fails the test rather than being patched around.",
"Read how HlslSourceCompatibilityTests resolves #include \"CultMath.Phacelle.hlsl\" (CultMath.hlsl:506); the GLSL lowering inlines the included file at that line, because GLSL has no #include.",
"Target dialect: GLSL ES 3.00 (#version 300 es), WebGL2. No implicit int/uint -> float conversion; step(vec4, float) and max(vec4, float) differ (max(genType, float) exists, step(genType, float) does not); C-style casts do not exist; lerp/frac/saturate/rsqrt/asuint are mix/fract/clamp(x,0.0,1.0)/inversesqrt/floatBitsToUint; floatN/intN are vecN/ivecN; static const is const. Mixed-signedness shifts are allowed.",
"Where the lowered text does not compile, fix the HLSL into the common subset with a legal, bit-identical HLSL edit (e.g. CultMath.hlsl:114 and :200 step(h, 0.0) -> step(h, float4(0.0, 0.0, 0.0, 0.0)); :471-473 and :502 (float)((uint)hash.x >> 8) before the float multiply). The HLSL -> C# mirror test and the FXC/dxc compiles keep those edits honest. Never special-case the lowering for one function.",
"Casts need a balanced-paren scanner, not a regex: (int)((state >> 28) + 4) at CultMath.hlsl:396 is the case that breaks a regex.",
"Dropped, as the C# mirror drops them: include guards (replaced by CULTMATH_GLSL guards), and the Texture2D/SamplerState functions at CultMath.hlsl:535-573. The GLSL declares no precision; the host shader does (precision highp float; precision highp int; the pcg hashes need 32-bit int).",
"Regeneration: the pin test writes shaders/CultMath.glsl when the environment variable CULTMATH_WRITE_GLSL=1 is set, then asserts equality. No new project, no PowerShell copy of the transformation.",
"Golden fixture: tests export 256 seeded points per function family (snoise float3/float2, snoise_grad, fbm_grad 4 octaves, ridged_grad, cellular, pcg3d/pcg4d, iv_* if present on main by then) with outputs as float32 bit patterns in hex, to tests/CultMath.Tests/fixtures/glsl-parity.json. JSON is tolerated here: the consumer is a browser-pane WebGL2 readback, a xenos boundary. The site cut evaluates it.",
"glslang: tools/get-glslang.ps1 fetches a pinned glslangValidator release the way tools/get-dxc.ps1 does; tools/compile-glsl.ps1 compiles a #version 300 es fragment wrapper that includes shaders/CultMath.glsl and calls every public function once. Run on Starfire, one job, detached with a log, polled in the foreground.",
"Verification on Yggdrasil: dotnet test from packages/cultmath/tests/CultMath.Tests; Stryker --since:c49c76ee from a plain clone (not a linked worktree: Stryker resolves --since paths against the main checkout). Never run verification as a background shell task."
],
"deletes": [],
"keeps_moves": [
"Both tracked CultMath.hlsl copies (packages/cultmath/shaders and unity/org.gamecult.cultmath/Shaders) stay one blob; the common-subset edits land in both.",
"The C# mirror test, the FXC probe and dxc -T lib_6_3 -HV 2021 all still pass on the edited HLSL; the Unity package does not ship the GLSL file.",
"design.md's HLSL -> C# transformation list (92-134) is unchanged; the GLSL list is a new sibling section."
],
"adds": [
"tests/CultMath.Tests/GlslLowering.cs: the HLSL -> GLSL ES 3.00 transformation, one static function string -> string, with the list as named steps in order: inline includes; drop guards and texture functions; type names; static const; casts via the balanced scanner; intrinsic renames (lerp, frac, saturate, rsqrt, asuint/asfloat); float literal passthrough; guards CULTMATH_GLSL added; a provenance header line naming the source file and that the file is generated.",
"tests/CultMath.Tests/GlslMirrorTests.cs: CommittedGlslEqualsLowering (writes when CULTMATH_WRITE_GLSL=1, prints a unified diff on failure); NoHlslOnlyTokensSurvive (regex over the output for float[234]\\(|int[234]\\(|lerp\\(|frac\\(|saturate\\(|static const|\\((float|int|uint)\\)|Texture2D|SamplerState -> 0 matches); EveryPublicFunctionIsLowered (the set of cultmath_* function names in the HLSL minus the texture ones equals the set in the GLSL); GoldenFixtureMatchesCSharp (regenerates the fixture in memory from C# and asserts equality with the committed file).",
"shaders/CultMath.glsl: the committed generated artifact.",
"tests/CultMath.Tests/fixtures/glsl-parity.json: the golden fixture (seed, points, per-function expected bit patterns, tolerance class per function: exact for integer-hash paths, ulp-bounded for transcendental).",
"tools/get-glslang.ps1 and tools/compile-glsl.ps1, mirroring get-dxc.ps1 / compile-hlsl-spirv.ps1 in shape.",
"packages/cultmath/docs/design.md: a section 'GLSL Target: Source Transformations' after :134 with the complete list, the common-subset rule for HLSL, the regeneration command, and that the GLSL is a derived file never edited by hand.",
"packages/cultmath/README.md: one paragraph that CultMath.glsl exists, what dialect it is, and how a WebGL consumer includes it (string concatenation before its own #version-led source; declare highp)."
],
"file_changes": [
{
"location": { "path": "packages/cultmath/shaders/CultMath.hlsl", "line": 114 },
"change": "step(h, 0.0) -> step(h, float4(0.0, 0.0, 0.0, 0.0)); same at :200. Bit-identical in HLSL and C#."
},
{
"location": { "path": "packages/cultmath/shaders/CultMath.hlsl", "line": 470 },
"change": "jitter components and the id at :502: an explicit (float) cast around the shifted uint before the float multiply, so the lowered text has no uint*float."
},
{
"location": { "path": "packages/cultmath/shaders/CultMath.Phacelle.hlsl", "line": 1 },
"change": "whatever common-subset edits the glslang compile demands; each one bit-identical, each one listed in the cut report."
},
{
"location": { "path": "packages/cultmath/docs/design.md", "line": 134 },
"change": "new section: GLSL Target: Source Transformations."
}
],
"authority_map": {
"owner": "GlslLowering (tests/CultMath.Tests) owns the HLSL -> GLSL transformation; shaders/CultMath.glsl is its derived output, pinned by GlslMirrorTests. CultMath.hlsl stays the one hand-written shader source.",
"inputs": [
"shaders/CultMath.hlsl and shaders/CultMath.Phacelle.hlsl text",
"C# math for the golden fixture values"
],
"outputs": [
"shaders/CultMath.glsl (GLSL ES 3.00 library, every public cultmath_* function except texture samplers)",
"tests/CultMath.Tests/fixtures/glsl-parity.json"
],
"derived_state": [
"CultMath.glsl is derived; a hand edit fails CommittedGlslEqualsLowering",
"the fixture is derived from C#; a stale fixture fails GoldenFixtureMatchesCSharp"
],
"forbidden_writers": [
"no per-function special case in the lowering; a dialect gap is fixed in the HLSL in the common subset",
"no second transformation implementation (no PowerShell or JS copy of the rules)",
"no GLSL in the Unity package; no precision statement in the library file"
],
"shared_paths": [
"both tracked CultMath.hlsl copies receive the same edits; the existing one-blob rule covers them",
"the same dotnet test run pins the C# mirror, the GLSL pin and the fixture"
],
"deletion_line": "Nothing is deleted in CultLib. The liability bought is one generated file, one test file, one fixture and two tool scripts; what it retires is every future hand copy of CultMath noise in a GLSL consumer, starting with the site."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green; paste counts",
"Yggdrasil: dotnet stryker --since:c49c76ee from a plain clone; triage every survivor by name",
"Starfire: tools/compile-glsl.ps1 exits 0 on the wrapper over shaders/CultMath.glsl; a negative-control wrapper with one lerp( call must fail (records that the compile check sees errors)",
"Starfire: FXC cs_5_0 and dxc -T lib_6_3 -HV 2021 still compile both HLSL copies after the common-subset edits"
],
"tests": [
{
"name": "GlslMirrorTests.CommittedGlslEqualsLowering",
"pins": "the committed GLSL is exactly the lowering of the committed HLSL; the regeneration path is the same code."
},
{
"name": "GlslMirrorTests.NoHlslOnlyTokensSurvive",
"pins": "the list is complete: no HLSL-only token survives in the output."
},
{
"name": "GlslMirrorTests.EveryPublicFunctionIsLowered",
"pins": "no function is silently dropped except the texture samplers."
},
{
"name": "GlslMirrorTests.GoldenFixtureMatchesCSharp",
"pins": "the fixture the site will read on WebGL2 is C#'s current truth."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "the common-subset edits changed no bit of any HLSL function."
}
],
"negative": [
{
"pattern": "lerp\\(|frac\\(|saturate\\(|float[234]\\(|int[234]\\(|static const|\\((float|int|uint)\\)|Texture2D|SamplerState|#include",
"scope": "packages/cultmath/shaders/CultMath.glsl: 0 matches."
},
{
"pattern": "precision (high|medium|low)p",
"scope": "packages/cultmath/shaders/CultMath.glsl: 0 matches (the host declares precision)."
},
{
"pattern": "CultMath\\.glsl",
"scope": "packages/cultmath/unity/**: 0 matches; the Unity package does not ship the GLSL."
},
{
"pattern": "git diff c49c76ee..HEAD -- packages/cultmath/shaders/CultMath.hlsl | grep '^[-+]' | grep -v '^[-+][-+]'",
"scope": "every changed HLSL line is listed in the cut report with the reason (dialect gap it closes); none changes a value, proven by the mirror test and the existing goldens."
}
],
"operator": []
},
"estimate": {
"lines_added": 1100,
"lines_removed": 8,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": ["GLSL ES 3.00 library file (generated)", "glsl-parity.json fixture"],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [],
"questions": []
}

View file

@ -0,0 +1,219 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 2,
"title": "Interval ops r2: envelope-then-noise fields, scenarios with authored empty space including the void, the tile march on cell ranges, iv_frustum_ball, wasted probes removed, dxc run",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "c9bd003b",
"depends_on": [],
"first": [
"Follow-up on cut report cultmath-tapes:cut_report:cut-interval-ops.h1 (head c9bd003b, worktree F:\\Projects\\CultLib-iv). Rulings in force: operator-tile-amortized-march (the bound stays; one probe per screen tile), operator-compiler-downloads. The operator's challenge, verbatim in the map's Pass 3 section: r1 measured a uniform noise slab with no empty space to skip, the worst case for intervals, and the density that matters is an analytic envelope (a pre-distortion SDF) modulated by noise, where the envelope's interval is tight and proves whole ray lengths empty in one probe. Map: docs/cultmath-interval-ground-cut.md, 'Pass 3'. r1's ops and bounds stay; this revision touches math.Interval.cs, CultMath.Interval.hlsl (both tracked copies), NoiseBoundTests.cs, design.md and README only.",
"The field, from Aetheria (Volumetric.cginc:165-221 and :91-102; Zone.cs:369-397 GetHeight, :426 PowerPulse; Settings.asset). SDF s(p) = p.y + h(p.xz), h >= 0 the well map: h = PowerPulse(|p.xz| / (2R), 2) * 64 (the zone bowl) + sum over wells of PowerPulse(|p.xz - c_b| / r_b, 16) * d_b, with PowerPulse(x, e) = (1 - 4x^2)^e for x in [0, 1/2] and 0 beyond, r_b = 500 M^0.25, d_b = 30 M^0.175 (Aetheria's GravityRadius/GravityDepth curves). Noise only where s < S: fade(s) = 1 - smoothstep(0.75 S, S, s); displaced height s' = s + A fade(s) n(p) with n the warped two-phase coverage noise plus half the detail octave (r1's field shape); density = max(0, (F - s') / B) (Aetheria's floor: FloorOffset F = -20, FloorBlend B = 10; the patch band and the 1e-9 fill are left out). Above the safety distance density is exactly 0, and so is the noise cost: Aetheria's dense march gates the noise pointwise (`if (dist < _SafetyDistance)`); the test's dense march does the same.",
"The composed bound over a tile's frustum slice [z0, z1], Envelope over the slice's exact box, not the ball: with slopes m in [m_c - s, m_c + s] per axis and z in [z0, z1], y = iv_mul([z0, z1], m_y) and x = iv_mul([z0, z1], m_x) (plus the tile's translation o); h's interval over the xz box is the sum over bowls of [pulse(far), pulse(near)], near and far the least and greatest distance from the box to the bowl's centre (pulse is monotone decreasing in distance, so each term is exact; the only slack is the sum). iv_s = iv_add(y, iv_h); iv_fade = the ordered endpoints of fade over iv_s (monotone); iv_n = iv_snoise_ball(ball.xyz * F0, ball.w * F0) with ball = iv_frustum_ball(..., warp = D); iv_s' = iv_add(iv_s, iv_scale(iv_mul(iv_fade, iv_n), A)); the slice is provably empty iff iv_s'.lo >= F. When iv_fade.hi == 0 the noise term is identically zero and iv_snoise_ball is not evaluated: the envelope alone proves the slice empty at no snoise cost. That short-circuit is what tiles compound.",
"Where the warp lives in the bound. Aetheria warps only the noise argument (triNoise3d(pos + fl * ...)); the surface height is read at the unwarped xz and the displacement is applied to y afterwards. So the envelope is evaluated on the pre-distortion SDF over the unwarped slice (no D), and only the noise ball is enlarged by D (iv_frustum_ball's warp argument). A warp applied to the envelope's argument would be unsound without enlarging the box by D; the field is defined so that it is not. The test EnvelopeBoundEnclosesDensity pins this: 2,000 seeded slices x 64 warped points, the composed bound encloses density(p) with no tolerance (every op is monotone or already widened in r1; if a seeded case fails at an endpoint, widen in the implementation, never the test).",
"iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp) -> float4(centre, radius), in C# and both HLSL copies, is the one new CultMath function: the noise ball of a tile over a depth segment. Camera: pinhole at the origin looking +z, focal length f pixels; a pixel's ray is p(z') = (m z', z'); a tile's slopes lie in m_c + [-N/(2f), N/(2f)]^2 (full footprint, so sub-pixel jitter is covered), s = N / (sqrt(2) f). Centre c = (m_c z_m, z_m), z_m = (z0 + z1) / 2; radius r = ((z1 - z0) / 2) |(m_c, 1)| + z1 s + warp. Proof: a point of slope m_c + e (|e| <= s) at depth z' is c + (z' - z_m)(m_c, 1) + z'(e, 0), the terms bounded by the two radius terms, and the warp moves it by at most D (WarpedPointsStayEnclosed). The slice is a convex polytope, so every point between rays is enclosed too. The two vectors are never parallel, so the inequality is strict and float32 cannot cross it.",
"(Tile ball, continued.) Test TileBallEnclosesEveryRaySegment: 2,000 seeded tiles (N in {1, 4, 8, 16}, f in [500, 3000], |m_c| <= 1.1, segments across each scenario's z range), 64 points each with sub-pixel offsets, both depth ends and a flow of length <= warp; no tolerance. Hand mutations that must die, listed in the file header: z0 for z1 in the lateral term; |m_c| for |(m_c, 1)|; the warp dropped. A moving camera needs nothing more: every frame's pre-pass evaluates iv_frustum_ball from that frame's camera and never carries a ball, a mask or a probe result across frames; the only cross-frame state is the TAA history, which is colour, not a soundness input. The test says so by drawing each tile's camera independently.",
"Implementation review of r1's march (NoiseBoundTests.cs:344-376), each item fixed here: (1) every probe was charged one snoise per ray; the tile shares it (ruling). (2) Coarse probes were attempted where the ball could never prove emptiness: iv_snoise_ball's hi is n(c) + L r clamped to 1, so a probe with L r F0 >= 1 + cutoff is a guaranteed failure; at r1's StepMax = 2 that is L r F0 = 3.7, so every coarse probe was a wasted evaluation (the 0.74x row). r2 attempts a noise probe only when L * radius * F0 < 1 - (the cutoff the probe needs), otherwise the level is 'maybe' at no cost. (3) Growth was capped at StepMax = 8 Step0, so a ray through clear space paid Depth / StepMax probes; r2 grows to the remaining range (the hierarchy is over cell-index ranges, halving on maybe, doubling on skip, capped only by the grid's end).",
"(Review, continued.) (4) The dense sample evaluated the detail octave even where occupancy was 0; r2's dense sample evaluates coverage first and the detail only when the envelope and coverage leave density > 0, as Aetheria's `if (dist < _SafetyDistance)` does; the dense baseline gets cheaper and the comparison honest. (5) The finest-level probe before integrating a cell stays: amortized over the tile it costs 1/N^2 and can skip 3 snoise per ray. (6) The step never regrew after a dense cell until a skip; with (3) it regrows from the next skip, which is enough.",
"The march, on cell ranges. The dense grid is a scenario-owned list of cell boundaries z_0 < ... < z_J (uniform for the slab scenarios; Aetheria's quadratic spacing z_j = near + (j/J)^2 (far - near), J = 256, for the Aetheria-like ones, from CloudShader.shader:116). MarchTile takes a range [j0, j1): the envelope probe over the slice (z_j0, z_j1); if empty, skip and double the next range; else if the noise probe is affordable (item 2) and proves empty, skip; else if the range is more than one cell, halve; else the cell is dense. It runs to the grid's end once per tile (a pre-pass does not know any ray's transmittance) and yields the dense cells; each of the N^2 rays integrates exactly those cells in order with the dense march's own Integrate and early-out at 0.02. Counted separately: snoise evaluations and envelope evaluations (pointwise in the dense march; interval probes in the tile march), per ray with probes divided by N^2.",
"The cost model, printed and reasoned about in snoise units: a pointwise envelope evaluation or an envelope probe costs e = 0.2 (a handful of pulses or a height-map fetch against ~150 flops of snoise; stated as an estimate, both counts printed so any weight can be applied). Per scenario the test prints: dense snoise and envelope per ray, tile snoise and envelope probes per ray, the combined cost ratio, and the probe-free ceiling (dense cost over the cost of the dense cells alone). The 2x contract is on the combined cost at N = 8 for scenarios (a) and (c); (b) and (d) are printed as the worst cases and not asserted.",
"Scenarios (IV-REPORT lines, each with 64 tiles at N in {1, 4, 8, 16}, warp in {0, D}): (a) height fog: h = 0, camera at y = S + 30, rays with slope m_y in [0, 0.5] (level and up), Aetheria's grid, expected near-total skip (0 snoise either way; the envelope count carries the ratio). (b) inside the fog: h = 0, camera at y = F - 10, rays level, expected near 1x: every ray saturates within a few cells and the pre-pass still probes the whole grid. (c) Aetheria-like: the zone bowl R = 2000 and four wells of mass {100, 1000, 10000, 1000} at seeded positions inside R, camera at a seeded xz in the bowl at y = max(0, S + 10 - h(camera)), rays gazing across with m_y in [-0.25, 0.1], Aetheria's units (noise scale 414 so F0 = 1/414, D = 15 * 8 / 2 = 60, A = 20 with snoise in [-1, 1]), Aetheria's grid. (d) the uniform slab: r1's field and parameters, kept as the reported worst case. The headline is (e) at N = 8, 200 tiles, printed with both its numbers; (a) and (c) are asserted.",
"(e) the void, the shipped scene (rulings operator-ground-void-brush, operator-void-true-cavity; site-ground r3): a cloud volume everywhere, carved by one negative pseudo-Gaussian sphere, parametrised by the hollow radius Rh (density exactly 0 inside) and the ramp width S (density rises to full over [Rh, Rh + S]); the brush (1 - (d / Rc)^2)^e with CARVE gives Rh = Rc sqrt(1 - CARVE^(-1/e)) and S from the exponent, so (Rh, S) are the authored knobs and (Rc, e) derived. Noise displaces the wall only: fade(d) = smoothstep(Rh - S, Rh, d); d' = d + A fade(d) n(warp(p)), A = 20, F0 = 0.01 (wavelengths 100 and 25), D = 10; K = 1/30 (optical depth 1 per 30 units of full density), termination at T < 0.02. Shipped point: Rh = 198, S = 50, the sun on the hollow's bottom, the camera 120 behind it along the orbit tangent and 45 above it (186 from the centre, 'low'). Rays over the whole frustum (vertical FOV 60, f = 935, so theta_px = 1/f). Every ray crosses empty interior and ends in a wall.",
"Footprint LOD (the operator's challenge, map 'Footprint LOD'): theta_px = 2 tan(fov/2) / H; the footprint at distance z is z theta_px. Step: Delta(z) = max(k z theta_px, min(S / 4, lambda_fine(z) / 2)), k = 4: never finer than four footprints, otherwise the finer of the two content limits (four samples across the ramp, two per wavelength of the finest live octave). Octaves: octave i of wavelength lambda_i carries weight w_i(z) = 1 - smoothstep(lambda_i / (2m), lambda_i / m, z theta_px), m = 4: one smooth blend per octave, no popping; TAA hides per-frame sparsity. The weights are part of the field, so the bound encloses the truncated field exactly: the noise interval over a slice is the sum over octaves of iv_mul([w_i(z1), w_i(z0)], iv_snoise_ball(c F_i, r F_i)), w_i monotone in z. At the shipped parameters (wavelengths 100 and 25, z <= 2 Rc) every weight is 1 in the frustum and the content limits set the step; the footprint bites when S < 4 k z theta_px.",
"(e) runs three marches per tile and prints them: the fixed-step reference (Delta_fixed = min(10, S / 4) everywhere, the pointwise envelope gate, the baseline a march that does not know where the wall is must pay); the masked fixed-step march (the same cells, so transmittance agrees exactly: the soundness pin, as (a)-(d)); and the masked LOD march, the shipped one (steps from Delta(z) inside the unmasked range only, octave weights applied), whose transmittance must be within 0.02 of the reference (printed; an approximation by design, not a soundness claim). Printed per configuration: steps per pixel and snoise per pixel for all three, the combined cost ratio reference / LOD, probes per tile. The grid for (e): Rh in {100, 200, 400, 1000} x camera offset in {centred, mid (0.5 Rh), low (0.94 Rh, the shipped)} x S in {10, 50, 150}, 32 tiles each at N = 8; the shipped point with 200 tiles.",
"Predictions to confirm or refute, N = 8, combined cost: (a) above 50x. (b) 0.8-1.0x. (c) 2-4x: clear cells collapse to a few probes per tile, fog cells cost the same on both sides; the noise ball proves nothing at Aetheria's scale (L r F0 >= 1.6), so item 2 keeps it unevaluated and the saving is the envelope's. (d) 1.3-1.5x: the ceiling at L = 10.099261. (e), from Imagination's model (scratchpad cavity_model.py): the LOD march needs about 6 steps and 12 snoise per pixel at every grid point with S <= 50 (the content limits S / 4 and the detail half-wavelength 12.5 set the step, not the footprint); S = 150 needs 14 steps and 36 snoise. Against the fixed-step reference at the shipped point: steps 35 to 6 (5-8x fewer), combined cost 1.7x, snoise alone 1.25x; cost 3.2x at S = 10, 1.4x at S = 150, 2.5x at Rh = 1000; camera offset barely matters. (e)'s contract is steps per pixel (Q8 retired). If (a) or (c) misses 2x, skip naming saving-2x-scenarios. Never tune the fields, the grids or e.",
"Stryker on Yggdrasil from a plain clone: --since:c9bd003b -m math.Interval.cs; report timeouts separately from kills (a timeout is not a demonstrated kill); if the saving test is what times mutants out, exclude it from Stryker's test selection (a measurement, not a pin) rather than raising the timeout. Compile checks on Starfire, one job, detached with a log, polled in the foreground: FXC cs_5_0 over every cultmath_iv_* including the new function, both tracked copies; then tools/get-dxc.ps1 (pinned, ruling operator-compiler-downloads) and dxc -T lib_6_3 -HV 2021 on both copies. Carried from r1 and still out of scope, name them in the report: the Unity CultMath.dll is not rebuilt; the Unity HLSL copies stay one blob with packages/cultmath/shaders; public asfloat(uint) stays. Branch from c9bd003b; if main moved by docs only, say so as r1 did."
],
"deletes": [
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 33,
"note": ":344-376 MarchIntervals, the per-ray adaptive march with its StepMax cap and unconditional probes, replaced by MarchTile on cell ranges."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 10,
"note": ":401-408 the doc comment naming the r1 fork and :424-425 the Assert.Skip naming it; the fork is answered. A skip survives only on a shortfall in (a) or (c), naming saving-2x-scenarios."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 12,
"note": ":319-330 Density: the unconditional three-snoise sample is replaced by the envelope-gated sample (coverage first, detail only when density can be nonzero)."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 15,
"note": ":423-437 the saving paragraph with r1's numbers (0.93x, 1.83x, 0.74x), replaced by the scenario paragraph with r2's printed numbers."
}
],
"keeps_moves": [
"Every iv_* op, both ball bounds, SNOISE_LIPSCHITZ and its provenance tests: untouched. The ruling keeps the bound; only the field model, the march and its test change.",
"SavingField's noise shape (F0 0.35 in the slab, the warped two-phase coverage, the detail octave, period 4) and (d)'s parameters: kept, as the worst case; scenario (d) must reproduce r1's dense count where the detail gating does not bite (print it).",
"MarchDense and Integrate: unchanged except that Density is envelope-gated; the dense grid becomes the scenario's boundary list.",
"IntervalTests, WarpedPointsStayEnclosed, TightnessReport, MeasureLipschitz, LipschitzConstantPinsSampledGradients: unchanged.",
"HlslSourceCompatibilityTests: no change expected; the new function must find its C# twin through the existing mirror."
],
"adds": [
"packages/cultmath/src/CultMath/math.Interval.cs: iv_frustum_ball after iv_fbm_ball, doc comment carrying the derivation and that the caller scales by F0 and adds its translation.",
"packages/cultmath/shaders/CultMath.Interval.hlsl: cultmath_iv_frustum_ball, same text in both tracked copies.",
"tests/CultMath.Tests/NoiseBoundTests.cs: Scenario (grid, wells, camera, ray slopes, units), SavingField with the envelope (PowerPulse wells, fade, floor), EnvelopeBound (the composed interval, test-side; iv_* ops only), MarchTile, MarchLod (footprint step and octave weights, the shipped march), the per-tile Run; TileBallEnclosesEveryRaySegment; EnvelopeBoundEnclosesDensity; IntervalSkipHalvesEvaluations prints the scenario x N x warp grid and asserts (a) and (c) at N = 8.",
"packages/cultmath/docs/design.md, 'Intervals': the frustum-ball paragraph after :401; the envelope-then-noise paragraph (how a consumer composes the bound, the short-circuit, where the warp lives); the rewritten saving paragraph with the four scenarios and the headline.",
"packages/cultmath/README.md :38-43: iv_frustum_ball named; one sentence that the bounds compose with an analytic envelope."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/src/CultMath/math.Interval.cs",
"line": 152
},
"change": "after iv_fbm_ball: public static float4 iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp)."
},
{
"location": {
"path": "packages/cultmath/shaders/CultMath.Interval.hlsl",
"line": 91
},
"change": "after cultmath_iv_fbm_ball: float4 cultmath_iv_frustum_ball(...); both tracked copies."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 294
},
"change": "SavingField becomes envelope-then-noise over a Scenario; Density is envelope-gated; MarchTile replaces MarchIntervals; Run draws tiles and N^2 rays each, counting snoise and envelope evaluations separately."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 409
},
"change": "IntervalSkipHalvesEvaluations: scenarios (a)-(e), N in {1, 4, 8, 16}, warp in {0, D}; the (e) grid Rh x offset x S with three marches; assert >= 2x combined cost for (a) and (c) and >= 2x fewer steps per pixel for (e) at the shipped point, else Skip naming saving-2x-scenarios."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 89
},
"change": "TileBallEnclosesEveryRaySegment and EnvelopeBoundEnclosesDensity beside WarpedPointsStayEnclosed."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 401
},
"change": "the frustum-ball and envelope-composition paragraphs; :423-437 the saving paragraph rewritten with r2's printed numbers only."
},
{
"location": {
"path": "packages/cultmath/README.md",
"line": 38
},
"change": "iv_frustum_ball named; the envelope composition in one sentence."
}
],
"authority_map": {
"owner": "CultMath owns the interval ops, the noise bounds and now the frustum ball; a consumer (the test's SavingField, the site's pre-pass, Aetheria's march) owns its envelope and composes the bound from iv_* ops. The saving test owns the proving-ground numbers per scenario; the site's browser measurement owns the shipped gain. The bound (SNOISE_LIPSCHITZ, iv_snoise_ball) is unchanged.",
"inputs": [
"the r1 interval surface, unchanged",
"a tile's geometry (centre slope, depth segment, footprint per depth, warp bound) and a scenario's grid, wells, camera and units; Aetheria's shapes and settings as read from its checkout, cited by file:line in the test"
],
"outputs": [
"iv_frustum_ball / cultmath_iv_frustum_ball: a ball enclosing every point of every ray of the tile over the segment, warped",
"printed numbers per scenario: dense and tile snoise and envelope counts per ray, combined cost ratio, probe-free ceiling, at N in {1, 4, 8, 16}"
],
"derived_state": [
"the tile ball and the envelope box are derived from the tile's geometry; no radius is read from a texture, a uniform or a tuned constant",
"whether a noise probe is attempted is derived from L, the radius and the cutoff (item 2), never from a mode flag",
"the probe-free ceiling is derived from the same run; it states what no amortization can beat"
],
"forbidden_writers": [
"no change to SNOISE_LIPSCHITZ, iv_snoise_ball, the noise shape or e; the scenarios may not be tuned toward 2x",
"no per-ray probe in the tile march; no early-out of the pre-pass on any ray's transmittance",
"no warp on the envelope's argument; the warp enlarges the noise ball only",
"no new CultMath function for the envelope: PowerPulse, the box and the composition are consumer-side, built from iv_* ops",
"no tape, prune, affine or choice token in the interval files; no number in design.md or a comment that the committed test did not print in this run"
],
"shared_paths": [
"the dense march and the tile march integrate the same cells of the same grid with the same Integrate; the transmittance check is the proof that nothing provably empty was there",
"the one composed bound (EnvelopeBound) serves the enclosure test and the march; no second composition",
"the existing mirror test covers the new HLSL function; the Unity copy stays one blob"
],
"deletion_line": "r1's per-ray march, its unconditional probes, its growth cap and the unconditional three-snoise dense sample go first; the tile march on cell ranges and the gated sample are written in their place. Nothing in src is deleted. The liability added is one geometry function and two tests plus the scenario model; it buys the tile-march soundness invariant in three runtimes and a saving number measured where there is empty space to find."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green (MeasureLipschitz/MeasureMonotonicity excluded by trait); paste counts and every IV-REPORT line",
"Yggdrasil: dotnet stryker --since:c9bd003b -m math.Interval.cs from a plain clone; 0 survivors; timeouts listed separately from kills",
"Starfire: FXC cs_5_0 kernel over every cultmath_iv_* including cultmath_iv_frustum_ball, both HLSL copies",
"Starfire: tools/get-dxc.ps1 then dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0"
],
"tests": [
{
"name": "NoiseBoundTests.TileBallEnclosesEveryRaySegment",
"pins": "every sample point of every ray of an N x N tile over a depth segment, sub-pixel jitter and warp included, lies inside iv_frustum_ball."
},
{
"name": "NoiseBoundTests.EnvelopeBoundEnclosesDensity",
"pins": "the composed bound (envelope box, fade, noise ball enlarged by the warp) encloses density at every warped point of 2,000 slices, no tolerance; intervals-enclose holds for the composition the site and Aetheria will use."
},
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "scenarios (a) and (c) at N = 8 cost at most half the dense combined cost with transmittance within 1e-3; (e) at the shipped point: the masked LOD march takes at most half the fixed-step reference's steps per pixel, the masked fixed-step march agrees exactly, the LOD march within 0.02; the (e) grid and every scenario, N and warp printed with both counts and the ceiling."
},
{
"name": "IntervalTests.EveryOpEnclosesItsPoints (existing)",
"pins": "r1's enclosure for every op still holds."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "cultmath_iv_frustum_ball matches iv_frustum_ball bit for bit."
}
],
"negative": [
{
"pattern": "tape|prune|affine|choice",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches (case-insensitive)."
},
{
"pattern": "snoise_grad|fbm_grad",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches."
},
{
"pattern": "PowerPulse|pulse|envelope|Scenario",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches; the envelope is consumer-side."
},
{
"pattern": "Assert\\.Skip",
"scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios; 0 if (a), (c) and (e) meet their contracts."
},
{
"pattern": "10\\.099261|0\\.35f",
"scope": "git diff c9bd003b..HEAD -- packages/cultmath/src: the constant is not touched; the slab's F0 stays in (d)."
},
{
"pattern": "git diff --name-only c9bd003b..HEAD",
"scope": "only packages/cultmath/**; both tracked CultMath.Interval.hlsl copies change together."
}
],
"operator": []
},
"estimate": {
"lines_added": 420,
"lines_removed": 80,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-ground-void-brush",
"cultmath-tapes:ruling:operator-void-true-cavity",
"cultmath-tapes:ruling:operator-compiler-downloads",
"cultmath-tapes:ruling:operator-campaign-home"
],
"questions": []
}

View file

@ -0,0 +1,268 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 3,
"title": "Interval ops r3: the frustum ball widened by its derived float32 bound, the analytic constant-density body, one shipped void, (c) held to its ceiling, LOD weights exercised, Stryker kills demonstrated",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "43183276",
"depends_on": [],
"first": [
"Follow-up on cut report cultmath-tapes:cut_report:cut-interval-ops.h2 (head 43183276, worktree F:\\Projects\\CultLib-iv) and Soul's verdict cultmath-tapes:verdict:cut-interval-ops.s1 with its five findings, each resolved below in order. New rulings in force: operator-wells-contract-vs-ceiling ((c)'s contract is at least 90% of the probe-free ceiling, not 2x) and operator-analytic-wall-body (where the envelope interval proves the density constant, one closed-form Beer-Lambert step). Map: docs/cultmath-interval-ground-cut.md, 'Pass 4'. r2's ops, field models, tile march and scenarios stand; this revision touches math.Interval.cs, both tracked CultMath.Interval.hlsl copies, NoiseBoundTests.cs, SnoiseBall tests in the same file, design.md and README only. Nothing new in src beyond the radius widening.",
"Finding 1, frustum-ball-float-crossing (Medium). The ball is widened, in C# and both HLSL copies: radius += (|c.x| + |c.y| + |c.z| + radius) * 2^-20 (the literal 9.5367431640625e-7, exact in float32), after the r2 radius. Derivation, u = 2^-24: the centre c = (m_c z_m, z_m) carries at most two roundings per component (z_m = fl(z0 + z1) * 0.5, then the product), so |c_float - c_exact| <= 2u |c|_1; the radius carries at most seven first-order roundings (z1 - z0; the axis sqrt at 3u; the product; z1 * footprint; the sum; + warp), so r_exact <= r_float (1 + 7u); the widening's own addition rounds once more. 2^-20 = 16u covers 8u (|c|_1 + r) twice over. fma contraction removes roundings and only tightens, so dxc's HLSL and any driver's GLSL stay covered. The sentence 'the two vectors are never parallel, so the inequality is strict and float32 cannot cross it' leaves math.Interval.cs:179-180, the HLSL and design.md:412; the rule and its derivation take its place.",
"(Finding 1, continued.) TileBallEnclosesEveryRaySegment gains Soul's extreme families beside the r2 domain, 2,000 cases each, 8 footprint corners at both depth ends with the flow pointed outward along the corner's direction from the centre, the distance computed in double from the float ball and the float ray (point (m z, z) formed in double from the float m and z), no tolerance: z0 == z1; thin (z1 - z0 = 1e-6 z1); |m_c| up to 1000; far (z up to 1e7); z0 = 0 with z1 down to 1e-3; f down to 1; warp up to 1e6. The derivation pin becomes two-sided: r_exact <= ball.w <= r_exact + 2^-19 (r_exact + |c|_1), r_exact in double, so a ball that forgets the widening fails and a ball that grows past the rule fails. The hand mutations of r2 stay; a fourth, the widening dropped, must fail the degenerate family alone. WarpedPointsStayEnclosed is unchanged.",
"Finding 2, aetheria-far-plane-miscited (Low). FogField's grid becomes Quadratic(0.3, 2048, 256), cited as the Main Camera (Aetheria d3075eb0 Assets/Scenes/ARPG.unity:31777 m_Name Main Camera, :31830 far clip plane 2048; VolumeCloudRenderer.cs:36 RequireComponent(Camera)); the comment at NoiseBoundTests.cs:594 no longer names the Tint camera's 1000. Soul re-measured (c) at far 2048: 1.39-1.51x at warp D, 2.46-2.74x at warp 0, ceiling within 1-2% of the ratio; the verdict on (c) does not move. Print every (a)-(c) row again at 2048.",
"Finding 3, shipped-void-two-scenes (Medium). ONE shipped void, authored as (Rh, S) per the map's own rule, everything else derived; the map, the site-ground r3 spec and the test agree on it: Rh = 198, S = 50, so Rc = Rh + S = 248 and e = -ln CARVE / ln(1 - (Rh / Rc)^2) = 0.40 (CARVE 1.5); fade band [Rh - S, Rh], A = 20, F0 = 0.01, D = 10, K = 1/30. This is the scene r2 measured (2.21x) and the Footprint-LOD rule's; the map's site section (Rc 640, e 4, a 442-wide ramp) was the other scene and is superseded. The honest choice changes the look (a 50-wide noisy wall with a kinked outer edge, not a soft pseudo-Gaussian wall), so it is Q9 in the map, a fork for the operator with (a) recommended. Hands codes (a); another ruling moves only the shipped constants and the site section. VoidField's doc comment derives (Rc, e) from (Rh, S) and names it the one shipped void.",
"Finding 4, lod-weights-never-exercised (Low). VoidField takes a focal length. One configuration reaches the weights: '(e) LOD far' Rh = 4000, S = 50, camera low, f = 467 (1080 rows at a 98 degree vertical FOV): theta = 1/467, so the fine weight (wavelength 25) is below 1 past z = 1460 and 0 past 2920, the coarse weight (100) below 1 past 5840, and the grid (|camera - c_v| + Rc + 6/K, about 7990) reaches both; 4 z theta exceeds 12.5 past z = 1460, so LodStep is footprint-set there too. It joins EnvelopeBoundEnclosesDensity's field list (the truncated check is no longer vacuous) and the saving test prints it, not asserted. Hand mutation that must fail the truncated check on it: w(z0) for w(z1) in WeightBound.",
"(Finding 4, continued: the deviation.) Recorded in the test's doc and design.md: the bound takes each octave weight over [w(z1), 1], not the spec's [w(z1), w(z0)], because one Bound serves the full field (w = 1) and the truncated one (w in [w(z1), w(z0)], w decreasing in z). [w(z1), 1] contains both, iv_mul is monotone in its interval argument, so the composed interval is a superset of either field's and encloses both. The slack is only in far tiles whose step the footprint already sets.",
"Finding 5, stryker-timeout-survivor (Low). Mutant 2525 (iv_sqr a.x >= 0 to a.x > 0) is equivalent: at a.x == 0 both branches return the same ends ((0, h) either way, (0, 0) when a.y == 0); the report says so by its diff, since numbers are per run. New enclosure cases kill the mirror-only survivors semantically: SnoiseBallEnclosesPoints and FbmBallEnclosesPoints draw radii LogUniform(1e-3, 1e3), so L / r in iv_snoise_ball (too tight once r > 1: at r = 100 the mutant's bound is 0.2 wide while snoise over the ball spans [-1, 1]) dies to an enclosure test, not only to the HLSL mirror. The mutants Soul listed as mirror-only (iv_scale s >= 0 to s > 0; the two iv_abs mutants) are hand-run and each either named equivalent with the one-line argument (signed zero or a boundary both branches map alike) or killed by one added point case in IntervalTests.",
"(Finding 5, continued.) Stryker runs again as r2 did, --since:43183276; every timeout in math.Interval.cs is hand-run (patch from the mutation report, dotnet test minus IntervalSkipHalvesEvaluations) and the report lists killed, survived and equivalent by diff. The report claims only demonstrated kills; a timeout is not a kill.",
"The analytic body (ruling operator-analytic-wall-body), consumer-side in the test and the site, no new CultMath function. When it applies: the Scenario's Bound over the remaining range [z, z_end] of the grid, gated, is a point interval with positive value: the envelope with the free noise interval [-1.5, 1.5] (coverage in [-1, 1] plus half the detail) proves fade = 1 and d_lo + A (fade n)_lo >= Rc, so every point of every ray of the tile over the rest of the grid has density exactly K for any noise value; or, generically, lo == hi > 0 from whichever path the bound took. Only then does the march integrate T *= exp(-K ext (z_end - z) |dir|) in one step and terminate (the grid end is past opacity by construction). For (a)-(d) no Bound is a positive point (the fog's density varies with s'), so the step never applies there, and the test asserts bodyStart = -1 for every (a)-(d) tile.",
"(Analytic body, continued.) MarchTile returns (dense cells, bodyStart, bodyDensity). As r2 on cell ranges; when a single cell is found not provably empty and its own gated bound is a positive point (the cell is provably constant), one more probe over [grid[j], grid[^1]]: if that is a positive point too, bodyStart = j, bodyDensity = its value, and every remaining cell is appended dense without probing (adding cells is always sound; it also drops the body's probes). The mask's meaning is unchanged, so the masked fixed-step march's exact agreement with the reference stands as the soundness pin. MarchLod: inside unmasked runs as r2; on reaching a cell >= bodyStart it takes the one analytic step over [z, grid end], counts one step, and stops.",
"(Analytic body, the grid end.) Every void configuration's grid ends at |camera - c_v| + Rc + 6/K (was 5/K): the body starts at d >= Rc + A 1.5 = Rc + 30 plus box slack, so 6/K leaves at least 4.9 of optical depth, T <= 0.0075 < 0.02, and 'unfinished rays 0' still prints. This is the grid end, not the field; the reference stops by opacity and its count does not move.",
"(Analytic body, the pin.) New test AnalyticBodyMatchesFineMarch over the (e) grid, the shipped void and the LOD-far configuration, 2,000 seeded tiles at N in {1, 4, 8, 16}: (1) wherever MarchTile reports bodyStart b: the Bound over [grid[b], grid end] is a positive point; the full and the truncated Density at 64 random (pixel, sub-pixel, cell >= b, depth in cell) points equal bodyDensity exactly, no tolerance; (2) the analytic factor exp(-bodyDensity ext L) over the body equals the fixed-step product of the body cells' Integrate factors (no early-out, same cells) within 1e-4 relative; (3) for every (a)-(d) tile bodyStart = -1. Hand mutation that must fail (1): in VoidField.Compose, CarveRadius for HollowRadius in the lo test (the body claimed from the ramp). The site's mirror of this (site-ground r3): the pre-pass writes the body-start cell in the texel's fourth word, the main pass takes the same one step; its browser check is the dense-reference agreement already in that spec.",
"Contracts after r3, asserted at N = 8 in IntervalSkipHalvesEvaluations, else one Assert.Skip naming saving-2x-scenarios with every shortfall: (a) at least 2x combined cost (the lower of warp 0 and D), measured 16384x. (c) at least 0.90 of its probe-free ceiling (Ratio / Ceiling, the lower of warp 0 and D), measured 0.99 at warp D and 0.96 at warp 0 in r2; the ruling's reading is that the ceiling is what intervals control and the fog band's cost is the field's. (e) at least 2x fewer LOD steps per pixel than the fixed-step reference at the ONE shipped void (200 tiles), the masked fixed-step march agreeing exactly and the LOD march within 0.02 of the reference. (b), (d) and the (e) grid incl. LOD-far: printed, not asserted. Nothing is tuned toward any contract: the fields, the grids (beyond the stated 6/K end), e = 0.2 and the seeds stay.",
"Predictions to confirm or refute (Imagination's rule from r2's printed rows: the LOD steps that evaluated noise are about snoise / 2.8 plus two pre-band steps; the rest were constant-body steps, which the analytic step makes one). Shipped void: steps per pixel 36.34 (ref) to about 12.5 (was 16.45), 2.6-3.3x fewer; snoise per pixel about 26.6, unchanged; combined cost about 1.38x (was 1.34x); envelope probes per tile about 50 (was 59.1). Grid, low camera, ref/LOD steps: S = 10 about 5-6x at Rh 100-400 and 11x at Rh 1000 (was 2.1-3.4x and 6.8x); S = 50 about 2.5x, 2.8x, 3.5x, 5.2x for Rh 100, 200, 400, 1000 (was 1.8, 2.2, 2.9, 4.5); S = 150 unchanged (rays end inside the 150-wide ramp before the body is proven). Rh 1000 centred S 10: about 30x (was 12.4x). LOD-far: printed, none predicted. If the shipped point lands under 2.6x, say which assumption failed: the pre-band's 0-snoise steps, or a body start later than Rc + 30 by box slack.",
"Compile and mutation checks as r2: Yggdrasil dotnet test (counts and every IV-REPORT line pasted); Stryker --since:43183276 -m math.Interval.cs from a plain clone with every timeout hand-run as in Finding 5; Starfire FXC cs_5_0 over every cultmath_iv_* on both HLSL copies and tools/get-dxc.ps1 then dxc -T lib_6_3 -HV 2021 on both, exit 0 (Soul could not re-run these for r2; they are re-run here because the HLSL changes). Carried and still out of scope, named in the report: the Unity CultMath.dll is not rebuilt; the Unity HLSL copies stay one blob with packages/cultmath/shaders; public asfloat(uint) stays. Main moved by docs only since 43183276's base (Imagination passes 3-3d and 4); say so. glsl-lowering r2 rebases onto this revision's head, not r2's (its fixture family for cultmath_iv_frustum_ball changes with the widening); its spec now says so."
],
"deletes": [
{
"path": "packages/cultmath/src/CultMath/math.Interval.cs",
"lines": 2,
"note": ":179-180 the sentence 'The two vectors are never parallel, so the triangle inequality is strict' and its float32 claim; replaced by the widening rule and its derivation."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 1,
"note": ":412 'the two vectors are never parallel, so the bound is strict'; replaced by the rule."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 1,
"note": ":594 the Tint camera citation (ARPG.unity:635-636, far 1000); replaced by the Main Camera's :31830, far 2048."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 3,
"note": ":1181-1194 the 2x test on ratios[c] and the skip message's wording; replaced by the 0.90-of-ceiling test and a message naming each shortfall."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 12,
"note": ":461-497 the r2 numbers (1.41x, 16.45, 2.21x, the (c) shortfall paragraph); replaced by r3's printed numbers and the three contracts."
}
],
"keeps_moves": [
"Every iv_* op, iv_snoise_ball, iv_fbm_ball, SNOISE_LIPSCHITZ and its provenance tests: untouched. iv_frustum_ball's centre and the r2 radius terms: untouched; only the widening term is appended.",
"FogField, SlabField, WarpedNoise, Compose, the gated probe (BestBall), MarchTile's range hierarchy, the masked fixed-step march and Run: kept; MarchTile gains the body return and MarchLod the one analytic step.",
"VoidField's field, Density, Bound, Weight, WeightBound and LodStep: kept; the constructor gains the focal length and the grid end moves to 6/K; the shipped point stays Shipped() = (198, 50, shipped camera).",
"TileBallEnclosesEveryRaySegment's r2 domain, EnvelopeBoundEnclosesDensity, IntervalTests, TightnessReport, MeasureLipschitz, LipschitzConstantPinsSampledGradients: kept; the first two gain cases.",
"HlslSourceCompatibilityTests: the mirror count stays 53; cultmath_iv_frustum_ball's new text must match the C# bit for bit."
],
"adds": [
"math.Interval.cs and both CultMath.Interval.hlsl: the widening line in iv_frustum_ball / cultmath_iv_frustum_ball, and the rule in the doc comment.",
"NoiseBoundTests.cs: the extreme families and the two-sided derivation pin in TileBallEnclosesEveryRaySegment; VoidField focal parameter and the LOD-far configuration; MarchTile's (bodyStart, bodyDensity); MarchLod's analytic step; AnalyticBodyMatchesFineMarch; the (c) ceiling contract; radii to 1e3 in the two ball tests; the WeightBound deviation in the doc comment; the shipped-void derivation in VoidField's doc comment.",
"IntervalTests.cs: at most three point cases for the mirror-only mutants that are not equivalent (Finding 5).",
"design.md 'Intervals': the widening rule and derivation; the analytic-body paragraph (condition, pre-pass word, one step, the pin); the WeightBound deviation; r3's printed numbers under the three contracts; the one shipped void with (Rc, e) derived.",
"README.md :41: one clause that iv_frustum_ball's radius carries its float32 rounding bound."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/src/CultMath/math.Interval.cs",
"line": 183
},
"change": "iv_frustum_ball: after the r2 radius, radius += (abs(cx) + abs(cy) + abs(zm) + radius) * 9.5367431640625e-7f; doc comment :168-182 states the rule and derivation in place of the strictness sentence."
},
{
"location": {
"path": "packages/cultmath/shaders/CultMath.Interval.hlsl",
"line": 110
},
"change": "cultmath_iv_frustum_ball: the same widening line; both tracked copies (packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Interval.hlsl identical blob)."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 128
},
"change": "TileBallEnclosesEveryRaySegment: the seven extreme families, outward flow at corners, double distance, the two-sided pin; the file header lists the fourth hand mutation (widening dropped) and the WeightBound and Compose mutations."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 58
},
"change": "SnoiseBallEnclosesPoints and FbmBallEnclosesPoints: radius LogUniform(1e-3, 1e3)."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 594
},
"change": "FogField: Grid = Quadratic(0.3, 2048, 256), Main Camera citation."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 829
},
"change": "VoidField: focal parameter (default 935); LodFar() = (4000, 50, low, f 467); grid end reach + Rc + 6/K; doc comment with the (Rh, S) -> (Rc, e) derivation, the one shipped void, the [w(z1), 1] deviation."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1009
},
"change": "MarchTile returns (cells, bodyStart, bodyDensity) with the one confirming probe; MarchLod takes the analytic step at bodyStart; Run and RunStats carry it; ReportVoid prints body steps."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1160
},
"change": "IntervalSkipHalvesEvaluations: (c) asserted as Ratio >= 0.90 Ceiling; the LOD-far row printed; the (e) grid printed with the analytic body; AnalyticBodyMatchesFineMarch added beside it."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 403
},
"change": ":403-430 the frustum-ball paragraph gains the widening rule; :412 strictness sentence deleted; :461-497 the saving section rewritten with r3's printed numbers, the three contracts, the analytic body and the shipped void."
},
{
"location": {
"path": "packages/cultmath/README.md",
"line": 41
},
"change": "iv_frustum_ball: 'its radius carries its own float32 rounding bound'."
}
],
"authority_map": {
"owner": "CultMath owns the interval ops, the noise bounds and the frustum ball, whose enclosure promise now covers its own float32 rounding (the widening is part of the function, not of any consumer). The consumer (the test's Scenario, the site's pre-pass) owns the envelope, the composed bound, the body proof and the Beer-Lambert step. The saving test owns the proving-ground numbers; the operator owns the shipped void (Q9) and the contracts (the two rulings).",
"inputs": [
"the r2 surface unchanged; a tile's geometry and warp; a scenario's grid, wells, camera, focal length and units, Aetheria's Main Camera far plane 2048 cited by line",
"the one shipped void as (Rh, S) = (198, 50); every other void constant derived from it"
],
"outputs": [
"iv_frustum_ball / cultmath_iv_frustum_ball: a float32 ball that encloses every exact point of every ray of the tile over the segment, warped, under float32 evaluation of the ball itself",
"per tile: the dense cells and, when the envelope proves the rest constant, the body-start cell and its density; printed numbers per scenario including the body steps"
],
"derived_state": [
"the widening is derived from the computed centre and radius (2^-20 of |c|_1 + r), never a tuned epsilon; a consumer adds nothing for the ball's own rounding and remains responsible for its world transform",
"(Rc, e) are derived from (Rh, S): Rc = Rh + S, e = -ln CARVE / ln(1 - (Rh/Rc)^2); no void constant is authored twice",
"the body start is derived from the same Bound the mask is derived from, by one more probe; the main pass reads it, never decides it",
"the (c) contract value is derived from the run (0.90 of its own ceiling); no absolute target is written for (c)"
],
"forbidden_writers": [
"no change to SNOISE_LIPSCHITZ, iv_snoise_ball, iv_fbm_ball, the noise shape, e = 0.2, the seeds or any field toward a contract; the grid end moves to 6/K for termination only",
"no analytic step from a pointwise density or from the noise ball alone: only a point-interval Bound over the whole remaining range proves the body",
"no early-out of the pre-pass on transmittance; no probe in the main pass (the body-start cell is read, its constant is one pointwise envelope value)",
"no second shipped void anywhere: the map's site section, the site-ground spec and VoidField.Shipped() name (198, 50) and derive the rest; no number in a doc or comment that the committed test did not print in this run",
"no new CultMath function; PowerPulse, the box, the composition and the Beer-Lambert step stay consumer-side; no tape, prune, affine or choice token in the interval files"
],
"shared_paths": [
"the masked fixed-step march still integrates exactly the dense march's cells (the soundness pin); only MarchLod takes the analytic step, and AnalyticBodyMatchesFineMarch ties that step to the fixed-step product over the same cells",
"one Bound serves the enclosure tests, the mask, the body proof and the gated probe; no second composition and no second constancy test",
"the widening lives once in C# and is mirrored bit for bit into both HLSL copies by the existing mirror test; GLSL follows by the lowering (glsl-lowering r2 rebases onto this head)"
],
"deletion_line": "The false strictness claim leaves the C# doc, the HLSL and design.md before the widening is written; the Tint camera citation leaves before the Main Camera's is written; the (c) 2x assertion and the second shipped void leave before the ceiling contract and the one void are written. Net in src: one line and a doc comment. In tests: one new pin test, seven case families, one configuration, the body return; what they buy is intervals-enclose holding for the ball's own float32 evaluation, the analytic body the ruling asked for with its soundness pinned, LOD weights actually exercised, and a mutation report that claims only what it demonstrated."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green (MeasureLipschitz/MeasureMonotonicity excluded by trait); paste counts and every IV-REPORT line, (a)-(d) at far 2048, the (e) grid with body steps, LOD-far, the shipped headline",
"Yggdrasil: dotnet stryker --since:43183276 -m math.Interval.cs from a plain clone; every timeout hand-run; killed, survived and equivalent listed by diff; 0 undemonstrated",
"Starfire: FXC cs_5_0 kernel over every cultmath_iv_* including cultmath_iv_frustum_ball, both HLSL copies, exit 0",
"Starfire: tools/get-dxc.ps1 (pinned) then dxc -T lib_6_3 -HV 2021 on both HLSL copies, exit 0, no diagnostics"
],
"tests": [
{
"name": "NoiseBoundTests.TileBallEnclosesEveryRaySegment",
"pins": "every exact point of every ray of the tile, corners at both depth ends with an outward flow, lies inside the float32 ball, in the r2 domain and in the seven extreme families, distance in double, no tolerance; the radius is the derivation plus at most 2^-19 (r + |c|_1) and at least the derivation."
},
{
"name": "NoiseBoundTests.EnvelopeBoundEnclosesDensity",
"pins": "the composed bound encloses the full and the footprint-truncated density at every warped point of 2,000 slices, including the LOD-far configuration where octave weights are strictly between 0 and 1 and at 0."
},
{
"name": "NoiseBoundTests.AnalyticBodyMatchesFineMarch",
"pins": "a body start is reported only where the Bound over the whole remaining range is a positive point and every sampled density there equals it exactly; the analytic factor equals the fixed-step product within 1e-4 relative; no (a)-(d) tile reports a body."
},
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "(a) at least 2x combined cost; (c) at least 0.90 of its probe-free ceiling; (e) at the one shipped void at least 2x fewer LOD steps per pixel, the masked fixed-step march exact, the LOD march within 0.02; every row printed including body steps; one Skip naming saving-2x-scenarios on any shortfall."
},
{
"name": "NoiseBoundTests.SnoiseBallEnclosesPoints and FbmBallEnclosesPoints",
"pins": "the ball bounds enclose their functions for radii up to 1e3, so a bound tightened for r > 1 (L / r) dies to an enclosure test."
},
{
"name": "IntervalTests.EveryOpEnclosesItsPoints (existing, plus point cases)",
"pins": "r1's enclosure for every op still holds; any non-equivalent mirror-only mutant from Finding 5 has a point case that kills it."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "the widened cultmath_iv_frustum_ball matches iv_frustum_ball bit for bit; 53 functions compared."
}
],
"negative": [
{
"pattern": "never parallel|cannot cross|strict",
"scope": "math.Interval.cs iv_frustum_ball doc comment, CultMath.Interval.hlsl, design.md 'Intervals' frustum-ball paragraph: 0 matches."
},
{
"pattern": "635-636|far 1000|Tint",
"scope": "NoiseBoundTests.cs: 0 matches; ':31830' and '2048' appear in FogField's citation."
},
{
"pattern": "Rc = 640|e = 4\\b|Rc ~ 220|e ~ 0\\.6|about 220|about 0\\.6",
"scope": "NoiseBoundTests.cs and design.md: 0 matches; the shipped void is derived from (198, 50) as Rc = 248, e = 0.40."
},
{
"pattern": "tape|prune|affine|choice|PowerPulse|pulse|envelope|Scenario|body|Beer",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches (case-insensitive); the analytic body is consumer-side."
},
{
"pattern": "Assert\\.Skip",
"scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios; 0 if (a), (c) and (e) meet their contracts."
},
{
"pattern": "10\\.099261|0\\.35f",
"scope": "git diff 43183276..HEAD -- packages/cultmath/src: the constant is not touched; the slab's F0 stays in (d)."
},
{
"pattern": "git diff --name-only 43183276..HEAD",
"scope": "only packages/cultmath/**; both tracked CultMath.Interval.hlsl copies change together and stay one blob."
},
{
"pattern": "git diff 43183276..HEAD -- packages/cultmath/src | grep '^[-+]' | grep -v '^[-+][-+]' | grep -v '///' | wc -l",
"scope": "at most 4 changed code lines in src: the widening line in iv_frustum_ball and nothing else."
}
],
"operator": []
},
"estimate": {
"lines_added": 260,
"lines_removed": 40,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-wells-contract-vs-ceiling",
"cultmath-tapes:ruling:operator-analytic-wall-body",
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-measure-with-empty-space",
"cultmath-tapes:ruling:operator-footprint-lod-march",
"cultmath-tapes:ruling:operator-ground-void-brush",
"cultmath-tapes:ruling:operator-void-true-cavity",
"cultmath-tapes:ruling:operator-compiler-downloads",
"cultmath-tapes:ruling:operator-campaign-home"
],
"questions": [
"Q9 in the map: the one shipped void. (a, recommended) Rh 198, S 50, derived Rc 248, e 0.40, the scene r2 measured; (b) the site section's Rc 640, e 4; (c) Rh 198, S 100. Hands codes (a)."
]
}

View file

@ -0,0 +1,292 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 4,
"title": "Interval ops r4: LOD agreement against a converged march at the body start, BodyDepth pinned, (c) against an oracle ceiling, WeightBound pinned, design.md from r4's own run",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "3ac7250a",
"depends_on": [],
"first": [
"Follow-up on verdict cultmath-tapes:verdict:cut-interval-ops.s2 (range 43183276..3ac7250a, worktree F:\\Projects\\CultLib-iv), four findings, one item each below. Touches packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs and packages/cultmath/docs/design.md only: no src, no HLSL, no README. Question cultmath-tapes:question:wells-ceiling-oracle decides only (c)'s assertion lines (item 9). Self dispatches with its ruling in the brief. Line numbers are at 3ac7250a. Every probe number quoted here is from Imagination's scratch probe at 3ac7250a, with the test's own seeds, tiles and N = 8. It is never committed; r4's run reprints its own numbers.",
"Finding lod-agreement-blind-past-body (High), measured. Comparing with the 10-unit fixed-step march cannot pin LodStep. At the shipped void, the LOD march's T at the body start differs from it by up to 0.300 of max(T, 0.02). LodStep doubled differs by 0.309. Both are unconverged midpoint quadratures of a ramp with 25-unit detail. A converged march separates them: h = 0.5, which agrees with h = 1 to 0.0065. Relative optical-depth error per ray, max at the headline: fixed-step 0.061, LOD 0.086, LodStep x2 0.177, LodStep x0.5 0.036. With the pre-body integration dropped, T is wrong by 26.5x max(T, 0.02). So the LOD march is 1.4x less accurate than the dense march before the body. Report it; it is not tuned.",
"The agreement rule. MarchLod gains `float cutoff`; Run's costed march passes 0.02, as now. It returns (Transmittance, AtBody). AtBody is T when z first reaches the body start, before the body step, or the final T when there is no body. New helper ConvergedDepth(field, tile, slope, stop, h, lod) returns the optical depth in double, by midpoint quadrature at step h. It runs over EVERY grid cell before `stop`, never the mask's, so it trusts no pre-pass, using the field's Density.",
"(Agreement, continued.) In Run with lod, per ray, with stop = bodyStart, or the cell count (grid.Length - 1) when there is none:\n- tauConv = ConvergedDepth(..., stop, 0.5, lod: true).\n- tauLod = -ln(max(AtBody, 1e-30)) from a second MarchLod at cutoff 0.\n- tauRef = -ln of the product of Integrate over cells < stop, lod: false, with no early-out.\nRunStats keeps MaxLodDepthError and MaxRefDepthError, each max |tau - tauConv| / max(tauConv, 1e-3). The converged march and the cutoff-0 march count into a throwaway Counts and no steps, so cost rows do not move.",
"The (e) agreement contract replaces 'LOD within 0.02 at the end': at the shipped headline, MaxLodDepthError <= 2 x MaxRefDepthError. The footprint march may be at most twice as wrong as the dense march it is measured against. This mirrors the 2x step contract and is Imagination's default; Self may put it to the operator. Measured 0.086 <= 0.122. LodStep x2 and the dropped pre-body integration fail it. The end-of-ray assertion :1455 is deleted. End-of-ray |dT| stays printed. Every (e) row and LOD-far prints both errors and their ratio, which is printed and not asserted. LOD-far's tauRef carries the deliberate octave truncation, so its ratio is not comparable.",
"BodyFactor becomes BodyDepth (:1198-1201). It returns the optical depth Density x ext x (grid end - z) x |dir| as a float. MarchLod multiplies by exp(-BodyDepth(...)). AnalyticBodyMatchesFineMarch part (2) deletes its own tau and analytic (:1375-1377). It compares BodyDepth(field, tile, ray, grid[b], lod: true, counts) with the double sum over body cells of -ln(Integrate(cell, lod: true)), within 1e-4 relative. Probe: in the T domain the comparison underflows at LOD-far (float exp of about -256 is 0, relative error 1); in the depth domain it does not. With |dir| dropped, the depth error is |dir| - 1, at least 0.00145 at the shipped camera, so the mutant dies on every tile.",
"Finding wells-ceiling-contract-vacuous (Medium), measured at N = 8. Warp 0: ratio 2.744x, mask ceiling 2.855x (0.961), oracle ceiling 7.263x (0.378), oracle-empty cells skipped 0.952, skip-nothing pre-pass 0.136. Warp D: 1.391x, 1.401x (0.993), 7.165x (0.194), 0.842, 0.139. At warp D about 80% of the masked cost lies in cells where every ray's sample is 0, so design.md's 'where the rays need them' is false. The ruling's reason needs a ceiling the pre-pass cannot move. Hence the question.",
"Measurement, whatever the ruling. Run draws a tile's N^2 slopes first, then marches. The random stream reorders, so every IV-REPORT row reprints. The oracle mask is the cells whose Density at mid-depth is > 0 for at least one of those slopes, evaluated into a throwaway Counts. The oracle march runs each ray over the oracle cells only, with the same Integrate and early-out, costed into a new RunStats.Oracle. Assert its T equals the dense march's exactly (probe max |dT| 0). RunStats.Ceiling (:1259-1260) goes. In its place: OracleCeiling = Dense.Cost / Oracle.Cost; Efficiency = Oracle.Cost / (Masked.Cost + Probes.Cost); ProbeOverhead = Masked.Cost / (Masked.Cost + Probes.Cost); CullFraction = oracle-empty cells the mask skips / oracle-empty cells. Report prints all four.",
"(c)'s assertion, at N = 8, the lower of warp 0 and D, per the ruling on wells-ceiling-oracle. :1437 records the chosen value. For the cull floor, Assert.True fails rather than skips.\n- oracle-target-cull-floor (recommended): hard CullFraction >= 0.5, plus Efficiency >= 0.90 in the shortfall list.\n- oracle-target-only: the shortfall alone.\n- cull-fraction: CullFraction >= 0.90 in the shortfall list.\n- overhead-plus-floor: ProbeOverhead >= 0.90 in the shortfall list, plus the hard floor.\nThe contracts line names the measured quantity. Note: under options 1 and 2 the test skips today, naming saving-2x-scenarios with (c)'s shortfall; (a) and (e) are still evaluated and listed.",
"Finding weightbound-mutation-survives (Low). VoidField gains public (float2 Coarse, float2 Fine) WeightBounds(float z0, float z1) beside Weights(z) (:992). WeightBound (:1035) stays private and is what WeightBounds calls. New Fact WeightBoundEnclosesOneSignedNoise on LodFar, 2,000 cases: z0 ~ U(1460, 0.95 x grid end), z1 ~ U(z0, grid end), 16 depths in [z0, z1] including both ends. Each octave weight lies in its bound. For the one-signed noise interval n = [0.25, 1] and n_s in {0.25, U(0.25, 1), 1}, w(z) n_s lies in iv_mul(bound, n). Probe: the spec's mutant [w(z0), 1] misses w(z1) x 0.25 in 2000 of 2000 fine-octave cases on (1460, 2920). The header (:14-20) moves that mutant to this test.",
"Finding design-saving-section-stale (Medium). Rewrite design.md :475-512 from r4's final run, where every figure appears in an IV-REPORT line the report pastes. Delete :422-423's '6e-4 at the shipped scales' sentence, which no test prints. Contents:\n- the tile march and its marches;\n- (a)-(d) at far 2048, with (c)'s oracle ceiling, efficiency, cull fraction and probe overhead;\n- (e) at the shipped void, Rh 198, S 50, Rc = Rh + S, e = -ln 1.5 / ln(1 - (Rh/Rc)^2): steps, snoise, cost, body tiles, the two depth errors;\n- the analytic body (condition, one probe, one step, the pin);\n- the WeightBound [w(z1), 1] deviation and why it encloses;\n- the contracts as asserted, (c) in the ruling's words.\n'The test skips' appears only if this run skips. No r2 or r3 number survives.",
"Site-ground's dense-reference browser check has the same blind spot (its converged mean luminance is compared after the walls' bodies). It is follow-up cultmath-tapes:follow_up:site-ground-dense-check-blind-past-body, owned by the next site-ground spec. Not this cut.",
"Mutation evidence, cheap by design. src is untouched, so Stryker has nothing in scope and is not run; the negative check proves the premise. Run ONE tools/mutation-table.sh job on Yggdrasil against the final head, test filter FullyQualifiedName~NoiseBoundTests, with these rows:\n- MarchLod `transmittance *= Integrate(field, tile, slope, z, next, lod: true, counts);` -> `transmittance *= 1.0f;` must fail IntervalSkipHalvesEvaluations;\n- LodStep's return wrapped in `2.0f * (...)`: same test;\n- BodyDepth without its sqrt factor: AnalyticBodyMatchesFineMarch;\n- WeightBound `).x, 1.0f)` -> `).y, 1.0f)`: WeightBoundEnclosesOneSignedNoise;\n- MarchTile `if (bound.y <= 0.0f)` -> `if (bound.y <= 0.0f && !field.Name.Contains(\"wells\"))`: IntervalSkipHalvesEvaluations, under the floor options; a survivor under oracle-target-only, by that option's design;\n- Compose `CarveRadius` -> `HollowRadius` in the lo test: AnalyticBodyMatchesFineMarch (carried).\nNo per-mutant hand loops, no Starfire jobs.",
"Budget: under 200k Hands tokens. Read NoiseBoundTests.cs :480-1469 and design.md :344-515 once; no other file. Verification is one full CultMath.Tests run on Yggdrasil after the code commits (paste counts and every IV-REPORT line), the mutation-table job, and one fix round. If the headline agreement fails at r4's own numbers, do not move the factor or the step: report the measured errors as a fork."
],
"deletes": [
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 1,
"note": ":1455 the end-of-ray assertion 'the footprint-aware march differs from the reference by'; blind past the body (finding lod-agreement-blind-past-body)."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 4,
"note": ":1198-1201 BodyFactor and its comment; replaced by BodyDepth, the one owner of the body's optical depth."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 3,
"note": ":1375-1377 the test's own tau, its exp and the T-domain comparison; replaced by BodyDepth against the cells' summed depth."
},
{
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"lines": 2,
"note": ":1259-1260 RunStats.Ceiling and 'What no amortization can beat'; the mask-derived ceiling stops being a contract owner."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 38,
"note": ":475-512 the saving section with r2's rows (1.41x, 16.45, 2.21x, 1.34x) and '(c) does not, so the test skips'."
},
{
"path": "packages/cultmath/docs/design.md",
"lines": 2,
"note": ":422-423 'At the shipped scales (|c|_1 near 600, r near 5) it adds 6e-4.'; printed by no test."
}
],
"keeps_moves": [
"packages/cultmath/src, both CultMath.Interval.hlsl copies, README.md and every other test file: untouched.",
"Fields, grids (6/K end), seeds, e = 0.2, LodStep, Density, Bound, Compose, MarchTile, Integrate, the masked fixed-step march's exactness asserts, the (a) and (e) step contracts and the Skip mechanism: unchanged.",
"TileBallEnclosesEveryRaySegment, EnvelopeBoundEnclosesDensity, the Lipschitz tests, TightnessReport, IntervalTests, HlslSourceCompatibilityTests: unchanged.",
"AnalyticBodyMatchesFineMarch parts (1) and (3): unchanged; part (2) changes domain and owner only.",
"design.md :344-474 (Intervals up to the saving section, with the r3 frustum-ball paragraph minus the 6e-4 sentence) and every other section: unchanged."
],
"adds": [
"NoiseBoundTests.cs: BodyDepth; MarchLod's cutoff and (Transmittance, AtBody) return; ConvergedDepth; Run's slope-first draw, oracle mask, oracle march and the cutoff-0 agreement march; RunStats Oracle, OracleCeiling, Efficiency, ProbeOverhead, CullFraction, MaxOracleDifference, MaxLodDepthError, MaxRefDepthError; Report and ReportVoid fields; the (e) depth contract; (c)'s assertion per the ruling.",
"NoiseBoundTests.cs: VoidField.WeightBounds(z0, z1); Fact WeightBoundEnclosesOneSignedNoise; the header's mutation list updated.",
"design.md: the saving section rewritten from r4's run."
],
"file_changes": [
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 14,
"end_line": 20
},
"change": "Header mutation list: w(z0) for w(z1) now fails WeightBoundEnclosesOneSignedNoise. Add: the dropped pre-body integration and LodStep x2 fail IntervalSkipHalvesEvaluations's depth agreement; BodyDepth without |dir| fails AnalyticBodyMatchesFineMarch."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 992
},
"change": "Beside Weights(z): public (float2 Coarse, float2 Fine) WeightBounds(float z0, float z1) => (WeightBound(CoarseWavelength, z0, z1), WeightBound(FineWavelength, z0, z1))."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1198,
"end_line": 1201
},
"change": "BodyFactor -> BodyDepth(Scenario field, Tile tile, float2 slope, float z, bool lod, Counts counts): the float optical depth Density x Extinction x (Grid[^1] - z) x sqrt(m.x^2 + m.y^2 + 1), with no exp."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1205,
"end_line": 1235
},
"change": "MarchLod(..., float cutoff, ref long steps, ref long bodySteps) returns (float Transmittance, float AtBody). Both loop guards use cutoff. At z >= body: AtBody = transmittance, then transmittance *= exp(-BodyDepth(...)), steps++, bodySteps++, return. Without a body, AtBody = final T."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1236
},
"change": "New ConvergedDepth(Scenario field, Tile tile, float2 slope, int stop, float h, bool lod): sum over cells c < stop of ceil(len / h) midpoint samples, Density(field, tile, slope, z, lod, throwaway) x Extinction x dz x |dir| in double. Every cell, not the mask's."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1237,
"end_line": 1265
},
"change": "RunStats:\n- Counts Oracle; OracleEmpty and OracleEmptySkipped; MaxOracleDifference; MaxLodDepthError; MaxRefDepthError.\n- OracleCeiling, Efficiency, ProbeOverhead and CullFraction as the spec's 'first' defines them.\n- Ceiling deleted.\nRatio and LodRatio are unchanged."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1270,
"end_line": 1305
},
"change": "Run, per tile:\n- MarchTile.\n- Draw N^2 slopes.\n- Oracle mask over all cells (mid-depth Density > 0 for any slope; throwaway Counts); count oracle-empty cells and those the mask skips.\n- Per ray: dense, masked and oracle marches (oracle into stats.Oracle; MaxOracleDifference).\n- With lod: the costed MarchLod (cutoff 0.02), then a cutoff-0 MarchLod and ConvergedDepth at h 0.5 into throwaway Counts, and tauRef with no early-out; update both depth errors."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1307,
"end_line": 1318
},
"change": "Report prints oracle ceiling, efficiency, cull fraction (with counts), probe overhead and max oracle |dT| in place of 'ceiling'. ReportVoid adds the depth errors of LOD and fixed-step against the converged march and their ratio; the end-of-ray |dT| stays."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1332
},
"change": "AnalyticBodyMatchesFineMarch part (2): depth = BodyDepth(field, tile, ray, grid[b], lod: true, counts); sum = the double sum over c in [b, last) of -Math.Log(Integrate(field, tile, ray, grid[c], grid[c + 1], lod: true, counts)); assert |depth - sum| <= 1e-4 x sum, naming field, b and both values. The doc comment says so."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1430,
"end_line": 1440
},
"change": "(a)-(d) loop: Assert MaxOracleDifference == 0 beside the existing <= 1e-3. :1437 records the ruling's (c) quantity (Efficiency, CullFraction or ProbeOverhead), the lower of warp 0 and D at N = 8, and the CullFraction floor value when the ruling has one."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1453,
"end_line": 1466
},
"change": "Headline:\n- MaxDifference == 0 stays.\n- :1455 is replaced by Assert MaxLodDepthError <= 2 x MaxRefDepthError, with both values in the message.\n- Under a floor option: Assert CullFraction >= 0.5 for (c) at both warps (hard).\n- Shortfalls: (c) per the ruling.\n- The contracts line names the quantity and prints the (e) depth errors."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 1402,
"end_line": 1414
},
"change": "IntervalSkipHalvesEvaluations doc comment:\n- the (e) agreement is the converged depth, 2x the fixed-step's;\n- (c) per the ruling, citing it;\n- the oracle is defined;\n- 'within 0.02' leaves."
},
{
"location": {
"path": "packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs",
"line": 304
},
"change": "After EnvelopeBoundEnclosesDensity: Fact WeightBoundEnclosesOneSignedNoise, as item 10 of 'first'. Its message names z0, z1, z, the octave, the weight and the bound."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 475,
"end_line": 512
},
"change": "The saving section is rewritten from r4's IV-REPORT lines (contents in item 11 of 'first'). The next paragraph, 'Consumers: ...' at :514, is kept."
},
{
"location": {
"path": "packages/cultmath/docs/design.md",
"line": 422,
"end_line": 423
},
"change": "Delete the sentence 'At the shipped scales (|c|_1 near 600, r near 5) it adds 6e-4.' Leave the rest of the paragraph."
}
],
"authority_map": {
"owner": "The saving test owns the proving-ground numbers and their oracles. The converged march (h = 0.5) is the accuracy oracle before the body. The oracle mask (cells some ray of the tile samples nonzero) is (c)'s ceiling oracle. BodyDepth is the one owner of the body's optical depth, in the march and in its pin. CultMath src is untouched. The operator owns the contracts: the rulings and the answer to wells-ceiling-oracle.",
"inputs": [
"the r3 fields, grids, seeds, tiles and marches at 3ac7250a",
"the ruling answering cultmath-tapes:question:wells-ceiling-oracle"
],
"outputs": [
"IV-REPORT rows: the LOD and fixed-step depth errors against the converged march; (c)'s oracle ceiling, efficiency, cull fraction and probe overhead",
"design.md's saving section, every figure from those rows"
],
"derived_state": [
"(c)'s ceiling is no longer derived from the pre-pass's own mask; it is derived from the oracle mask, which the pre-pass cannot move.",
"Dense/Masked is no longer a contract owner; Masked/(Masked+Probes) is printed as probe overhead (an owner only under overhead-plus-floor).",
"The fixed-step march is no longer the LOD march's accuracy oracle; it is a peer measured against the converged march, and its error sets the LOD's allowance (2x).",
"End-of-ray |dT| is display-only.",
"The body's optical depth is derived once, by BodyDepth; the test no longer re-derives it."
],
"forbidden_writers": [
"no assertion compares the LOD march with the fixed-step march directly, or after the body",
"AnalyticBodyMatchesFineMarch does not compute the body's depth itself; it calls BodyDepth",
"neither the oracle mask nor ConvergedDepth reads MarchTile's output",
"no change to src, the HLSL, the fields, grids, LodStep, seeds, the 0.2 weight or the 2x factor toward a contract",
"no figure in design.md that this run's IV-REPORT lines do not print"
],
"shared_paths": [
"MarchLod is one function for the costed march (cutoff 0.02) and the agreement march (cutoff 0)",
"one Run computes dense, masked, oracle, LOD and converged for every scenario; the headline and the grid print the same fields",
"the site's mirror of the march inherits the same check through follow-up site-ground-dense-check-blind-past-body"
],
"deletion_line": "The end-of-ray LOD assertion, BodyFactor, the test's own tau, RunStats.Ceiling, the (c) Ratio/Ceiling special case, design.md's r2 rows and the unprinted 6e-4 sentence all leave before their replacements are written. Net: about +170 / -60 in tests and docs, nothing in src. What that buys: (e) can now fail on a wrong pre-body march, the body depth can fail on a wrong |dir|, (c) can fail on a pre-pass that proves nothing, and the WeightBound mutant dies."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests -c Release green or skipped only by saving-2x-scenarios as the ruling implies (MeasureLipschitz/MeasureMonotonicity excluded by trait); paste counts and every IV-REPORT line",
"Yggdrasil: one tools/mutation-table.sh job at the final head with the six rows in 'first' item 13; the table pasted; every row CAUGHT by the named test except the wells row under oracle-target-only",
"No Stryker run and no Starfire job: no src or HLSL change in 3ac7250a..HEAD (negative check)"
],
"tests": [
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "At the shipped void the footprint march's optical depth at the body start is within 2x the fixed-step march's error of a converged march; the oracle march equals the dense march exactly; (c) is asserted per the ruling against a ceiling the pre-pass cannot move; (a) and (e) step contracts as before."
},
{
"name": "NoiseBoundTests.AnalyticBodyMatchesFineMarch",
"pins": "BodyDepth, the depth MarchLod uses, equals the summed fixed-step depth of the body cells within 1e-4 relative, in the depth domain (no underflow), so a body depth without |dir| fails on every tile; parts (1) and (3) unchanged."
},
{
"name": "NoiseBoundTests.WeightBoundEnclosesOneSignedNoise",
"pins": "Each octave weight at every sampled depth in [z0, z1] lies in WeightBounds(z0, z1), and w(z) n lies in iv_mul(bound, n) for a one-signed n, so a lower end above w(z1) fails."
},
{
"name": "NoiseBoundTests (rest) and CultMath.Tests (rest)",
"pins": "unchanged and green."
}
],
"negative": [
{
"pattern": "git diff --name-only 3ac7250a..HEAD",
"scope": "exactly NoiseBoundTests.cs and design.md under packages/cultmath."
},
{
"pattern": "git diff 3ac7250a..HEAD -- packages/cultmath/src packages/cultmath/shaders packages/cultmath/unity",
"scope": "empty."
},
{
"pattern": "BodyFactor|Ceiling =>|\\.Ceiling\\b|MaxLodDifference <= ",
"scope": "NoiseBoundTests.cs: 0 matches."
},
{
"pattern": "Math\\.Exp\\(-\\(double\\)tau\\)|var tau = density",
"scope": "NoiseBoundTests.cs: 0 matches (the test does not re-derive the body)."
},
{
"pattern": "1\\.41x|16\\.45|2\\.21x|1\\.34x|6e-4|does not, so the test skips|where the rays need them",
"scope": "packages/cultmath/docs/design.md: 0 matches."
},
{
"pattern": "Assert\\.Skip",
"scope": "NoiseBoundTests.cs: at most one, in IntervalSkipHalvesEvaluations, naming saving-2x-scenarios."
}
],
"operator": []
},
"estimate": {
"lines_added": 170,
"lines_removed": 60,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-wells-contract-vs-ceiling",
"cultmath-tapes:ruling:operator-analytic-wall-body",
"cultmath-tapes:ruling:operator-footprint-lod-march",
"cultmath-tapes:ruling:operator-measure-with-empty-space",
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-shipped-void"
],
"questions": [
"cultmath-tapes:question:wells-ceiling-oracle"
]
}

View file

@ -0,0 +1,146 @@
{
"campaign": "cultmath-tapes",
"cut": "interval-ops",
"revision": 1,
"title": "Interval ops: cultmath_iv_* over float2(lo, hi) in C# and HLSL with enclosure tests, a committed Lipschitz constant for snoise, and ball bounds for snoise and fbm",
"repo": "GameCult/CultLib",
"branch": "hands/cultmath-interval-ops",
"base": "c49c76ee",
"depends_on": [],
"first": [
"Map: docs/cultmath-interval-ground-cut.md, sections 'Ownership', 'Interval culling along the ray' and 'The proving-ground contract'. Parked target: docs/cultmath-tape-target.md, sketch step 1 (:59-64) and 'Open before unparking' (:106-115). This cut is step 1 and nothing after it: no tape, no pruning, no min/max choice tracking, no affine arithmetic (map D1).",
"Common rules from the Asura cut map (Asura docs/cut-map.md, 'Common rules for every addition', :453-489): C# in packages/cultmath/src/CultMath as a partial of math (the Phacelle pattern: math.Phacelle.cs beside math.cs), HLSL in a new shaders/CultMath.Interval.hlsl included from CultMath.hlsl the way CultMath.Phacelle.hlsl is (:506), names prefixed cultmath_, both tracked HLSL copies one blob, bodies inside design.md's transformation list (:92-134), THIRD-PARTY-NOTICES untouched (nothing is ported; Fidget's ideas are used, not its code).",
"Representation: an interval is float2(lo, hi), lo <= hi, both finite. The empty interval does not exist; a caller that needs 'provably empty' tests hi < cutoff. Point intervals are iv_point(x) = float2(x, x).",
"Op set, each with the C# twin the mirror test demands: iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale(iv, float), iv_abs, iv_min, iv_max, iv_sqr, iv_sqrt (lo clamped at 0), iv_exp, iv_clamp(iv, float, float), iv_saturate, iv_smoothstep(float e0, float e1, iv) (monotone: endpoints), iv_lerp(iv, iv, float t). No others until a consumer names one.",
"Noise bounds: CULTMATH_SNOISE_LIPSCHITZ (static const float, provenance in the comment: measured max |snoise_grad| over 1e6 seeded points plus 1e4 gradient-ascent refinements, times 1.10; the test that measures it is committed as a slow, explicitly-run test and the constant is pinned by a fast test). iv_snoise_ball(float3 c, float r) = [snoise(c) - L r, snoise(c) + L r] intersected with [-1, 1]. iv_fbm_ball(c, r, octaves, lacunarity, gain) = the octave sum of iv_snoise_ball(c * f_i, r * f_i) scaled by a_i, octaves clamped to [0, 16] like fbm_grad.",
"Write the enclosure property first, as the shared harness: for each op, 10,000 seeded random intervals (widths from 1e-4 to 10, centres in [-20, 20]) and 16 points inside each; assert f(x) in op(I) with no tolerance (the bound is computed in the same float32 arithmetic; where rounding could exclude an endpoint, widen the op by one ulp in the implementation, not the test, and say so in design.md). For the ball bounds: 2,000 balls, r in [1e-3, 2], 64 points inside each.",
"The saving test: a representative coverage field (one snoise octave at F0 = 0.35, cutoff 0.25) and 1,000 seeded rays through a slab of depth 12; march once at the dense step and once with the adaptive interval march from the map (Δ0, ΔMAX = 8 Δ0, ΔDENSE = Δ0/2, grow on skip, halve on maybe); count snoise evaluations; assert the integrated transmittance agrees within 1e-3 and the evaluation count is at least 2x lower. Print both counts and the ratio; the cut report quotes them.",
"The warp enclosure test (the site's march rests on it): for 2,000 seeded (centre c, radius r, warp bound D) triples and random flow vectors with |flow| <= D, every warped point c' = x + flow (x in the ball of radius r) lies in the ball of radius r + D, and snoise(c') lies in iv_snoise_ball(c, r + D). This is a property of the existing bound, not a new function; it is written as a named test so the domain-warp use is pinned and documented in design.md.",
"The saving test's field is domain-warped: coverage = sum over two phases of snoise(p + flow * (phase_k - 0.5) * PERIOD) with a bounded flow, matching the map's field shape, so the interval radius includes D.",
"Tightness: a committed test prints the mean width ratio (interval width / true range over the 64 points) for iv_snoise_ball at r = 0.05, 0.25, 1.0 and for iv_fbm_ball at 4 octaves. No assertion beyond finiteness; the number is for the report and for a later affine cut to beat.",
"Verification on Yggdrasil: dotnet test from packages/cultmath/tests/CultMath.Tests; Stryker --since:c49c76ee from a plain clone. Starfire: FXC cs_5_0 kernel calling every iv_* function and both ball bounds; dxc -T lib_6_3 -HV 2021. Detached with a log, polled in the foreground; never a background shell task. Numbers in comments or docs come from committed tests only.",
"If glsl-lowering has merged to main before this branch does, rebase and regenerate shaders/CultMath.glsl (CULTMATH_WRITE_GLSL=1) so the pin test is green on the merge; if not, the lowering cut regenerates after this one. Either order works because the GLSL is derived."
],
"deletes": [],
"keeps_moves": [
"snoise, snoise_grad, fbm_grad, ridged_grad, cellular, phacelle: untouched. The bounds call snoise (value) only; snoise_grad is used by the Lipschitz measurement test, not by any bound (map D2).",
"design.md's invariant 8 section (136-) is unchanged; intervals are not value-and-gradient primitives and do not claim the float4(grad, value) layout.",
"HlslMirrorTests' ban on erosion kernels stands; nothing named interval touches it."
],
"adds": [
"packages/cultmath/src/CultMath/math.Interval.cs: public static partial class math with iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale, iv_abs, iv_min, iv_max, iv_sqr, iv_sqrt, iv_exp, iv_clamp, iv_saturate, iv_smoothstep, iv_lerp, iv_snoise_ball, iv_fbm_ball, and the constant SNOISE_LIPSCHITZ; each with a doc comment stating the enclosure it guarantees and the ulp widening where one is needed.",
"packages/cultmath/shaders/CultMath.Interval.hlsl: the HLSL mirrors with the same names prefixed cultmath_, and CULTMATH_SNOISE_LIPSCHITZ as static const; included from CultMath.hlsl after the Phacelle include (:506). Both tracked HLSL copies.",
"tests/CultMath.Tests/IntervalTests.cs: the enclosure harness over every op; iv_mul sign cases (all four quadrant combinations and a zero-straddling factor); iv_sqrt with lo < 0; iv_abs straddling zero gives [0, max(|lo|,|hi|)]; iv_smoothstep monotonicity; iv_min/iv_max against the pointwise min/max; a mutant that shrinks any bound by one ulp dies (hand-mutation list in the file header for the float arithmetic Stryker cannot reach, as in 2a-ii-a).",
"tests/CultMath.Tests/NoiseBoundTests.cs: iv_snoise_ball and iv_fbm_ball enclosure; LipschitzConstantPinsSampledGradients (fast, 1e5 points); MeasureLipschitz (slow, explicit trait, prints the max and the refined max); TightnessReport; the saving test (InvalSkipHalvesEvaluations) with its printed counts.",
"packages/cultmath/docs/design.md: a section 'Intervals' after the invariant 8 section: representation, the op list, the ulp-widening rule, the Lipschitz provenance and margin, what the ball bounds guarantee and what they do not (nothing about tapes), and that the site's ground shader and Aetheria's nebula raymarch are the consumers.",
"packages/cultmath/README.md: one paragraph under the noise entry naming the interval family and its purpose (empty-space culling with a proof)."
],
"file_changes": [
{
"location": { "path": "packages/cultmath/shaders/CultMath.hlsl", "line": 506 },
"change": "add #include \"CultMath.Interval.hlsl\" after the Phacelle include; both tracked copies."
},
{
"location": { "path": "packages/cultmath/src/CultMath/math.cs", "line": 602 },
"change": "no change to snoise; a comment pointer from snoise to SNOISE_LIPSCHITZ in math.Interval.cs so the next kernel change knows the constant must be re-measured."
},
{
"location": { "path": "packages/cultmath/docs/design.md", "line": 136 },
"change": "new 'Intervals' section after invariant 8."
},
{
"location": { "path": "packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs", "line": 348 },
"change": "none expected; confirm the mirror resolves the second #include and that every cultmath_iv_* finds its C# twin (a mirror without a twin fails Assert.Contains MirrorOnly)."
}
],
"authority_map": {
"owner": "CultMath owns interval arithmetic and the noise bounds in C# and HLSL (and GLSL by derivation). The Lipschitz constant is owned by the committed measurement test; the kernel of snoise owns its value.",
"inputs": [
"the existing snoise(float3) value function; snoise_grad for the measurement test only",
"seeded random points and intervals in tests"
],
"outputs": [
"cultmath_iv_* and the two ball bounds, each enclosing its function over its input region",
"three printed numbers for the report: the Lipschitz max, the tightness ratios, the evaluation saving"
],
"derived_state": [
"CULTMATH_SNOISE_LIPSCHITZ is derived from the snoise kernel; a kernel change must re-run MeasureLipschitz and re-pin",
"shaders/CultMath.glsl (if the lowering has landed) is derived and regenerated, never edited"
],
"forbidden_writers": [
"no interval op without the enclosure test and a C# twin",
"no bound that reads snoise_grad; no value-only/gradient twin of any iv_* function",
"no tape, bytecode, pruning, choice flag or affine form in this cut; names containing tape, prune or affine do not appear",
"no number in a comment or design.md that a committed test did not print"
],
"shared_paths": [
"the same IntervalTests enclosure harness runs every op; a new op is one entry in its table",
"the existing HLSL mirror test covers every HLSL interval function with no new comparison path"
],
"deletion_line": "Nothing is deleted. The liability is one C# file, one HLSL file, two test files and a doc section; it buys the invariant intervals-enclose for every GPU consumer, and it is step 1 of the parked tapes campaign landed in advance."
},
"verification": {
"builds": [
"Yggdrasil: dotnet test packages/cultmath/tests/CultMath.Tests green (MeasureLipschitz excluded by trait); paste counts",
"Yggdrasil: dotnet stryker --since:c49c76ee from a plain clone; 0 survivors in math.Interval.cs except listed equivalents; hand mutations for float3 arithmetic listed and killed",
"Starfire: FXC cs_5_0 kernel over every new function; dxc -T lib_6_3 -HV 2021 on both HLSL copies"
],
"tests": [
{
"name": "IntervalTests.EveryOpEnclosesItsPoints",
"pins": "intervals-enclose for every op, no tolerance."
},
{
"name": "NoiseBoundTests.SnoiseBallEnclosesPoints / FbmBallEnclosesPoints",
"pins": "the ball bounds enclose snoise and fbm over 2,000 balls x 64 points."
},
{
"name": "NoiseBoundTests.WarpedPointsStayEnclosed",
"pins": "a domain warp bounded by D is enclosed by enlarging the ball radius by D; the site's interval march is sound."
},
{
"name": "NoiseBoundTests.LipschitzConstantPinsSampledGradients",
"pins": "no sampled |snoise_grad| exceeds the constant; a constant lowered by 1% fails."
},
{
"name": "NoiseBoundTests.IntervalSkipHalvesEvaluations",
"pins": "the adaptive march needs <= half the evaluations of the dense march for the same transmittance within 1e-3."
},
{
"name": "HlslSourceCompatibilityTests.EveryMirrorFunctionMatchesCSharpMath (existing)",
"pins": "every cultmath_iv_* and both bounds match C# bit for bit."
}
],
"negative": [
{
"pattern": "tape|prune|affine|choice",
"scope": "packages/cultmath/src/CultMath/math.Interval.cs and shaders/CultMath.Interval.hlsl: 0 matches (case-insensitive)."
},
{
"pattern": "snoise_grad|fbm_grad",
"scope": "math.Interval.cs and CultMath.Interval.hlsl: 0 matches; the bounds use values only."
},
{
"pattern": "spherical_erosion|AdvancedErosionFilter",
"scope": "packages/cultmath: 0 matches (HlslMirrorTests' standing rule)."
},
{
"pattern": "git diff --name-only c49c76ee..HEAD",
"scope": "only packages/cultmath/** (and shaders/CultMath.glsl if regenerated); no other package or src tree."
}
],
"operator": []
},
"estimate": {
"lines_added": 900,
"lines_removed": 0,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [],
"questions": []
}

View file

@ -0,0 +1,246 @@
{
"campaign": "cultmath-tapes",
"cut": "site-ground",
"revision": 3,
"title": "Site ground r3: A cloud volume carved by one negative pseudo-Gaussian sphere, a sun at the hollow's bottom lights the walls and orbits, the camera follows low inside; pre-pass, camera-aware TAA",
"repo": "GameCult/gamecult-site",
"branch": "main",
"base": "535d170",
"depends_on": [
"site-masthead:cut_spec:wash-home-only.r1",
"cultmath-tapes:cut_spec:glsl-lowering.r2",
"cultmath-tapes:cut_spec:interval-ops.r3"
],
"first": [
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'Pass 3' (the void scene, the composed bound, the pre-pass, the budget), then 'The shader design' for what pass 3 does not restate (sampled wash, palette input, pausing, contrast). Rulings in force: cultmath-tapes:ruling:operator-ground-void-brush (the scene), operator-tile-amortized-march (the march), site-masthead:ruling:operator-chromeless-column; wash-home-only's authority map. Re-pin base to main after wash-home-only lands. Stardust and DoF stay the next cut; leave the named composite slot. r2's token, readback, controller, vendoring, parity and fallback items stand unless this revision names them.",
"The original, read once (Aetheria at e6181c11, 2021-03-13, Assets/Scenes/Main Menu.unity; Prefabs/RPG/Planets/Sun.prefab; Materials/Brushes/Gravity Well.mat, Main Menu Sun Fog Tint.mat, Main Menu Ambient Fog Tint.mat, Sector Boundary Fog.mat; Materials/Fog Marching Main Menu.mat; Shaders/Raymarching/VolumeRender.shader at that commit; Shaders/Brushes/PowerBrush.shader). What survives, in the original's units: the brush is PowerBrush (1 - x^2)^16 with depth 40 over radius 256 (half depth at r = 52, pseudo-Gaussian sigma about 45), the sun's own gravity-well child; a wide boundary bowl (depth 75, power 0.25, radius 1500) is flat across the scene and acts as a constant 75; floor offset -16.46, floor blend 39.6, so the fog top is at y = -91 outside the void and dips to -131 at the sun; the sun sphere (radius 12.5) sits at y = -130, the bottom of the void.",
"What survives (continued): the sun is a child of 'Center', which rotates 5 degrees per second about y (72 s period), at orbit radius 175; the camera is a Cinemachine vcam following the sun (framing transposer, distance 150, damping 10, handheld noise amplitude 1 at frequency 0.25) and looking at 'Look Target' (0, -130, 0), the orbit centre at the sun's depth; the saved camera sits at y = -27, 64 above the fog top. The lighting was authored, not marched: the sun's 'Fog Tint' brush (1 - x^2)^3, depth 2, radius 128, colour (1, 0.4, 0.1) orange, painted into the tint map, times a scene-wide ambient tint (0.18, 0.59, 1) blue, depth 0.01. Stardust: 256^2 particles, spacing 2, floor -60, ceiling 0, height exponent 800 / (ceiling + wellDepth) so particles climb higher where the well is deeper (the void fills with stardust). Noise: strength 150, frequency 0.01, safety 50. Not surviving: the 2021 d() details, the tint map's runtime contents, the video.",
"The field (ruling operator-void-true-cavity: a spherical hollow inside a cloud volume that surrounds it on every side, not the 2021 bowl). Units are the original's. Outer volume: an unbounded uniform fill, chosen over a shell because every ray then ends in a wall and the march terminates by opacity, never by a far plane; Z1 = |camera - c_v| + Rc + 6/K (614 at the shipped point) bounds the pre-pass range and is past opacity for every ray, so the analytic body step below always terminates. The carve is the pseudo-Gaussian brush g(d) = (1 - (d / Rc)^2)^e, d = |p - c_v|, density = K max(0, 1 - CARVE g(d')), authored by two knobs: the hollow radius Rh (density exactly 0 inside; Rh = Rc sqrt(1 - CARVE^(-1/e))) and the ramp width S (density full at Rh + S; set by the exponent: e = 4 gives S = 1.3 Rh, e = 0.25 gives S = 0.12 Rh).",
"Shipped (Q9 in the map, option (a) pending the operator): Rh = 198, S = 50, so Rc = Rh + S = 248 and e = -ln CARVE / ln(1 - (Rh / Rc)^2) = 0.40; CARVE = 1.5, K = 1/30: a wall is opaque within about 150 units of full density. The ramp and the noise fade band are both S wide; nothing void-shaped is authored twice (interval-ops r3, finding shipped-void-two-scenes). Noise displaces the wall only: fade(d) = smoothstep(Rh - S, Rh, d), d' = d + A fade(d) n(warp(p)), A = 20, F0 = 0.01.",
"Scene motion. The sun orbits the origin at radius 175 in xz with period 72 s (5 degrees per second, the original's Center pivot). The cavity centre is c_v = sun + (0, Rh - 12.5, 0): the sun sphere (radius 12.5) rests on the hollow's bottom and the hollow moves with it. The camera follows the sun 120 behind it along the orbit's tangent and 45 above it, so it sits 186 from c_v, 12 inside the hollow (the handheld drift is 2 units, two low-frequency snoise on time), low in the cavity, looking at the sun's position plus (0, 60, 0) so the lit lower wall and the dark upper wall share the frame. The 2021 follow distance 150 and height 25 do not fit a hollow a camera can stand in; 120 and 45 are the re-derivation, and the report says so. worldAt(t) in ground.js is the one owner of sun, cavity and camera, uploaded as uniforms; the shaders never integrate motion.",
"The flow field, authored around the void: flow(xz) = SWIRL * tangent(xz - sun.xz) * (1 - (r / Rw)^2)^2 + GLOBAL * (snoise(xz Ff + o1), snoise(xz Ff + o2)) (the swirl is largest mid-slope and zero at the rim and the centre; the global term is r2's). The flow texture is a world-space window of side 4 Rw centred on the sun, quarter of the canvas resolution, re-rendered every frame; warp, hue, motion vectors and (next cut) the stardust read it, so D4 holds with a moving scene. D = FLOW_MAX * PERIOD / 2 as before, with FLOW_MAX the authored maximum of the swirl plus the global amplitude.",
"The march (tile pre-pass, interval-ops r2's design). Pre-pass at W/8 x H/8: per tile, the loop over the slab's cell ranges (uniform cells, StepDense = min(10, S / 4) = 10, 62 cells to Z1 = 614 along +view depth; the mask has 96 bits): the envelope probe first (exact box: y and xz ranges by cultmath_iv_mul of the depth and slope intervals, d's interval is the box's least and greatest distance to c_v, exact; g, fade and the carve are monotone in it), then the noise ball (cultmath_iv_frustum_ball with D, cultmath_iv_snoise_ball) only when fade.hi > 0 and L r F0 leaves room to prove anything; writes the 96-bit mask of cells not proven empty to an RGBA32UI texel's first three words, and in the fourth word the body-start cell: the first cell whose own bound is a positive point and whose slice to Z1 is one too (the envelope with the free noise interval proves density exactly K for the rest of the range), 0xFFFFFFFF when none (interval-ops r3, ruling operator-analytic-wall-body).",
"(March, continued.) Main pass: texelFetch the mask, then march the unmasked range with the footprint step Delta(z) and the octave weights (next item), the per-frame sample count capped by the budget S_budget and the start jittered so TAA completes what one frame does not; masked cells are skipped exactly; at the body-start cell take one analytic Beer-Lambert step, T *= exp(-K (Z1 - z) |dir|) with K the field's pointwise envelope value there (no probe in the main pass), and stop; integrate transmittance and the depth-weighted mean distance, apply the light. Every tile's interior cells retire in a few envelope probes and its wall cells are marked; there are no sky tiles. Behind a fully transmissive pixel the resolve shows --gamecult-nebula-base; by construction only an unfinished ray (budget exhausted before the wall, measured, must be under 1%) reaches it.",
"Footprint LOD (the operator's challenge, map 'Footprint LOD'): theta_px = 2 tan(fov/2) / H; the footprint at distance z is z theta_px. Step: Delta(z) = max(k z theta_px, min(S / 4, lambda_fine(z) / 2)), k = 4: never finer than four footprints, otherwise the finer of the two content limits (four samples across the ramp, two per wavelength of the finest live octave). Octaves: octave i of wavelength lambda_i carries weight w_i(z) = 1 - smoothstep(lambda_i / (2m), lambda_i / m, z theta_px), m = 4: one smooth blend per octave, no popping; TAA hides per-frame sparsity. The weights are part of the field, so the bound encloses the truncated field exactly: the noise interval over a slice is the sum over octaves of iv_mul([w_i(z1), w_i(z0)], iv_snoise_ball(c F_i, r F_i)), w_i monotone in z. At the shipped parameters (wavelengths 100 and 25, z <= 2 Rc) every weight is 1 in the frustum and the content limits set the step; the footprint bites when S < 4 k z theta_px.",
"Where the scene sits and what to turn (Imagination's model, scratchpad cavity_model.py; the test and the browser confirm): interval-ops r2 measured the shipped point at 16.45 LOD steps and 26.6 snoise per pixel against 36.3 fixed steps (2.21x); r3's analytic body is predicted to bring the steps to about 12.5 (2.6-3.3x), snoise unchanged; the wall's step is set by S / 4 and the detail half-wavelength, not by distance; the browser confirms or refutes. Cheaper without losing the mood, in order: (1) a lower per-frame S_budget, which LOD and the mask make nearly lossless (S_budget = 4 converges in about three frames at the walls); (2) a thinner ramp S (S = 10 is 3.2x on cost but reads as a surface; 25-50 keeps the fog); (3) a longer detail wavelength or no detail octave near the far wall (the 12.5-unit half-wavelength is the floor for S >= 50); (4) a larger hollow (Rh = 1000: 2.5x) costs nothing per pixel and changes the composition. Camera offset barely matters at these scales.",
"Lighting, the cheapest version that keeps it moody (the original's own mechanism, made 3D): at each sample, light = SUN_COLOUR * (1 - (d / Rl)^2)^3 for d = |p - sun| < Rl, Rl = Rh = 198 (the original's 128 scaled with the hollow), plus AMBIENT * ambientTint; colour = density * (light + ambient) * hue(flow). No light march: zero extra snoise, one length and one pow per sample. The lower wall within Rl of the sun glows and the upper wall stays dark because the pulse is zero beyond Rl, which is the moody split; the sun inside the hollow is occluded by nothing, so the sun itself is a small additive disc drawn in the resolve at its projected position, occluded by the accumulated transmittance along its ray. Not in this cut, measured before it is ever added: one envelope-only transmittance sample toward the sun per primary sample (self-shadowing by the bowl's geometry), about +25% march cost; it is desktop-only if ever.",
"TAA with a moving camera. ground.js uploads this frame's and the previous frame's view-projection. For each pixel the resolve reconstructs the world point at the march's depth-weighted mean distance (written by the main pass into a second history channel, R16F), moves it back by the field motion (p + flow(p) * dt: the pattern moves at -flow), projects it with the previous VP and fetches history at that uv. The same 3x3 min/max clamp, alpha = max(1/frames, 1/32), out-of-bounds alpha 1; history reset on resize, first frame and budget step. Transmittance, colour and the depth channel are all reprojected by the one motion vector. ?ground=debug-mv shows |mv| with the camera moving and the flow frozen, and with the flow moving and the camera frozen, so both terms are seen separately.",
"Stardust distribution and the sun disc are the next cut (site-stardust), authored to the void: particles spawned in the bowl's volume with the original's height rule (more headroom where the well is deeper), lit by the same pulse, moving with the swirl. This cut leaves the composite slot and the depth history they need.",
"Fit with the tokens. Inputs: --gamecult-nebula-0 (orange 255 138 42) is SUN_COLOUR; --gamecult-nebula-2 (sky 89 183 255) is the ambient tint; --gamecult-nebula-1 (violet 109 96 255) is the cloud hue's second pole; --gamecult-nebula-base the sky. Outputs from the 8x8 readback as r2, with one change: --gamecult-light-0-at is written from the sun's projected screen position (quantised to 1%, at most 2 Hz), so the CSS wash's main light follows the sun while the shader runs; --gamecult-light-0 and -a come from the texel there; lights 1 and 2 keep their seed positions and sample their colours. When the canvas is absent the seeds paint a still of the same composition (the seed light-0-at is the sun's mean screen position).",
"Prove in the browser pane, in addition to r2's list: with ?ground=debug the pre-pass's mean probes per tile and the fraction of cells masked, and the mean snoise per pixel and the fraction of samples placed in the wall ramp against a dense-reference frame (?ground=dense: no pre-pass, S samples over the whole slab; the two converged frames' mean luminance agree within 1/255; the masked frame's per-frame variance at the walls is lower, printed); the camera reprojection test above; the orbit (after 36 s the sun's screen position has moved half an orbit and the TAA shows no smear at the walls); the contrast clamp with the lit cavity (max luminance <= 0.035 still holds; the sun disc is exempt only inside its own radius, which the clamp test masks and reports)."
],
"deletes": [
{
"path": "site/quartz/styles/custom.scss",
"lines": 6,
"note": "ritual body block 119-124: its own background goes; its seeds live under :root:has(body[data-slug]) (r2)."
},
{
"path": "site/quartz/styles/custom.scss",
"lines": 9,
"note": "body 10-27: the literal colours and stops become token references (r2)."
}
],
"keeps_moves": [
"r2's tokens, sampled wash, controller, vendoring (now two files: the site vendors CultMath.glsl only, MIT; the parity run concatenates CultMath.Phacelle.glsl for the phacelle family), parity flags, fallbacks and the contrast clamp: unchanged except where this revision names them.",
"--gamecult-wash and the index override from wash-home-only: untouched.",
".page::before, the tour/Pitch VN ground, the Thing deck's .hero-bg, the masthead, the engine: untouched.",
"The CSS wash stays the ground when the canvas is absent, not yet drawn, or removed by the controller."
],
"adds": [
"site/quartz/components/GameCultGround.tsx: as r2.",
"site/quartz/static/ground/ground.js (~500 lines): r2's passes plus worldAt(t) (sun, void, camera), the two view-projections, the pre-pass, the mask texture, the depth history, the sun-disc composite and the light-0-at writer.",
"site/quartz/static/ground/flow.frag.glsl (world-space window, swirl plus global), prepass.frag.glsl (the tile loop, mask and body-start out), cloud.frag.glsl (masked sampling, light, depth out), taa.frag.glsl (camera plus flow reprojection), downsample.frag.glsl.",
"site/quartz/static/ground/CultMath.glsl and glsl-parity.json, vendored from CultLib at interval-ops r3's head or later (cultmath_iv_frustum_ball with its float32 widening present).",
"scripts/ground/check-cultmath-glsl.ps1: the vendoring drift check.",
"custom.scss and docs/brand-design-language.md: as r2, with the void composition named in the brand doc's ground paragraph."
],
"file_changes": [
{
"location": {
"path": "site/quartz/styles/custom.scss",
"line": 4,
"end_line": 8
},
"change": ":root block: the fifteen ground seeds and --gamecult-ground-mean; light-0-at seeded at the sun's mean screen position."
},
{
"location": {
"path": "site/quartz/styles/custom.scss",
"line": 10,
"end_line": 31
},
"change": "body: four nebula input tokens; radials and the linear gradient reference the ground tokens (r2)."
},
{
"location": {
"path": "site/quartz/styles/custom.scss",
"line": 103,
"end_line": 124
},
"change": "ritual block: background declaration deleted; :root:has(body[data-slug]) carries its seeds and nebula inputs (r2)."
},
{
"location": {
"path": "site/quartz.layout.ts",
"line": 19
},
"change": "header: [GameCultThemeLock(), GameCultGround(), GameCultMasthead()]."
},
{
"location": {
"path": "docs/brand-design-language.md",
"line": 69,
"end_line": 84
},
"change": "wash section rewritten: the void scene, the sampled wash, the seeds, the nebula inputs and what each drives."
}
],
"authority_map": {
"owner": "The shader owns the ground. ground.js owns worldAt(t) (sun, void, camera), the flow texture, the pre-pass mask, the march, the TAA, the readback and the sampled tokens; custom.scss owns the four nebula inputs, --gamecult-wash and the seeds; CultLib owns every noise and interval function; the composed bound in prepass.frag.glsl is built from cultmath_iv_* only.",
"inputs": [
"the four nebula tokens and --gamecult-wash (computed on body)",
"prefers-reduced-motion, visibilityState, canvas size, devicePixelRatio, elapsed time",
"measured GPU frame time (timer query, fence, or rAF cadence)",
"the shader's own 8x8 readback and the sun's projected position",
"the vendored CultMath.glsl at its pinned CultLib commit"
],
"outputs": [
"one opaque moving canvas under the page, luminance <= 0.035 outside the sun disc",
"the fifteen ground tokens and --gamecult-ground-mean on :root inline, at most 2 Hz; light-0-at follows the sun",
"debug, dense-reference and parity readbacks (console, flag only)"
],
"derived_state": [
"sun, void and camera positions are derived from time by worldAt(t); nothing integrates motion frame to frame",
"the tile mask is derived each frame from that frame's camera and the field; it is never carried across frames",
"the flow texture is the one owner of motion: warp, hue, motion vectors and the stardust read it",
"the TAA history (colour, transmittance, depth) is a cache reprojected by camera and flow; reset on resize, first frame and budget step",
"the ground tokens are derived from the readback while the shader runs; the seeds are the fallback; the shader never reads them",
"S and dpr are derived from measurement within the session; the sessionStorage token set is a cache"
],
"forbidden_writers": [
"no colour, alpha, position or radius literal in ground.js or any GLSL file; the scene's dimensions (H0, Dw, Rw, F, S, Rl, orbit radius, period, camera offset) are named constants in one place in ground.js, uploaded as uniforms",
"no sample placed in a masked cell; no probe in the main pass; no early-out of the pre-pass on transmittance",
"no light march in this cut; the light is the analytic pulse",
"no read of the ground output tokens by the shader or ground.js except the seed read before the first readback",
"no userAgent, platform or maxTouchPoints; no class or attribute set on body or html; no second wash token; no second flow evaluation; no noise or interval function defined in the site's GLSL",
"no synchronous readPixels to client memory; one requestAnimationFrame scheduler; localStorage never"
],
"shared_paths": [
"every page paints one canvas and one token set; the ritual essays differ only in seeds and inputs",
"slow device, hidden tab, reduced motion, missing WebGL2 and missing half-float all pass through the one controller",
"the 8x8 readback and the sun projection are the only paths from the shader to CSS",
"the pre-pass and the main pass read the same uniforms from the same worldAt(t); the dense-reference mode differs only by skipping the mask"
],
"deletion_line": "As r2: the ritual block's own background and the body literals go in the same commit the tokens are declared. Nothing else in the engine or content changes. Against pass 2's design this revision removes the static camera and the everywhere-clouds field; it adds one pre-pass, one mask texture, one depth history channel and the scene function."
},
"verification": {
"builds": [
"Deploy Quartz green on the landing commit: gh run list -R GameCult/gamecult-site -w \"Deploy Quartz\" -L 1",
"scripts/ground/check-cultmath-glsl.ps1 exits 0 against a CultLib checkout at the pinned commit",
"the built stylesheet contains rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))) verbatim and the ritual :root:has rule"
],
"tests": [
{
"name": "browser pane, /?ground=debug: max relative luminance over the canvas outside the sun disc after 32 frames <= 0.035",
"pins": "text contrast with the lit cavity."
},
{
"name": "browser pane, /?ground=debug vs /?ground=dense: converged mean luminance within 1/255; steps and snoise per pixel printed for both, debug at most half of dense's steps; mask fraction printed",
"pins": "the mask culls only provably empty cells; the shipped march takes at most half the fixed-step steps; the saving on the shipped scene is a number."
},
{
"name": "browser pane, /?ground=debug-mv: |mv| non-zero with the camera moving and flow frozen, and with flow moving and camera frozen; zero under reduced motion",
"pins": "both reprojection terms are live and separable."
},
{
"name": "browser pane, / for 40 s: the sun's screen position moves about half an orbit; no history smear at the walls (3x3 clamp); light-0-at on :root follows within 2 s",
"pins": "the moving scene converges and the CSS wash follows the sun."
},
{
"name": "browser pane, /?ground=parity: every fixture family within its tolerance class on WebGL2, cultmath_iv_frustum_ball exact; numbers and GPU string in the report",
"pins": "GLSL = C# on a device, including the tile ball."
},
{
"name": "browser pane, /?ground=parity-wash and a ritual essay: canvas vs CSS seed wash within 2/255 at 16 points with density 0 and the sun disc off",
"pins": "the seeds and the shader's base agree."
},
{
"name": "reduced motion, hidden tab, WebGL2 disabled, sessionStorage cleared: as r2",
"pins": "every fallback goes through the controller."
},
{
"name": "git diff base..head --stat: custom.scss, quartz.layout.ts, docs/brand-design-language.md, the new component, static/ground/*, scripts/ground/* only",
"pins": "no engine, content or masthead change."
}
],
"negative": [
{
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(|\\b(255|138|109|183)\\b",
"scope": "site/quartz/static/ground/*.js and *.glsl: 0 matches."
},
{
"pattern": "gamecult-ground-|gamecult-light-",
"scope": "site/quartz/static/ground/*.glsl: 0 matches; in ground.js only inside the token writer and the seed read."
},
{
"pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|setInterval|classList|setAttribute\\(",
"scope": "site/quartz/static/ground/ground.js: 0 matches."
},
{
"pattern": "cultmath_iv_",
"scope": "cloud.frag.glsl, taa.frag.glsl, flow.frag.glsl: 0 matches; the bound lives only in prepass.frag.glsl."
},
{
"pattern": "float cultmath_|vec4 cultmath_|float snoise|hash\\(",
"scope": "every site .frag.glsl: 0 matches; cultmath_* is defined only in the vendored CultMath.glsl."
},
{
"pattern": "Mozilla|MPL|cultmath_phacelle",
"scope": "site/quartz/static/ground/CultMath.glsl: 0 matches; the site vendors the MIT file."
},
{
"pattern": "radial-gradient\\([^)]*rgba?\\([0-9]",
"scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches."
},
{
"pattern": "gamecult-titlebar|gamecult-nav-link|sidebar|composite|GameCult-Quartz",
"scope": "git diff base..head changed lines: 0 matches."
}
],
"operator": [
"open / on the desktop and on the phone; say whether the hollow, the sun's orbit and the camera's height read as the old main menu's mood (uniforms: A, SWIRL, Rl, CARVE, the camera offset)",
"on the phone with ?ground=debug, read the logged GPU ms per pass and the controller's final S/dpr; say whether it got warm after five minutes",
"say whether the analytic light is moody enough or the self-shadowing sample (desktop only) should be tried",
"navigate / -> /Blog -> back; say whether the ground is continuous (Q5)"
]
},
"estimate": {
"lines_added": 1250,
"lines_removed": 15,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [
"GLSL ES 3.00 shader files in static/ground",
"vendored glsl-parity.json",
"one sessionStorage key holding the last sampled token set",
"an RGBA32UI tile-mask texture and an R16F depth history (GPU-side only)"
],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"cultmath-tapes:ruling:operator-ground-void-brush",
"cultmath-tapes:ruling:operator-void-true-cavity",
"cultmath-tapes:ruling:operator-tile-amortized-march",
"cultmath-tapes:ruling:operator-analytic-wall-body",
"cultmath-tapes:ruling:operator-split-phacelle",
"site-masthead:ruling:operator-chromeless-column"
],
"questions": [
"Q4 in the map: palette input tokens (a, recommended)",
"Q5 in the map: carry the last sampled wash across page loads (a, recommended)",
"Q9 in the map: the one shipped void (a, recommended: Rh 198, S 50, Rc 248, e 0.40); ruled in interval-ops r3's fork, this spec follows it"
]
}

View file

@ -0,0 +1,206 @@
{
"campaign": "cultmath-tapes",
"cut": "site-ground",
"revision": 2,
"title": "Site ground: a per-pixel WebGL2 nebula under every page, flow-warped, interval-culled, TAA-reprojected, budget-controlled by measurement; the CSS wash is sampled from the result, seeds fall back",
"repo": "GameCult/gamecult-site",
"branch": "main",
"base": "535d170",
"depends_on": [
"site-masthead:cut_spec:wash-home-only.r1",
"cultmath-tapes:cut_spec:glsl-lowering.r1",
"cultmath-tapes:cut_spec:interval-ops.r1"
],
"first": [
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'The shader design' (field, culling with the warp, TAA motion-aware, sampled wash, palette input), 'Budget', D4-D8, Q4 and Q5. Rulings in force: site-masthead:ruling:operator-chromeless-column; wash-home-only's authority map (no second wash token, no class toggled by script, no page rule setting a radial alpha). Re-pin base to main after wash-home-only lands; line numbers below are from 535d170; re-read by selector. Stardust and DoF are the next cut: leave the resolve pass with a named, empty composite slot for them (a uniform-gated branch is not needed; a comment and a function stub are).",
"Aetheria reference, read once, copy nothing: Volumetric.cginc:165-221 (two-phase flow-map warp, tri2 cross-fade), :130-135 (globalFlow); Raymarching/CloudShader.shader:83-104 (integration) and :261-314 (what not to do: gamma 0.5 clip, fixed 0.05 blend, packed depth, the density2 bug at :311).",
"Engine facts to confirm before writing: how GameCult-Quartz bundles a component's afterDOMLoaded and whether a dynamic import() of /static/ground/ground.js is allowed, else emit a module script tag; site/quartz/static/** served at /static/**; enableSPA false (quartz.config.ts:15). Local build: scripts/quartz/quartz.ps1 dev.",
"Tokens. Inputs (Q4 a), on body in custom.scss beside --gamecult-wash: --gamecult-nebula-0/1/2 (\"255 138 42\", \"109 96 255\", \"89 183 255\") and --gamecult-nebula-base (#07111a). Outputs with seeds, on :root: --gamecult-ground-0/1/2 (#03070d #07111a #09141f), and for N in 0..2 --gamecult-light-N (rgb triple), -a (0.18/0.16/0.14), -at (\"78% 14%\"/\"18% 10%\"/\"52% 0%\"), -r (18%/24%/28%), plus --gamecult-ground-mean (#07111a). The body background becomes radial-gradient(circle at var(--gamecult-light-0-at), rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))), transparent var(--gamecult-light-0-r)) x3 over linear-gradient(180deg, var(--gamecult-ground-0) 0%, var(--gamecult-ground-1) 44%, var(--gamecult-ground-2) 100%).",
"Ritual essays: their seeds go under :root:has(body[data-slug=\"Blog/...\"]) for the three slugs (yellow 255 178 30 .15 at 78% 8% 24rem; cobalt 49 88 255 .22 at 18% 4% 30rem; violet 91 54 184 .19 at 44% 20% 34rem; stops #030414 0%, #070b24 42%, #040514 100%; nebula inputs likewise overridden to yellow/cobalt/violet and base #050614). The ritual block's own background declaration is deleted; it inherits body's.",
"ground.js owns: WebGL2 context on #gamecult-ground (alpha:false, antialias:false, powerPreference 'low-power'); capability check (WebGL2, EXT_color_buffer_half_float or float); input token read via getComputedStyle(document.body) -> uniforms; the flow texture pass (quarter res, RG16F, two snoise per texel, once per frame); the march pass; the TAA resolve with reprojection; the 8x8 downsample and the async PBO readback; the token writer on document.documentElement.style; the scheduler (30 fps cadence, visibilityState pause, reduced-motion freeze and stop at 32 frames); the budget controller; the debug flag (?ground=debug) and parity flags.",
"Sampled wash: after the resolve, draw the final frame into an 8x8 RGBA8 target; gl.readPixels into a PIXEL_PACK_BUFFER; gl.fenceSync; on later frames poll getSyncParameter(SYNC_STATUS) and only when SIGNALED call getBufferSubData. Never readPixels to client memory directly (that stalls). At most 2 Hz. Compute the token values as the map states (row means for the stops; light colour and excess at the seed light's centre texel; the mean for --gamecult-ground-mean) and write them with setProperty on :root only when a value moved by more than 1/255. Q5 (a): keep the last written set in sessionStorage under one key and apply it on load before the first frame; it is a cache, and a missing or malformed entry means the seeds.",
"Budget controller, one function, one state machine: GPU time per frame by EXT_disjoint_timer_query_webgl2 when present, else a fenceSync after the resolve polled to completion, else rAF cadence. Over a 60-frame window: if the median exceeds the budget (4.0 ms), step down: S 16 -> 8 -> 4, then dpr -> 1 (Q2), then remove the canvas (the seeds paint). Never step up within a session. A median rising 50% over 5 minutes also steps down. No storage of the verdict, no userAgent, no maxTouchPoints.",
"cloud.frag.glsl: #version 300 es; precision highp float; precision highp int; the vendored CultMath.glsl is concatenated by ground.js before this file's body. The field, warp, interval march (ball radius F0 * (Δ/2 + D) with D = FLOW_MAX * PERIOD / 2) and integration exactly as the map's pseudocode; hue from the flow texture's channels mixed over the three nebula inputs; uniforms: resolution, time, dt, frame, jitter, S, intensity, nebula colours, base, Z0/Z1, F0, CUTOFF, K, PERIOD, FLOW_MAX. flow.frag.glsl: the two low-frequency snoise. taa.frag.glsl: motion vector from the flow texture (mv = project(-flow * dt)), history fetched at uv - mv, 3x3 min/max clamp, alpha = max(1/frames, 1/32), out-of-bounds alpha 1; writes new history and the screen (luminance clamp 0.035) via drawBuffers or two draws.",
"Vendoring (D3): site/quartz/static/ground/CultMath.glsl byte-equal to CultLib shaders/CultMath.glsl at the commit named in its first line. scripts/ground/check-cultmath-glsl.ps1 diffs it against git show <sha>:packages/cultmath/shaders/CultMath.glsl from a CultLib checkout and exits non-zero on drift.",
"Parity on the device: with ?ground=parity, ground.js evaluates the vendored fixture (CultLib tests/CultMath.Tests/fixtures/glsl-parity.json) one pixel per point, reads back float bit patterns, and reports per function the max ulp distance and the count outside its tolerance class. With ?ground=parity-wash, density and stardust are forced to 0 and the flow frozen; the canvas must equal the CSS seed wash within 2/255 at 16 points on / and on a ritual essay (this pins that the seed values and the shader's base agree at zero density, which is what the fallback promises).",
"Prove in the browser pane: max relative luminance after 32 frames on / at intensity 1 (<= 0.035); the parity-wash probe; the debug readback (mean evals per pixel, unfinished-ray fraction < 1%); the token writer (after 2 s on /, :root's inline style carries all fifteen tokens and --gamecult-ground-mean, each within the expected range, and getComputedStyle(body).backgroundImage resolves to the sampled values); the motion test (with ?ground=debug-mv the resolve writes |mv| into red: non-zero where flow is non-zero, zero under reduced motion); GPU ms per pass with the timer query at the pane's desktop size and at the mobile preset's pixel count. Phone GPU time is operator verification."
],
"deletes": [
{
"path": "site/quartz/styles/custom.scss",
"lines": 6,
"note": "ritual body block 119-124: its own background (three radials and the linear gradient) goes; its seeds live under :root:has(body[data-slug]) and it inherits body's ground."
},
{
"path": "site/quartz/styles/custom.scss",
"lines": 9,
"note": "body 10-27: the six literal colours and three literal stops leave as literals (rewritten as token references in the same lines; counted 9 removed, 9 added)."
}
],
"keeps_moves": [
"--gamecult-wash and the index override from wash-home-only: untouched; the shader reads it as intensity and the CSS wash still multiplies by it.",
".page::before (the grid overlay), the tour/Pitch VN ground, the Thing deck's .hero-bg, the masthead, the engine: untouched.",
"The CSS wash stays the ground when the canvas is absent, not yet drawn, or removed by the controller; it renders the sampled tokens when there are any and the seeds otherwise.",
"docs/brand-design-language.md 'The ground is a wash, not a flat fill' (69-84): rewritten: the ground is the shader; the CSS wash is sampled from it; the seeds are today's literal wash; the four nebula inputs are where the palette is tuned."
],
"adds": [
"site/quartz/components/GameCultGround.tsx: renders <canvas id=\"gamecult-ground\" aria-hidden=\"true\"></canvas>, mounts /static/ground/ground.js after DOM load; in sharedPageComponents.header after GameCultThemeLock (quartz.layout.ts:19). Its beforeDOMLoaded script applies the sessionStorage token cache to :root if present (Q5 a), so the first paint matches the last sample.",
"site/quartz/static/ground/ground.js (~400 lines, no dependencies, no build step): context, tokens in and out, flow/march/resolve/downsample passes, PBO readback, scheduler, controller, debug and parity modes, a named composite slot for the stardust cut.",
"site/quartz/static/ground/flow.frag.glsl, cloud.frag.glsl, taa.frag.glsl, downsample.frag.glsl; a shared 3-vertex full-screen vertex shader inline in ground.js.",
"site/quartz/static/ground/CultMath.glsl and glsl-parity.json, vendored from CultLib with the pin in the header.",
"scripts/ground/check-cultmath-glsl.ps1: the vendoring drift check.",
"custom.scss: the four nebula input tokens on body; the fifteen ground seeds plus --gamecult-ground-mean on :root with the ritual overrides under :root:has(body[data-slug]); #gamecult-ground { position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }.",
"docs/brand-design-language.md: the wash section rewritten as above; one paragraph on the ground shader: what it reads (inputs), what it writes (outputs), what it may not hold, and the fallback."
],
"file_changes": [
{
"location": { "path": "site/quartz/styles/custom.scss", "line": 4, "end_line": 8 },
"change": ":root block: the fifteen ground seeds and --gamecult-ground-mean declared beside color-scheme."
},
{
"location": { "path": "site/quartz/styles/custom.scss", "line": 10, "end_line": 31 },
"change": "body: four nebula input tokens declared; radials and the linear gradient reference the ground tokens; alphas keep the wash-home-only calc."
},
{
"location": { "path": "site/quartz/styles/custom.scss", "line": 103, "end_line": 124 },
"change": "ritual block: background declaration deleted; a :root:has(body[data-slug]) rule carries its seeds and nebula inputs."
},
{
"location": { "path": "site/quartz.layout.ts", "line": 19 },
"change": "header: [GameCultThemeLock(), GameCultGround(), GameCultMasthead()]."
},
{
"location": { "path": "docs/brand-design-language.md", "line": 69, "end_line": 84 },
"change": "wash section rewritten: shader-owned ground, sampled wash, seeds, nebula inputs."
}
],
"authority_map": {
"owner": "The shader owns the ground. custom.scss owns the four nebula inputs, --gamecult-wash and the seed values; ground.js owns the flow texture, the march, the TAA, the readback and the sampled ground tokens on :root; CultLib owns every noise and interval function the field calls.",
"inputs": [
"the four nebula tokens and --gamecult-wash (computed on body)",
"prefers-reduced-motion, visibilityState, canvas size, devicePixelRatio, elapsed time",
"measured GPU frame time (timer query, fence, or rAF cadence)",
"the shader's own 8x8 readback",
"the vendored CultMath.glsl at its pinned CultLib commit"
],
"outputs": [
"one opaque moving canvas under the page, luminance <= 0.035",
"the fifteen ground tokens and --gamecult-ground-mean on :root inline, at most 2 Hz",
"debug and parity readbacks (console, flag only)"
],
"derived_state": [
"the ground tokens are derived from the shader's result while it runs; the seeds are the fallback; the CSS wash renders whichever is in force; the shader never reads them",
"the flow texture is the one owner of motion: warp, hue, motion vectors (and the stardust, next cut) read it",
"the TAA history is a cache, reprojected by the flow; reset on resize, first frame and budget step",
"S and dpr are derived from measurement within the session; never stored",
"the sessionStorage token set is a cache of the last sample; absent or malformed means seeds",
"CultMath.glsl in the site is derived from CultLib at the pinned commit"
],
"forbidden_writers": [
"no colour, alpha, position or radius literal in ground.js or any GLSL file",
"no read of --gamecult-ground-* or --gamecult-light-* by the shader or by ground.js, except the seed read before the first readback",
"no navigator.userAgent, platform, maxTouchPoints, or any device-name branch",
"no class or attribute set on body or html by ground.js; token writes are setProperty on :root only",
"no second wash token; no page rule setting a radial alpha; no second flow evaluation outside flow.frag.glsl",
"no noise or interval function defined in the site's GLSL; only cultmath_* calls",
"no synchronous readPixels to client memory; no setInterval; one requestAnimationFrame scheduler; localStorage never"
],
"shared_paths": [
"home, every content page and the ritual essays paint one canvas and one token set, in CSS and in the shader",
"slow device, hidden tab, reduced motion, missing WebGL2 and missing half-float all pass through the one controller",
"the 8x8 readback is the only path from the shader to CSS; the beforeDOMLoaded cache apply and the live writer set the same tokens the same way",
"the same Deploy Quartz workflow; static files ship with the overlay"
],
"deletion_line": "The ritual block's own background and the nine body literals go in the same commit the tokens are declared. The canvas, four shaders, ground.js and the vendored library are the whole addition; nothing else in the engine or the content changes."
},
"verification": {
"builds": [
"Deploy Quartz green on the landing commit: gh run list -R GameCult/gamecult-site -w \"Deploy Quartz\" -L 1",
"scripts/ground/check-cultmath-glsl.ps1 exits 0 against a CultLib checkout at the pinned commit",
"the built stylesheet contains rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))) verbatim and a :root:has(body[data-slug=\"Blog/small-ritual-of-reach\"]) rule"
],
"tests": [
{
"name": "browser pane, /?ground=debug: max relative luminance over the canvas after 32 frames <= 0.035",
"pins": "text contrast: body text keeps >= 7:1 on every ground pixel."
},
{
"name": "browser pane, /?ground=parity-wash and a ritual essay: canvas vs CSS seed wash within 2/255 at 16 points with density 0 and flow frozen",
"pins": "the seeds and the shader's base agree; the fallback promise holds."
},
{
"name": "browser pane, / after 2 s: :root inline style carries the sixteen tokens; body's computed backgroundImage resolves to the sampled values; a read 1 s later differs only where the frame changed",
"pins": "the CSS wash is sampled from the shader, at most 2 Hz, without a stall (no readPixels-to-client in the profiler)."
},
{
"name": "browser pane, /?ground=parity: every fixture function within its tolerance class on WebGL2; numbers and GPU string in the report",
"pins": "GLSL = C# on a device; the proving ground's third runtime."
},
{
"name": "browser pane, /?ground=debug at desktop and the mobile preset: mean evals per pixel, unfinished rays < 1%, GPU ms per pass; ?ground=debug-mv shows motion where flow is non-zero",
"pins": "the step budget, the interval saving with the warp, and the reprojection on the shipped field."
},
{
"name": "reduced motion emulated: frames stop at 32, mv is 0, time frozen; hidden tab: no frames; WebGL2 disabled: no canvas in the DOM and the seed wash paints; sessionStorage cleared: seeds paint first",
"pins": "every fallback goes through the controller and lands on the same CSS ground."
},
{
"name": "git diff base..head --stat: custom.scss, quartz.layout.ts, docs/brand-design-language.md, the new component, static/ground/*, scripts/ground/* only",
"pins": "no engine, content or masthead change."
}
],
"negative": [
{
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(|\\b(255|138|109|183)\\b",
"scope": "site/quartz/static/ground/*.js and *.glsl: 0 matches."
},
{
"pattern": "gamecult-ground-|gamecult-light-",
"scope": "site/quartz/static/ground/*.glsl: 0 matches; in ground.js only inside the token writer and the seed read, never in a uniform upload."
},
{
"pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|setInterval|classList|setAttribute\\(",
"scope": "site/quartz/static/ground/ground.js: 0 matches."
},
{
"pattern": "readPixels\\([^)]*(Uint8Array|Float32Array|Uint16Array)",
"scope": "ground.js: 0 matches outside the parity and debug readbacks, which are flag-gated and named."
},
{
"pattern": "float cultmath_|vec4 cultmath_|float snoise|hash\\(",
"scope": "flow/cloud/taa/downsample.frag.glsl: 0 matches; cultmath_* is defined only in the vendored CultMath.glsl."
},
{
"pattern": "radial-gradient\\([^)]*rgba?\\([0-9]",
"scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches."
},
{
"pattern": "gamecult-titlebar|gamecult-nav-link|sidebar|composite|GameCult-Quartz",
"scope": "git diff base..head changed lines: 0 matches."
}
],
"operator": [
"open / on the desktop and on the phone; say whether the nebula's motion and intensity are right (uniforms: PERIOD, FLOW_MAX, intensity)",
"on the phone with ?ground=debug, read the logged GPU ms and the controller's final S/dpr; say whether it got warm after five minutes",
"navigate / -> /Blog -> back; say whether the ground is continuous or shows a seed flash (Q5)",
"open a ritual essay and confirm its ground still reads as its own paper"
]
},
"estimate": {
"lines_added": 980,
"lines_removed": 15,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": ["GLSL ES 3.00 shader files in static/ground", "vendored glsl-parity.json", "one sessionStorage key holding the last sampled token set"],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"site-masthead:ruling:operator-chromeless-column"
],
"questions": [
"Q4 in the map: palette input tokens (a, recommended)",
"Q5 in the map: carry the last sampled wash across page loads (a, recommended)"
]
}

View file

@ -0,0 +1,141 @@
{
"campaign": "cultmath-tapes",
"cut": "site-stardust",
"revision": 1,
"title": "Site stardust: stateless particles advected by the ground's flow texture, rendered to a half-res layer, blurred by the clouds' accumulated opacity (raymarch-driven DoF), composited behind the nebula",
"repo": "GameCult/gamecult-site",
"branch": "main",
"base": "535d170",
"depends_on": [
"cultmath-tapes:cut_spec:site-ground.r2"
],
"first": [
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'Stardust', 'Depth of field driven by the raymarch', 'Budget', D4, D9, D10. Re-pin base to main after site-ground lands. The composite slot site-ground left in the resolve is where this cut plugs in; do not restructure the resolve.",
"Aetheria reference, read once, copy nothing: Compute/Stardust/Stardust.compute:118-147 (grid cell -> hashed spawn, lifetime = frac(time/period + hash), position -= flow * lifetime * period, size = parabola(lifetime, 2), colour from a hue ramp x nebula tint); Stardust.shader:49-86 (billboard from quadPoints by SV_VertexID/SV_InstanceID, powerPulse falloff, distance exponents); CustomDoF/DepthOfField.hlsl:29-32 (CoC read from a raymarch output), :150-201 (disk gather weighted by CoC), :217-242 (combine). The web version is far smaller: no distance exponents (the slab is thin), no shadow pass, no near/far split.",
"Particles are stateless: no buffers are updated. One static 6-vertex quad VBO; drawArraysInstanced with count N; the vertex shader derives everything from gl_InstanceID, time and the flow texture (vertex texture fetch is standard in WebGL2): cell = (id mod G, id / G) over the slab mid-plane; hashes via cultmath_pcg3d (the vendored CultMath.glsl is concatenated before the vertex source too) for spawn jitter, lifetime offset, size and brightness; lifetime = fract(t / PERIOD_P + offset); position = spawn - flow(spawn uv) * lifetime * PERIOD_P; size = base * parabola(lifetime, 2); colour = hue at the screen position (from the flow texture's channels, the same mix as the clouds) * brightness. Reduced motion freezes t as site-ground does.",
"Stardust layer: half-resolution RGBA16F target, cleared each frame, additive blend, powerPulse falloff in the fragment shader; N = 16384 desktop, 4096 phone; the controller scales N with S (N halves when S halves) and the layer follows dpr.",
"DoF: in the resolve's composite slot, read opacity = 1 - T from the reprojected, clamped history (not the current frame) and gather the stardust layer with an 8-tap Poisson disk of radius RMAX * opacity in layer texels (RMAX ~ 12 px at dpr 1, scaled by dpr); weight taps by a soft disk; composite out = ground + clouds + stardust_blurred * T, then the luminance clamp. All stardust is behind the whole cloud layer (D9).",
"Debug flags: ?ground=debug-stardust writes the stardust layer alone; ?ground=debug-coc writes opacity into red. Prove in the browser pane: the stardust and the clouds move together (with ?ground=debug-mv from site-ground and the particle velocity written in green under ?ground=debug-stardust-v, the two directions agree within 5 degrees at 64 sampled pixels where |flow| is above a threshold); luminance clamp still holds after the composite; GPU ms per pass (stardust layer, gather) with the timer query at desktop and at the mobile preset's pixel count; the full-stack total against the budget table; the unfinished-ray fraction unchanged by this cut.",
"Budget contract (map, 'Budget'): the stardust layer plus the gather cost no more than a quarter of the march at the same S; if the measurement says otherwise, the gather drops to 4 taps before N drops, and the report says which. Stardust is never cut before the clouds' S on the ladder."
],
"deletes": [],
"keeps_moves": [
"site-ground's flow texture, march, TAA, readback and token writer: untouched; the stardust reads the flow texture and the hue mix the same way the clouds do.",
"The sampled wash now includes the stardust in its 8x8 readback by construction (it samples the final frame); nothing changes in the writer.",
"The budget controller gains one scaled quantity (N) and no new state; the ladder order is unchanged."
],
"adds": [
"site/quartz/static/ground/stardust.vert.glsl and stardust.frag.glsl: the instanced billboard and the powerPulse sprite.",
"ground.js: the stardust pass (one VBO, one program, drawArraysInstanced), the half-res layer, N in the controller, the gather in the resolve's composite slot, the two debug flags.",
"taa.frag.glsl: the composite slot filled: opacity from history, the 8-tap gather, out = ground + clouds + stardust_blurred * T.",
"docs/brand-design-language.md: two sentences under the ground shader paragraph: the stardust and the DoF, and that they read the same flow and hue as the nebula."
],
"file_changes": [
{
"location": { "path": "site/quartz/static/ground/ground.js", "line": 1 },
"change": "stardust pass, layer, N scaling, gather hookup, debug flags; line numbers are site-ground's and are re-read by function name."
},
{
"location": { "path": "site/quartz/static/ground/taa.frag.glsl", "line": 1 },
"change": "the named composite slot becomes the opacity-weighted gather and the final composite."
},
{
"location": { "path": "docs/brand-design-language.md", "line": 69 },
"change": "two sentences on stardust and DoF under the ground shader paragraph (post site-ground numbering)."
}
],
"authority_map": {
"owner": "ground.js owns the stardust pass and the DoF gather; the flow texture (site-ground) owns the particles' motion; the TAA history owns the opacity the DoF reads; the nebula input tokens own the particles' hue through the same mix as the clouds.",
"inputs": [
"the flow texture and the hue mix (site-ground)",
"the reprojected, clamped history's transmittance",
"time, N and dpr from the controller; gl_InstanceID"
],
"outputs": [
"a half-res stardust layer per frame",
"the composite with stardust blurred by cloud opacity behind the nebula, still under the luminance clamp"
],
"derived_state": [
"every particle's position, lifetime, size and colour are derived from gl_InstanceID, time and the flow texture each frame; nothing is stored or updated",
"N is derived from S by the controller; the layer size from dpr",
"the blur radius is derived from the accumulated opacity; there is no second CoC source"
],
"forbidden_writers": [
"no particle buffer update, transform feedback or CPU-side particle state",
"no second flow function or hue mix; the vertex shader samples the flow texture",
"no colour literal; no per-depth transmittance; no second march",
"no new controller state beyond N; the ladder order is not changed",
"no change to the readback or the token writer"
],
"shared_paths": [
"clouds and stardust read one flow texture and one hue mix, so they move and colour as one",
"the one resolve pass composites ground, clouds and stardust and applies the one luminance clamp",
"the one controller scales S, N and dpr together"
],
"deletion_line": "Nothing is deleted. The addition is two small shaders, one pass and one gather; it buys the stardust and the DoF the operator named, and it must prove it fits the same budget."
},
"verification": {
"builds": [
"Deploy Quartz green on the landing commit",
"scripts/ground/check-cultmath-glsl.ps1 still exits 0"
],
"tests": [
{
"name": "browser pane, /?ground=debug-stardust-v with ?ground=debug-mv: particle velocity and cloud motion vectors agree within 5 degrees at 64 sampled pixels where |flow| exceeds the threshold",
"pins": "one flow: the stardust and the nebula move as one."
},
{
"name": "browser pane, /?ground=debug-coc: opacity is 0 where the clouds are empty and rises inside them; the stardust gather radius follows it (a visible blur under a dense cloud, sharp in a gap)",
"pins": "the raymarch drives the DoF, as in Aetheria's hack."
},
{
"name": "browser pane, /?ground=debug: max relative luminance after 32 frames <= 0.035 with stardust on",
"pins": "the clamp holds after the composite."
},
{
"name": "browser pane, timer query at desktop and at the mobile preset: stardust layer + gather <= 25% of the march at the same S; full-stack totals quoted against the budget table",
"pins": "the stardust fits the same budget or the report says what gave."
},
{
"name": "reduced motion: particles frozen, frames stop at 32; hidden tab: no frames",
"pins": "the stardust obeys the one controller."
}
],
"negative": [
{
"pattern": "bufferSubData|transformFeedback|TRANSFORM_FEEDBACK|new Float32Array\\((N|count|particles)",
"scope": "ground.js stardust section: 0 matches; particles are stateless."
},
{
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(",
"scope": "stardust.vert.glsl, stardust.frag.glsl, ground.js: 0 matches."
},
{
"pattern": "snoise\\(|float cultmath_",
"scope": "stardust.vert.glsl and stardust.frag.glsl: 0 matches; the flow and hue come from the texture, not from a second evaluation."
},
{
"pattern": "readPixels|setProperty",
"scope": "git diff base..head changed lines in ground.js: 0 matches; the readback and the token writer are not touched."
}
],
"operator": [
"open / on the desktop and on the phone; say whether the stardust sells the motion and whether the blur under the clouds reads like the video (RMAX, N, PERIOD_P are the dials)",
"on the phone with ?ground=debug, read the full-stack GPU ms and the final S/N/dpr; say whether it got warm"
]
},
"estimate": {
"lines_added": 320,
"lines_removed": 4,
"dependencies_added": [],
"dependencies_removed": [],
"formats_added": [],
"formats_removed": [],
"targets_added": [],
"targets_removed": []
},
"rulings": [
"site-masthead:ruling:operator-chromeless-column"
],
"questions": []
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,125 @@
# CultMath tapes: interval-pruned field evaluation on CPU and GPU
Status: steps 2 and 4 (graph, C# evaluator, HLSL interpreter) unparked on
2026-10-14 (ruling tape-unparked-for-trophic-graphs, which supersedes
tape-target-unparks-when-asura-stable); their model page and cut sequence are
`docs/cultmath-interval-ground-cut.md`, "Pass 7". Steps 3 and 5 (pruning,
meshing) stay parked. The affine-forms
piece was unparked on 2026-10-03 (ruling affine-forms-unparked) and is on main
(156782da), alongside step 1's intervals; its map and as-built record are in
`docs/cultmath-interval-ground-cut.md`. This note records the direction for the
parked steps so they can be picked up without re-deriving it.
## Intent
Evaluate large implicit fields, meaning deeply nested CSG trees of brushes with
thousands of nodes, cheaply enough to mesh at many levels of detail and to
raymarch. The operator's words (2026-10-02): "I would love to design a space
station out of a grammar of nested brushes, and have the ability to
automatically mesh it at multiple levels of detail so I can get arbitrarily
close to its tiny details without melting my GPU."
GameCult has no deep CSG trees today because nothing could evaluate them
cheaply. The capability comes first, and the content follows.
## Prior art
[Fidget](https://www.mattkeeter.com/projects/fidget/) by Matt Keeter
(MPL-2.0, Rust, v0.5.1 on 2026-09-27; repo `mkeeter/fidget`).
- It compiles an expression to a register-allocated bytecode tape.
- It evaluates that tape as points, SIMD batches, intervals, and forward-mode
gradients.
- The central trick is interval evaluation over a region. When it proves that
one operand of a `min` or `max` always wins, the tape for that region is
rewritten without the other branch. In the project's example, the tape
shrinks from 254 clauses to about 20 at pixel level.
- `fidget-jit` targets x86-64 and AArch64.
- `fidget-wgpu` has a WGSL tape interpreter, interval ops and tape
simplification on the GPU (`interval_ops.wgsl`, `tape_interpreter.wgsl`,
`tape_simplify.wgsl`).
- `fidget-mesh` implements Manifold Dual Contouring. It is known to be weak on
thin features.
- Halfspace is the IDE codesigned with `fidget-wgpu` (Rust, egui and wgpu,
MPL-2.0).
## Chosen shape (option c)
The ideas are ported into CultLib and owned here. Fidget is not a dependency.
Unity renders HLSL on its own graphics backend, so `fidget-wgpu` cannot run
inside it.
Three options were weighed on 2026-10-02:
| Option | What it is | Trade |
|---|---|---|
| a | Fidget as a Rust native plugin in Unity | Fastest demo; costs per-platform native builds, an FFI boundary, and an upstream that warns of breaking changes |
| b | Offline bake with a Rust tool; ship meshes | Simplest; the baked depth caps how close a viewer can get |
| c | CultLib owns tape, interval and pruning in C# and HLSL | Most coherent end state; the largest code liability we would own |
The operator parked (c). An (a) spike to validate authoring and LOD behaviour
before owning code was suggested but not ruled.
Sketch, to be replaced by the target pass:
1. **CultMath `interval`** in C# and HLSL with parity tests:
- arithmetic, `min`/`max`, `smin`, `sqrt`, `clamp` and `smoothstep`;
- interval or Lipschitz bounds for every CultMath noise primitive.
This step needs no tape. It stands alone, and is the first cut to take even
if the rest stays parked.
2. **Tape**: an expression graph lowered to bytecode, with point, interval and
gradient evaluators in C#. Gradients reuse CultMath's analytic-gradient
primitives, which Asura cut 2a built.
3. **Pruning**: per-region tape simplification driven by interval results.
4. **HLSL interpreter** for the same bytecode, with interval ops and pruning.
Fidget's MPL-2.0 WGSL is the reference. MPL code keeps its own MPL-headed
files, as Phacelle does.
5. **Meshing**: an octree of chunks with seams stitched between LOD levels.
- The default is surface nets (GameCult.Geometry, Asura Cut 2) plus
Asura's tile pass: Newton refinement onto `f = 0`, then crease snapping
from field gradients (Asura Cut 6c).
- The operator's lean (2026-10-02) is that gradient work beats dual
contouring here. Thin authored bevels can be shaded by their analytic
normals, and the refined positions can be iterated further.
- Dual contouring is not planned. The tile pass on station brushes is its
test.
## Truth and lowerings
The field is data: a typed CultCache document holding the expanded tree. A
generator, such as a brush grammar, expands into that document, and everything
downstream caches it. The CPU evaluator, the HLSL interpreter, the mesher and
collision are all lowerings of that one document. This is how "one
implementation of the field" survives having both CPU and GPU evaluators.
Asura's invariant 2 had to forbid CPU evaluation because its field is HLSL
source.
## Known consumers
- **Aetheria volumetric raymarching** (clouds, nebulae). Step 1 alone gives it
empty-space skipping. Coverage and shape terms carry the culling, because
detail octaves only bound to ±amplitude.
- Prerequisite: Aetheria's shaders must use CultMath noise, not their own
copies. That is already owed at Aetheria's CultMath pin bump (see the Asura
cut map, snoise release follow-ups).
- **Space stations from a brush grammar**, meshed in LOD chunks around the
camera. This is a separate project with its own target. This campaign
supplies its evaluator.
- Asura does not need this campaign. Its field is noise-dominated, and its
`A_max` shell and band-limiting already give the cheap culling.
## Decided since the unpark, and what stays open
- The spike-or-(c) choice was settled by the operator's unpark: the tape
unparked for Aetheria's trophic species graphs (ruling
`cultmath-tapes:ruling:tape-unparked-for-trophic-graphs`).
- Interval representation: affine forms for noise and warp, unparked on
2026-10-03 (ruling `cultmath-tapes:ruling:affine-forms-unparked`): one
shared symbol, mapped in `docs/cultmath-interval-ground-cut.md`, "Pass 5".
- Register model: the HLSL interpreter keeps its registers in groupshared
memory, a slice per thread, linear-scan allocated at lowering (ruling
`cultmath-tapes:ruling:tape-register-model-ruled`; research in
`docs/research/tape-register-model.md`).
- Open: whether the brush-grammar station project lives in Aetheria or in
its own repo (follow-up `cultmath-tapes:follow_up:brush-grammar-station-repo`,
raised as an operator question when a station cut reaches the frontier).

View file

@ -0,0 +1,88 @@
# CultMesh garden path: map
Campaign `cultmesh-garden-path`, stage 1 (server-authoritative garden path for the
orbs .io game). Target: `docs/cultmesh-garden-path-target.md` (typed as
`cultmesh-garden-path:target:r2`). Rulings, questions and follow-ups are documents
in the mind; this page holds the model page, body facts and the stage 1 cut lines.
Written by Imagination (`imagination-gp-0b`, session `self-2026-10-27-eureka`)
against CultLib `d1bf21b3`. Refreshed by `imagination-gp-stage1` (session
`self-2026-10-27c-eureka`) against `51ca81a1`: every fork the page waited on is
ruled, and the cells below carry the rulings.
## Model page
One row per kind. "Store" says where it lives: a CultCache document in the room's
store, or transient per-tick data that never enters a store. Proposed values are
marked as such; they are the design this page offers for the cuts, not landed
behaviour. A cell that waits on an operator question names it.
The governing split, from `state-is-cultcache` and the CultCache scope restraint:
every value that crosses a boundary is CultCache-encoded (one serialisation model,
no JSON), but only state that outlives a tick is a record in a store. Inputs, ticks
and snapshots are typed CultNet message payloads, not store records. The store
receives exactly one write set per tick (ruled `tick-state-commit`: tick-batch).
| Kind | Store | Identity (what names it) | Lifecycle (what happens to it) | Authority (who decides; forbidden writers) |
|---|---|---|---|---|
| **Game definition** (documents, systems, authority declaration, tick rate) | Proposed: one CultCache document per server process (`gamecult.mesh.game_definition`), read-only to everyone but the server's startup. | Game id string from code (`"orbs"`), namespaced by Verse id. The id alone is not injective (two builds share it), so proposed identity is `(game id, definition hash)`, the hash derived from the lowered definition: document schema ids and versions, system ids, order and marks (`[Predicted]`, later `[Adjudicable]`), tick rate, authority declaration. Schema ids (`orbs.player`) are global strings; the game prefix is convention, not enforced (body fact F9). | Created at server startup from code; never revised at runtime; a new build supersedes it with a new hash. Clients carry their own compiled hash (C# in wasm, ruling `browser-sim-runtime`) and are refused at join on mismatch. A declaration a mode cannot honour is refused at startup (`modes-do-not-leak`). Never replayed. | The game's code, through the builder, at startup; the runtime validates. Forbidden: clients, any runtime mutation, Odin (Odin may read and index it, never write it). |
| **Game document** (Player, Pellet, Shot) | CultCache documents in the room store. | `(schema id, record key)`. Today the key is a free string the caller picks; `[CultIndex] Id` in the illustration is an index, not the key. Proposed: the runtime mints keys. Player key derives from the session principal (one Player per principal per room). Spawned documents (Shot, Pellet) key on `(spawner key, tick, spawn ordinal within that tick)`, so a client's predicted spawn and the server's authoritative spawn derive the same key and reconcile instead of duplicating. That is injective as long as spawn order within a system step is deterministic for the predicting client, which the predicted system's own code gives under one runtime (C# both sides). The developer's `Id` field becomes redundant; whether to keep it as a mirror of the key is an API call for the cut, not an operator fork. | Created by a system during a tick, or by the join system (Player). Revised at most once per tick, in that tick's commit. Deleted by a system (pellet eaten) or by the game's leave system after a session ends. Across reconnect within the grace window the Player record is unchanged; only the session's connection binding moves. Across a server restart: gone (ruled `room-durability`: ephemeral; a restart is a new room epoch at tick 0). Replayed only on the client, during reconciliation; the server never replays in stage 1. | The server tick runner on the room's primary is the one writer of committed state, and systems write only through the tick's working set. Forbidden writers: client document puts through `CultNetDatabaseServer.HandlePutAsync` (today accepted from any peer, F1); `PutPredictedAsync` and `CultMeshGameSession.PredictAsync` on any networked store; game code calling database APIs; the simulation-fact committer (F7). |
| **Player input** | Transient. A CultCache-encoded payload on a CultNet message; never a store record in stage 1. | `(session principal, input sequence)`, the sequence monotonic per session; each input also carries the tick it targets. Namespace: the room. Injective given the principal. The input type is a schema'd CultCache type, so the schema catalog publishes it like any document. | Sampled each client tick (devices read by the TS shell, handed to wasm); sent with the last few unacknowledged inputs repeated, so one loss is absorbed; buffered by the server per session against its target tick; consumed exactly once by the matching tick (proposed policy for late inputs: applied to the next tick and counted); acknowledged in the snapshot as "last processed sequence"; kept by the client until acknowledged, for replay; then discarded. Stage 4 changes this: inputs become signed, hash-chained and retained for a dispute window. | The client's principal authors the content; the tick runner decides which tick consumes it. Forbidden: server code altering input content; other clients; any input that arrives without a bound session. |
| **Tick** | Transient counter; proposed: the last committed tick is written into the room record in each tick's batch, so a snapshot or checkpoint knows it. | `(room id, room epoch, tick number)`, the number starting at 0 when the room epoch starts. Injective with the epoch. | Advanced only by the server tick runner at the one declared rate (ruled `tick-model`: one-fixed-rate); never revised. The client estimates the server tick from snapshots and runs its predicted tick ahead by its round trip plus a buffer; on reconnect it re-syncs from the next full snapshot. | Server tick runner. Forbidden: client clocks, transport arrival time, wall-clock reads inside systems (systems receive the tick, not the time). Today no tick scheduler exists anywhere in the C# body (F6). |
| **Predicted state and reconciliation** | Client-side only. In the browser they live in the C# wasm CultCache store; the TS renderer reads a per-frame view (ruled `browser-client-store`: wasm-owns-store). | Same `(schema id, record key)` as the authoritative document, plus the client tick it was predicted for. | Each client tick: predicted systems run over owned documents and the client's own spawns. On a snapshot for tick T acknowledging input sequence S: predicted documents are replaced by the authoritative ones at T, then inputs after S are replayed through the predicted systems up to the current client tick; predicted spawns are matched by derived key, and an unmatched one is removed once the acknowledgement passes its tick. Today `Reconciled` means "the authoritative put replaced the predicted copy" with no replay (F3); the replay loop does not exist. | Predicted state owns nothing outside the client. It is derived from the last authoritative snapshot plus unacknowledged inputs plus the predicted systems. The snapshot owns truth. Forbidden: sending predicted documents to the server; `CultNetClientAuthorityScope` in server mode (F4); the renderer writing. |
| **Session and the player's identity** | Proposed: session membership as a small CultCache document in the room store (`gamecult.mesh.game_session`, low churn, readable by systems and present in snapshots). The connection handle is transient process state. | Principal: the fingerprint of a browser-held P-256 key, proved by signing a join challenge (ruled `player-identity`: browser-keypair). Connection: a server-minted id per transport connection (ephemeral). Logical session: `(principal, room)`, bound across reconnects. Today the client's identity is a self-asserted `runtimeId` string (browser client options; `SourceRuntimeId` on operation envelopes); CultMesh's session identity proves the server to the client and not the reverse (F5). The body still has no authenticated client principal (F5, F13); the session cut builds it. | Connect, then join (definition hash check, principal proof), then the session record is created and the join system spawns the Player. Disconnect suspends the session for a grace window; resume binds a new connection to the same session; expiry ends the session, systems see the leave, and the game's leave system removes the Player. On server restart sessions end (ruled `room-durability`). | The server's join and leave handler on the room primary decides membership; the principal proves itself by signing the join challenge. Forbidden: a client-asserted `runtimeId` as identity; transport peer address; peer catalogs. |
| **Authority declaration and the lease it lowers to** | Declaration: inside the game definition. Lease: proposed as a CultCache document (`gamecult.mesh.authority_lease`); today a plain class in an in-memory dictionary (F2). | Declaration: per document type, `(writer, checker, arbiter)`. Lease: `LeaseId`, carrying Verse, peer, roles, shard ids, issuer, validity window, epoch and signature (`CultMeshAuthorityLease`). Namespace: the Verse. | Declaration fixed per build. At startup `Authority.Server` lowers to: the server holds a `ShardPrimary` lease on the room shard, and clients hold no write scope at all. Renewed before expiry; revocation or expiry stops the tick runner from committing; a new primary takes a new epoch. Proposed issuer in stage 1: the server's own deployment key, which clients pin through the browser trust policy, so an indie deployment needs no Odin. Odin-issued leases stay possible. | The game definition decides the declaration; the issuer issues leases; `CultMeshAuthorityResolver` decides whether a peer holds a role on a shard. Today the database's write gate (`CultNetShardDescriptor.IsPrimary`, `CultNetClientAuthorityScope`, both set from options) never consults the resolver: two authority structures (F2, F4). Proposed: the write gate is derived from lease resolution. First step (cut `remote-write-authority`): a peer's put or delete reaches the database only when `CultNetDatabaseServerOptions.RemoteWriteAuthority` grants it; none configured refuses every remote write (F1, GP1). The lease-backed grant needs an authenticated principal bound to the connection, so it follows the session cut. `CultNetClientAuthorityScope` is no longer an owner; it is derived from the declaration, and in stage 1 it is empty. |
| **Shard / room** (stage 1: one) | `CultNetShardDescriptor` (configuration); the room's state is its CultCache store. | Room id `(Verse, game id, room name)`, for example `orbs/main`, carried as the shard id plus the shard epoch. | Created at server start (epoch 1; on restart a new epoch, ruled `room-durability`); never moved in stage 1; ended at shutdown. Join means "join the one room"; matchmaking is out of scope. Interest management is stage 5 per the target, so in stage 1 every client receives the whole room. | The runtime holding the `ShardPrimary` lease. Forbidden: a second primary; clients. |
| **Snapshot sent to clients** | Transient. A CultNet message whose payload is CultCache-encoded documents; never stored. The server keeps per-client baselines in memory for deltas. | `(room, epoch, tick, recipient session, baseline tick)`. | Derived after a tick commit at the send rate; a delta against the recipient's last acknowledged snapshot, or a full snapshot on join and resume; carries the last processed input sequence for that session. Discarded once sent; a baseline is dropped when a newer one is acknowledged. Today the nearest things are per-record change pushes from `CultNetDatabaseSubscriptionServer` (no tick, no acknowledgement, no input sequence) and shard catch-up snapshots in `CultMeshSnapshots.cs` (F8). | The snapshot builder decides nothing; it is derived from the committed tick. Forbidden: the per-record subscription stream acting as a second game replication path. That stream stays for non-game observers. |
| **Simulation fact / witness observation** (found) | `CultMeshSimulationFact` is a CultCache document; observations are hub memory. | `(shard, epoch, frame, subject, kind)`. | Stage 1: not used. The target replaces it at stage 4. | Today `CultMeshGameSession` serves observations by default (`ServeSimulationObservations = true`) and commits quorum facts through the primary (F7). In server mode there is no checker, so the observation server and the committer must not be wired. Forbidden writer in stage 1. |
| **Shard mutation log entry** (found) | `CultNetDatabase` in-memory `SortedList` per shard, plus an optional durable store. | `(shard id, sequence)`. | Today every primary write appends one entry, and the in-memory list is never trimmed (F10). Per-record writes at tick rate would grow it without bound. In stage 1 the tick's write set either bypasses the per-record log or lands as one bounded entry per tick. Ruled: the tick's write set lands as one batch per tick (`tick-state-commit`), in memory (`room-durability`); the bounded per-tick entry is the tick-runner cut's to specify. | `CultNetDatabase` on the primary. Forbidden: per-system-write appends from the tick runner. |
### Cells filled by rulings (2026-10-27)
- Session principal: `player-identity` (browser-keypair). Not yet in the body (F5, F13).
- Browser documents and the renderer's read: `browser-client-store` (wasm-owns-store).
- Room state across restarts: `room-durability` (ephemeral).
- Tick rate model: `tick-model` (one-fixed-rate).
- How a tick reaches the store: `tick-state-commit` (tick-batch; the tick-runner cut times `CultCacheBatch` at tick rate first).
- Stage 1 transport: `browser-transport` (WebSocket behind a port carrying only inputs and snapshots; a WebTransport server in parallel, `webtransport-server-layer`: managed C# over the MsQuic bridge).
- Still unprobed: `CultCacheBatch` commit cost at tick rate for about 50 players plus pellets (wasm measuring probe and tick-runner cut).
## Body facts
All are source reads at CultLib `d1bf21b3` on 2026-10-27 by `imagination-gp-0b`. No
runtime probe was run in this pass. Each fact names the read that would falsify it.
- **F1. The primary applies any peer's document put.** `CultNetDatabaseServer.HandlePutAsync(message, peer)` (`src/GameCult.Networking/CultNetDatabaseServer.cs:307`) calls `ApplyPutAsync(message)` (`CultNetDatabase.cs:1022`), which checks only that this runtime is primary for the shard and the epoch matches (`EnsurePrimary`, `:2003`). `peer` is used only to send errors. Any connected client can overwrite any game document on the server.
- **F2. Leases are not typed state and do not gate writes.** `CultMeshAuthorityLease` is a plain class (no `[CultDocument]`); `CultMeshAuthorityLeaseCatalog` is a `Dictionary` (`CultMeshAuthority.cs:242`). The only `[CultDocument]` types in `GameCult.Mesh` are body publication, CDN chunk and manifest, content transfer state, discovery state, entity prefab package and simulation fact. `GameCult.Networking` (the database) references no lease type; the one Networking file naming `Lease` is `CultNetSchemaMessages.cs`.
- **F3. Reconciliation is replacement, not replay.** `CultNetDatabase.cs:1247-1252`: a predicted put records a prediction key; the next non-predicting write to that key turns the change into `Reconciled`. No code re-applies inputs after an authoritative state.
- **F4. Client write scope is a runtime-id string match from options.** `CultNetClientAuthorityScope.Matches` compares `OwnerRuntimeId` with the database's own `_runtimeId` (`CultNetDatabase.cs:111-153`, `EnsureClientAuthority` `:1934`). It gates local prediction only and has no relation to leases.
- **F5. No authenticated client principal.** `CultMeshSessionIdentity.cs` verifies transports for `(verse, authorityRuntimeId, protocol, routeGeneration)`, which proves the server to the client. The browser client's `runtimeId` (`packages/cultmesh-browser/src/index.ts:182`) and the operation envelope's `SourceRuntimeId` (`CultNetOperationServer.cs:39`) are self-asserted.
- **F6. No tick scheduler.** A grep of `GameCult.Networking` and `GameCult.Mesh` for tick or timestep names finds none. `Frame` exists only as a witness-asserted number in `CultNetSimulationConsensus` and `CultMeshSimulationFact`.
- **F7. The game session serves the witness path by default.** `CultMeshGameSession` constructs `CultNetSimulationObservationServer` when `ServeSimulationObservations` (default `true`) and always constructs a `CultMeshSimulationFactCommitter` (`CultMeshGameSession.cs:70-104`). `PredictAsync` is `PutPredictedAsync` (`:161`).
- **F8. The browser can watch and invoke, not put.** The `cultmesh-browser` README says it "does not expose direct document writes". The client offers `leaseRawDocument` (`index.ts:349`) and `invoke` of typed operations (`:370`), over one WebSocket. On C#: `CultNetDatabaseSubscriptionServer` and `CultNetOperationServer` take `ICultNetSchemaServer`, so they run over `CultNetWebSocketSchemaServer`; `CultNetDatabaseServer` (puts) takes the concrete UDP `Server` (`CultNetDatabaseServer.cs:36`). Operations are request and response with receipts and idempotency keys, which is a poor fit for an input stream at tick rate.
- **F9. Schema-id namespacing is convention.** `[CultDocument("orbs.player", ...)]` ids are global strings; nothing ties a schema id to a game id.
- **F10. The in-memory mutation log is never trimmed.** `RecordMutationLogEntry` (`CultNetDatabase.cs:1610`) inserts into a per-shard `SortedList`; no `Remove` or trim on that list exists in the file. Compaction applies to the durable store and to burned sequences only.
- **F11. CultCache has a write-set batch; CultNetDatabase does not expose one.** `CultCacheBatch` with `Commit` and `TryCommit` (`GameCult.Caching/CultCache.cs:1796`, `:2212`) is the 2.0.0 write set (`variants:ruling:write-set-content-version-declared-ownership`). `CultNetDatabase` has no batch method: every write is a per-record door.
- **F12. A reflection site on the mesh put path.** `CultMesh.cs:3502` `PutDatabaseDocumentAsync` picks `PutAsync` or `PutPredictedAsync` by `GetMethods` and `MakeGenericMethod`, which matters to the AOT prerequisite (`cultlib-gaps:follow_up:cs-aot-safe-registration`; probe in `research/cultmesh-garden-path/cultcache-aot-probe-2026-10-27.md`).
- **GP1 (runtime probe, 2026-10-28, `imagination-gp-stage1`).** Scratch NUnit test at `51ca81a1`, run on Yggdrasil (`ygg-verify.sh`, `mcr.microsoft.com/dotnet/sdk:10.0`, filter `ImagGpProbeTests`, never pushed to origin). The server writes `gp:player` = "server-owned" through `CultNetDatabase.PutAsync` on a primary shard. `CultNetDatabaseServer._putHandler`, invoked with an uninitialized `CultNetServerPeer` (no field set: no endpoint, no transport, no identity) and a put of "client-overwrote", leaves the record "client-overwrote", and `Server.CanProcessBeforeVerification(put)` returns `true`. Output: `GP1 after=client-overwrote canProcessBeforeVerification=True`, Passed 1. This confirms F1 at runtime: nothing about the peer is consulted, and puts and deletes pass before the legacy login verification (`Server.cs:562-575`).
- **F13. A server peer carries no identity.** `CultNetServerPeer` (`Server.cs:829`) holds the LiteNetLib `NetPeer`, its endpoint and transport stats. The only principal on `Server` is the legacy username and password login (`PlayerData.PlayerId`, `IsVerified` `:697`), which the database door never reads. `ServerSecurityOptions` is one shared `ConnectionKey`: transport admission, not authority.
- **F14. A new refusal code is a coordinated change.** The `cultnet.error.v0` `code` is a closed enum (`contracts/cultnet/cultnet.error.schema.json:28-31`, with a byte-identical copy under `packages/cultnet-rs/contracts/`). Rust `CultNetErrorCode` (`packages/cultnet-rs/src/contracts.rs:242-273`) refuses unknown codes, `packages/cultnet-rs/tests/error_message.rs` decodes captured C# bytes per code, and TypeScript imports the root schema (`packages/cultnet-ts/src/contracts.ts:10`). `unowned_schema` was added this way and is the pattern.
- **F15. Forwarding re-sends as a new client.** `TryForwardPutAsync` (`CultNetDatabaseServer.cs:654`) forwards a `not_primary` put through `ICultNetShardWriteForwarder`, and `CultNetSchemaWriteForwarder` opens its own client connection (`CultNetShardWriteForwarding.cs:122-128`). The primary therefore sees the forwarding runtime, never the original peer.
- **F16. Consumers of the remote write door.** No C# consumer outside CultLib constructs `CultNetDatabaseServer` (grep of the Aetheria, Epiphany, Ghostlight, huginn and gamecult-ops `*.cs` files finds only vendored CultLib copies). In CultLib, tests reach the put and delete handlers directly in `CultNetJournalTests.cs:738`, `:935-936` and `SoulP4cProbeTests.cs:57`. `CultNetDatabaseServer.ApplyPutAsync` and `ApplyDeleteAsync` (`:73`, `:81`) have no caller in `src` or `tests` besides the handlers. TypeScript puts (the `cultnet-ts` Idunn presence and signed health publishers) target Rust servers.
- **F17. The Rust document server admits a put by size only.** In `packages/cultmesh-rs/src/rudp_document_server.rs:682-720`, a `DocumentPutRaw` is refused only when its served response would not fit; no authority is asked. This is F1's gap in the Rust runtime (a follow-up, not stage 1).
## Cut sequence (stage 1)
One line per cut not yet mapped. The model page has no empty cell as of 2026-10-28. Remote write authority (`cultmesh-garden-path:cut_spec:cut-remote-write-authority.r1`, mapped 2026-10-28, so no longer a line here) goes first: it waits on nothing, every later server cut stands on a closed write door, and GP1 shows the door open today. The wasm measuring probe is the sibling that may run beside it, since it touches no file this cut does; ruling `browser-sim-runtime` names it the first cut of the browser runtime track, not of stage 1 as a whole.
- **Wasm runtime measuring probe.** Download size, startup and 60 Hz tick cost of a stub orbs tick under Mono browser-wasm and NetWasm, including `CultCacheBatch` commit cost at tick rate. This is the first cut named by ruling `browser-sim-runtime`, which it can overturn. Its NetWasm and AOT variant waits on `cultlib-gaps:follow_up:cs-aot-safe-registration`.
- **Witness services off in server mode.** `CultMeshGameSession` stops constructing the observation server and fact committer when the declaration has no checker (F7). Waits on nothing; can run beside remote write authority.
- **Leases as typed state and the lease-derived grant.** `CultMeshAuthorityLease` becomes a CultCache document, the lease catalog a store view, and the Mesh host's `RemoteWriteAuthority` resolves the connection's principal through `CultMeshAuthorityResolver`. Waits on remote write authority and on session and principal.
- **Demote `CultNetClientAuthorityScope`.** It becomes derived from the authority declaration (empty in server mode). Waits on game definition.
- **Changelog entry for the remote write refusal.** `unity/org.gamecult.cultlib/CHANGELOG.md` is also written by `variants:cut_spec:cut-write-ownership-cs.r1`, so Self adds this cut's bullet at merge, after that cut lands, and the two specs share no file.
- **Game definition and server tick runner.** Builder, document and system registration, definition document and hash, fixed tick, tick-boundary commit. Rests on rulings `tick-model`, `tick-state-commit` and `room-durability`; waits on nothing mapped.
- **Session and principal.** Join, resume and leave; principal proof; session document. Rests on ruling `player-identity`; waits on game definition (the join checks the definition hash).
- **Input and snapshot path.** Input messages with redundancy, a server input buffer, acknowledgements, delta snapshots. Rests on ruling `browser-transport`; waits on session and principal.
- **Browser client runtime.** Wasm simulation host, TypeScript shell for transport and rendering, prediction with replay reconciliation. Waits on the measuring probe and `browser-client-store`.
- **Orbs reference game and garden-path proof.** The game written with no networking code (`garden-path-needs-no-networking`), and a browser client that writes (`browser-is-first-class`). Waits on all of the above.

View file

@ -0,0 +1,153 @@
# CultMesh garden path: target
Status: draft target for campaign `cultmesh-garden-path`, 2026-10-27. It holds the
ends, not the means. The code below is illustrative: it shows what a developer
should have to write, and no name in it is decided.
## The problem
Operator, 2026-10-27: "the value is in solving the multiplayer scaling problem
for indie developers in an ergonomic fashion. Having one authoritative server is
fine, up to a point, but there's no real open source playbook on how to ship an
MMO, and existing solutions like SpacetimeDB have a huge ergonomics gap. I want
CultMesh to feel like magic: as a garden path, you annotate your domain POCOs and
simulation code and declare your authority structure ... and it'll all just work
without the developer needing to understand networking deeply."
Prior art: `research/cultmesh-garden-path/prior-art-2026-10-27.md` (frameworks,
ergonomics, scale-out) and `research/witness-authoritative-networking/` (consensus
and P2P anti-cheat). What the field shows, in short:
- One API over several authority modes ships today (Photon Fusion: Server, Host,
Shared; Unity NGO: client-server and distributed authority), and in both the
modes leak into the developer's code (`IsServer` against `HasAuthority`,
features missing in one mode).
- SpacetimeDB gives one machine per database. Scaling past it, client
prediction, and the reducer-shaped rewrite of game code are left to the
developer.
- Determinism, where it is required at all, is required of the whole simulation
(Croquet/Multisynq, Photon Quantum), and that tax is the main ergonomic cost.
- Shipped world-scale designs treat a spatial unit as indivisible and move
players between units (EVE, Pax Dei, Folia); nobody publishes an open,
end-to-end playbook (Astron, the nearest, is discontinued).
## What a developer writes (illustrative)
The reference game is a small browser .io game (operator, 2026-10-27: "the .io
game, Aetheria later"): players steer orbs, eat pellets, and shoot.
```csharp
[CultDocument("orbs.player", "orbs.player.v1")]
public sealed class Player
{
[CultIndex] public string Id { get; set; } = "";
public Fix2 Position { get; set; }
public Fix2 Velocity { get; set; }
public int Mass { get; set; }
}
[CultDocument("orbs.shot", "orbs.shot.v1")]
public sealed class Shot
{
[CultIndex] public string Id { get; set; } = "";
public string Shooter { get; set; } = "";
public Fix2 Origin { get; set; }
public Fix2 Direction { get; set; }
}
// Runs every tick. Predicted on the owning client, corrected by authority.
public sealed class Movement : CultSystem
{
[Predicted]
public void Step(Player player, PlayerInput input, Tick tick) { /* ... */ }
}
// A non-owner may re-run this to settle a dispute, so it must be
// deterministic. The analyzer enforces that on this system only.
[Adjudicable]
public sealed class Hits : CultSystem
{
public HitResult Resolve(Shot shot, Rewind<Player> target) { /* ... */ }
}
var game = CultMesh.Game("orbs")
.Documents<Player, Pellet, Shot>()
.Systems<Movement, Eating, Hits>()
.Authority(Authority.Server);
// .Authority(Authority.TrustedCoop);
// .Authority(Authority.Witnessed(w => w.Challenger(Party.Victim).Quorum(3).Arbiter(Party.Server)));
// .Authority(Authority.Regions(r => r.Grid(512).Handoff())); // later: past one server
```
Changing the authority line is the whole migration between modes, unless the new
mode re-executes a system on a non-owner and that system is not yet
deterministic. Then the analyzer names exactly those systems.
## Invariants
- `one-model`: server-authoritative, trusted co-op, witnessed and regioned
authority are configurations of one authority declaration, answering per
document: who writes, who checks, who arbitrates. No mode has its own API for
state, systems or queries.
- `modes-do-not-leak`: game code does not branch on the authority mode or ask
"am I the server"; a system states what it does and the runtime decides where
it runs. Where a mode cannot offer a capability, the declaration is refused at
startup with a reason, never degraded silently at runtime.
- `determinism-is-per-system`: only code a non-owner may re-execute must be
deterministic. That requirement is marked on the system and checked by an
analyzer at build time, not by documentation or by desync in production.
- `state-is-cultcache`: game state is ordinary CultCache documents; the
framework adds no second state model and no JSON on the load-bearing path.
- `authority-is-typed-state`: who holds authority over what is CultMesh state
(leases, shard primaries), inspectable and revocable, never inferred from
transport success or peer catalogs.
- `garden-path-needs-no-networking`: the reference game is written without the
developer touching sockets, serialisation, tick scheduling, prediction or
reconciliation code.
- `honest-guarantees`: each mode states its fault model (trusted server, trusted
peers, honest-majority sampled witnesses with server arbitration) and its
documentation and tests claim nothing past it.
- `browser-is-first-class`: the reference game's client runs in a browser and can
write (submit inputs, own predicted state), not only watch.
- `built-for-load`: what the garden path ships that is database-shaped (the
tick commit, the batch door, logs, subscriptions) is architected for
unreasonable load from the start (operator 2026-10-27,
`cultlib-gaps:ruling:database-shaped-built-for-load`): "if what we're shipping
is database shaped, people will use it as such".
## Sequence (each a campaign or stage of its own; one foundation at a time)
1. Server-authoritative garden path for the .io game: documents, systems,
ticked scheduling, client prediction and reconciliation, browser client that
writes. This is the proof of ergonomics.
2. Trusted co-op: authority moves to the owning peer; same game code.
3. Deterministic dispute kernel (CultMath fixed-point hit tests, cross-runtime
vectors) and the `[Adjudicable]` analyzer.
4. Witnessed authority: signed, hash-chained inputs; victim challenge; sampled
network-near witnesses; server arbitration; browser-to-browser channels.
Demonstration: a cheating client caught and corrected.
5. Regions: spatial authority handoff and interest management past one server.
6. Aetheria on the same declaration.
## Not in scope
- Aetheria until stage 6. Eve and Gjallar (deprecated).
- A new CultCache storage engine (the paged single-file store stays postponed,
`variants:ruling:page-tree-store-postponed`). The garden path fills gaps in
their owners, built for load.
- Asynchronous Byzantine total ordering, zero-knowledge proofs, and chains of
any kind.
- The existing witness aggregator (`CultNetSimulationConsensus`, unsigned and
weighted by observed votes) as a base for stage 4: it is the trusted co-op
first stage by design and will be replaced, not extended.
## Open forks (to the Fork Desk, each with prior art first)
1. Where simulation code runs in the browser: C# compiled to WebAssembly (one
implementation everywhere, which also shrinks the determinism problem to
fixed-point maths, at the cost of runtime download size), C# on the server
with TypeScript systems in the browser (two implementations of each predicted
system), or another arrangement.
2. Server transport for the browser: WebSocket as today, or WebTransport
(QUIC datagrams) on the provider side.
3. Tick model: fixed tick with a declared rate, or per-system rates.

View file

@ -0,0 +1,286 @@
# TypeScript CultMesh Content Plane: Map
Campaign `cultmesh-ts-content`. Imagination wrote this on 2026-10-19 at CultLib
`8990bd46` (origin/main `a88c629e` plus the target doc), and refreshed it the
same day at `2de07065` for C2 and C3, reading C1 from `b8b825e1`. The target
is `docs/cultmesh-ts-content-target.md`. The HTTP prior art is
`docs/research/2026-10-19-content-addressed-http-answers.md` (cited below as
**R**, with its section number). This page holds the body facts, the model
page, the rationale and the cut sequence. Specs and questions live in the
typed mind, not here.
## 1. Body facts
F1-F18 were probed by running code or read from source at `8990bd46`. F19-F27
were probed or read at `b8b825e1` (C1's head) and `2de07065`. F28-F33 were
probed on 2026-10-19 over the C2+C3 merge tree (`7adbf3cb` merged with C3's
head, extracted by `git archive`), on Windows and in a Linux container. Scratch probes
ran under the session scratchpad (`imagination/`), not in the repo.
| # | Fact | Evidence |
|---|---|---|
| F1 | A C#-packed manifest is a MessagePack **fixarray of 10** (`0x9a`): `[artifactId, kind, version, contentHash, sizeBytes, mimeType, createdAtUtc, chunks[], tags[], metadata{}]`. Each chunk ref is a 4-array `[chunkHash, offset, sizeBytes, recordKey]`. The fixture manifest is 1,111 bytes: 6 chunks (5 x 262,144 + 4,831), content hash `f4ae2a7c…2af1`. The empty body packs to 315 bytes with one zero-size chunk whose hash is `e3b0c442…b855` (the SHA-256 of nothing). | probe: `@msgpack/msgpack` 3.1.3 decode of `contracts/cultmesh/content-vectors.cs-written.json`, kind `manifest` |
| F2 | `@msgpack/msgpack` 3.1.3 re-encodes all four C# manifest vectors **byte-identically**, metadata included (`zeta, alpha, mid` keep insertion order). Integers come out minimal (`4831` is `cd12df`, `262144` is `ce00040000`), as MessagePack-CSharp writes them. | same probe, `encode(decode(bytes))` equals the input for all four |
| F3 | Three decoder hazards for a TS manifest reader. (a) JS objects put integer-like keys first (`{zeta,"2",alpha,"1"}` iterates `1,2,zeta,alpha`), so metadata must be ordered entries, not an object, or a `{"10":…,"2":…}` manifest re-encodes in another order. (b) Plain `decode` takes the last of duplicate map keys silently (`{"a":"2"}`); C# `Dictionary` and Rust `ordered_map` refuse duplicates. `new Decoder({ mapKeyConverter })` can refuse them: probed. (c) Plain `decode` rounds int64 above 2^53 (`…993` reads as `…992`); `useBigInt64: true` keeps it exact. | probe (node) |
| F4 | `@msgpack/msgpack`'s decoder is iterative: a 1,000,000-deep `0x91` input decodes in 160 ms but costs about 158 MB of heap. So nesting does not crash it but amplifies memory about 150 times. `checkPayloadDepth` (the owner of ruling `payload-depth-limit-256`) is not exported from any `@gamecult/cultcache-ts` entry: `document.ts:108` exports only the error and the constant. | probe (node); source `packages/cultcache-ts/src/document.ts:2,108,123` |
| F5 | The C# HTTPS connector sends `GET <endpoint>?chunkHash=<hex>&recordKey=<escaped>`, **without** a `sha256:` prefix, because `BuildChunkUri` runs `NormalizeHash` first. Probed URIs: `https://ygg.example/content?chunkHash=5993ac…&recordKey=mesh%3Acdn%3Achunk%3A5993ac…`. An existing endpoint query is kept and joined with `&` (`?tenant=sp&chunkHash=…`). A null record key sends `recordKey=` (empty). Other characters use `Uri.EscapeDataString`, so on the wire `k with space/ü&=?#` becomes `k%20with%20space%2F%C3%BC%26%3D%3F%23`. | probe: `BuildChunkUri` lines copied into a net10.0 console (`CultMeshHttpsContentTransport.cs:114-122`) |
| F6 | The connector calls `EnsureSuccessStatusCode` and fails on any status outside 2xx. When `Content-Length` is present and is not the chunk's `SizeBytes`, it throws before reading. With no `Content-Length` it copies the stream without checking the length. Verification belongs to the caller's transfer owner. `CanConnect` admits only `https`. The constructor takes a `Func<HttpMessageHandler>`, so a test can pin a self-signed certificate without modifying the connector. | source `CultMeshHttpsContentTransport.cs:18-29,92-104` |
| F7 | `NormalizeHash` (C#) strips a case-insensitive `sha256:` prefix *before* trimming, then `Trim().ToLowerInvariant()`. It never checks for hex or a length of 64. So the record-key grammar accepts any non-blank string, and a provider must add its own hex check before a hash can become a file name. | source `CultMeshCdn.cs:549-560` |
| F8 | `StableToken` (the token in a manifest record key) is a C# rule with no Rust mirror: `cultnet-rs/src/content.rs` builds no artifact record key. Its Unicode semantics differ from the naive JS spelling in four probed ways. .NET `ToLowerInvariant` leaves `U+0130` unchanged, while JS gives `i` plus `U+0307`. .NET `Trim` keeps `U+FEFF` (and trims `U+0085`, per `char.IsWhiteSpace`), while JS `trim` drops `U+FEFF` and keeps `U+0085`. JS whole-string lowercasing applies final-sigma (`ΑΣ` becomes `ας`), while per-unit lowercasing gives `ασ`. `char.IsLetterOrDigit` is false on a lone surrogate and on `U+00B2`, which JS `\p{L}\p{Nd}` agrees with only when it is tested per UTF-16 unit. | probe (net10.0 console and node); source `CultMeshCdn.cs:562-580` |
| F9 | `CreatedAtUtc` defaults to `DateTimeOffset.UtcNow.ToString("O")`, which looks like `2026-10-19T12:07:25.5235579+00:00`: 7 fractional digits and `+00:00`, never `Z`. | probe (net10.0) |
| F10 | `contracts/cultmesh/content-vectors.cs-written.json` and `.rs-written.json` are written by `tests/GameCult.Mesh.Tests/CultMeshContentVectorTests.cs` and `packages/cultnet-rs/tests/content.rs`, under `CULTNET_WRITE_VECTORS=1`. Each side judges both files. Kinds are `request`, `response` and `manifest`. Rust panics on an unknown kind (`tests/content.rs:302`), and C# requires the Rust labels to equal its own (`CultMeshContentVectorTests.cs:54`). **So a new kind or label in the cs-written file breaks both siblings.** A TS file must be a separate file. | source |
| F11 | **Neither content-vector suite runs in CI.** `cultmesh-portability.yml` filters `dotnet test` to `AuthorityProof`, `RealtimeWireProtocol` and a lease churn test. `cultnet-interop.yml` runs `cargo test … rudp` (a name filter) and the Networking `Rudp` filter. The C#/Rust content parity has only ever been run by hand. | grep of `.github/workflows/*.yml` |
| F12 | TS→C# vector precedent: `scripts/write-cultmesh-realtime-frame-vectors.mjs` writes `realtime-frame-vectors.ts-written.json` from built `dist`, deliberately from **different inputs** than the C# file. `CultMeshRealtimeWireProtocolTests` judges it, and `cultmesh-portability.yml:145-149` runs it. | source |
| F13 | Cross-process precedent for C# against a Node provider exists. `packages/cultnet-ts/test/interop/cultnet-interop.test.ts:536-560` spawns the TS QUIC provider, then runs `dotnet test --filter NativeConnectorReceivesFromExternalProvider` with the endpoint in `CULTMESH_NATIVE_EXTERNAL_ENDPOINT`. The C# test skips without the variable (`tests/GameCult.Mesh.Quic.Native.Tests`, line 21). | source |
| F14 | `cultnet-ts` is CommonJS. Its `./wire`, `./contracts` and `./authority` subpaths import only `@msgpack/msgpack`, `ajv` and `cultcache-ts/document`, so they are browser-safe. Its root `index` pulls `node:` modules (`peer`, `rudp`, `transport`, `security` and others). `cultmesh-browser` (ESM) already imports `cultnet-ts/wire`, `/contracts` and `/authority`. `cultmesh-ts` loads `koffi` lazily, inside a function (`realtime-quic-native.ts:139`), so a new subpath of it pays no native load. The root has `esbuild` ^0.28.1 as a devDependency. | source, grep |
| F15 | TS packages build with `tsc` and test with `node --test dist-test/test/*.test.js` per workspace. `scripts/run-typescript-workspaces.mjs` runs them in order `cultcache-ts, cultnet-ts, cultmesh-ts, cultmesh-browser`, after `check-typescript-tests-wired.mjs`. No StrykerJS config exists in CultLib (`docs/mutation-testing.md:156-159`). | source |
| F16 | **No CultLib TS package except `cultcache-ts` has a publish job.** `publish-packages.yml` 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
does not state it.
- **The Fetch facts** decide the reader (R §6). Fetch decodes content codings
before the page sees the body, so `Content-Length` describes the encoded
body, which F20 reproduces in Node. `Content-Length` is CORS-safelisted, and
`ETag` is not. `Access-Control-Allow-Origin: *` passes the CORS check for any
request whose credentials mode is not `include`.
| Kind | Identity (what names it) | Lifecycle | Authority (who decides; forbidden writers) |
|---|---|---|---|
| **Manifest** `gamecult.mesh.cdn_artifact_manifest.v1` | The record key (row below), derived from `artifactId`, `version` and `contentHash`. On the wire it is the 10-array of F1. Its namespace is the schema id. Two manifests for one body under different ids or versions are distinct and legitimate. | Immutable once packed. A new body or version is a new manifest and a new key. Nothing revises one in place, and deletion is not in scope (no GC). `createdAtUtc` is set at pack, and a caller may pin it so that repacking is reproducible. | `packContent` in `cultnet-ts/content` (C1, `b8b825e1`) is the only TS writer. The encoder and decoder in the same module are the only TS byte owners. The reader takes a manifest value and never builds or edits one. **Forbidden:** a second TS manifest type or encoder in `cultmesh-ts`, `cultmesh-browser` or a consumer, and any URL or endpoint field. |
| **Chunk ref** `CultMeshCdnChunkRef` | Its `chunkHash` (SHA-256 of the chunk bytes) within one manifest, plus `offset`. Two refs may share a hash: this is legal, and the reader fetches each ref. | Immutable inside its manifest. `recordKey` is derived and may be blank on read. A blank key goes on the wire as `recordKey=` (F5), and the provider accepts it. | The packer writes. `validateManifest` (C1) judges contiguity, non-negative sizes and the size sum. The reader and provider never edit one. |
| **Chunk record key** `mesh:cdn:chunk:<hex>` | Fully derived from `normalizeHash(chunkHash)`. It is injective only over valid hex, because C# accepts any non-blank string (F7). | Derived on demand, never stored independently of a manifest. | `chunkRecordKey` in `cultnet-ts/content`. On the HTTP wire the provider **checks** a non-blank `recordKey` against it, as the C# TCP server does (F21), and never derives a file from it. |
| **Artifact record key** `mesh:cdn:artifact:<token>:<versionToken\|unversioned>:<hex>` | Derived by `StableToken` (F8). It is not injective: `A/b` and `a b` both give `a-b`, and the content hash at the end disambiguates. | Derived on demand. | `artifactRecordKey` in `cultnet-ts/content` (async; C1 deviation). Truth comes from the C#-written `contracts/cultmesh/cdn-record-key-vectors.json`. Neither C2 nor C3 uses it. |
| **Content hash / chunk hash** | Lowercase 64-hex SHA-256. On input, `normalizeHash` accepts `sha256:`, any case and surrounding whitespace, as C# does. The HTTP boundary and the reader also demand 64 hex through `parseSha256Hex`. | Immutable. It *is* the identity. | `sha256Hex` and `parseSha256Hex` (C1) are the only TS hasher and the only hex rule. The provider and reader call them and keep no copy. The negative greps pin this. |
| **Stored chunk** (provider disk) | File `<root>/<hex>`, flat, one file per distinct chunk hash. This is the reference layout (F21: C# stores one record per chunk). For a single-chunk artifact the chunk hash equals the content hash, so StreamPixels' whole-artifact file `/srv/streampixels/content/<hex>` (F23) is already a stored chunk. A multi-chunk artifact's publisher writes `packContent(...).chunks`, one file each. | Written by the publisher outside the provider, and never written by the provider. Immutable. Deletion has no owner (GC is out of scope). A corrupt or oversized file stays on disk and is never served. | The publisher writes; **the provider is read-only**. The provider verifies every read against its name before serving (`verify-before-serve`, the C# per-request behaviour of F21). A symlink or a replaced file cannot leak anything, because only bytes that hash to the requested name are ever served. **Forbidden:** any path derived from `recordKey`, the URL path or an unvalidated hash. |
| **Provider store index** | **None.** The file name is the index. The directory store maps hex to `<root>/<hex>` on each request, after its own `parseSha256Hex` equality check. No in-memory table exists, so nothing can go stale. | Not applicable: it is computed per request. | `ContentChunkStore` is the port: `readChunk(chunkHash, maxBytes) → Promise<Uint8Array \| undefined>`. `createDirectoryChunkStore(root)` stats before reading and rejects a file over `maxBytes` with `ContentChunkTooLargeError`, so an oversized file is never read into memory. A host may supply another store, for example one indexed from manifests, and the handler verifies whatever any store returns. |
| **The empty chunk** `e3b0c442…b855` | The SHA-256 of zero bytes. | Intrinsic: it needs no file. | The provider answers it as 200 with `Content-Length: 0` without consulting the store, so empty artifacts work against any store (bazel-remote does the same, R §4). The reader verifies it locally and sends no request. |
| **Wire request** | `GET` or `HEAD <mount>?chunkHash=<hash>&recordKey=<key>`. Extra query parameters are ignored, because an endpoint may carry its own (F5). The mount path belongs to the host, and the handler never reads it. | Stateless. | Built by the C# connector or the TS reader. The TS reader builds the URL **byte-identical** to `BuildChunkUri`, `Uri.EscapeDataString` escaping (F19) and the empty-query `?&` join included, so C# and browser clients share HTTP cache entries. |
| **Content coding** | Identity, always. | Not applicable. | Provider: no `Content-Encoding`, and `Cache-Control` carries `no-transform`, which forbids an intermediary to transform the content (RFC 9111 §5.2.2.6). That keeps the C# connector's `Content-Length` check meaningful (F6). The reader does not rely on it: it judges the bytes it received (F20). |
| **Answer 200** (GET) | The body is exactly the verified chunk bytes. | Immutable per URL. | Provider. Headers: `Content-Length: <size>`, `Content-Type: application/octet-stream`, `X-Content-Type-Options: nosniff`, `Cache-Control: public, max-age=31536000, immutable, no-transform` and `Access-Control-Allow-Origin: *`. Octet-stream with `nosniff` keeps a public chunk from rendering as HTML on the host's origin. The IPFS `Content-Disposition: attachment` adds nothing over that for a fetch client, so it is not sent. |
| **Answer 200** (HEAD) | The same headers as GET, with no body. | Same as GET. | Provider. HEAD verifies exactly as GET does, so it can never advertise a corrupt chunk. RFC 9110 §8.6 requires HEAD's `Content-Length` to equal GET's. |
| **No ETag, no 304** | Not applicable. | Not applicable. | Provider. `immutable` with a one-year max-age means browsers do not revalidate. `ETag` is not CORS-exposed (R §6), and the URL is already the content identity. An `ETag` without a 304 path is noise, and a 304 path is a second answer to test. The first draft had both, and both are cut. |
| **Answer 400** | `chunkHash` missing, repeated, or not 64 hex after `normalizeHash`. Or `recordKey` repeated, or non-blank and not exactly `mesh:cdn:chunk:<hex>` (the C# server's "record key disagrees" rule, F21). | Stateless. | Provider. Body is a fixed `text/plain; charset=utf-8` reason with no echo of input (bazel-remote echoes the URL; we do not). Headers: `nosniff`, `Cache-Control: no-store`, `Access-Control-Allow-Origin: *`. |
| **Answer 404** | A valid hash whose chunk the store does not hold. | Stateless; a later publish can turn it into 200. | Provider. Same error headers as 400. `no-store` keeps an absence from being cached as immutable. |
| **Answer 405** | Any method but GET and HEAD, OPTIONS included. | Stateless. | Provider, with `Allow: GET, HEAD`. The reader's GET is a CORS simple request (no custom headers, credentials omitted) and never preflights. A client that adds headers would preflight, and its preflight is refused, which is correct because the provider honours no request header. The first draft's 204 preflight answer is cut. |
| **Answer 500** | The store returned bytes whose SHA-256 is not the requested hash; the store holds a file over `maxChunkBytes`; or the store failed. | Persists until the operator replaces the file. | Provider. It sends a fixed reason and none of the chunk's bytes, then calls the host's `onServerFault({ chunkHash, kind, error })` with `kind` ∈ {`hash-mismatch`, `too-large`, `store-error`}, so the host can fail health. A throwing hook does not change the answer. It is 500 and not 404 because the IPFS trustless gateway reserves 500 for "genuine server errors" (R §1), and corruption is one the operator must see, while a 404 would hide it as absence. |
| **Range** | Not supported. A `Range` header is ignored, and the answer is 200 with the full chunk (RFC 9110 §14.2). No `Accept-Ranges` header is sent. | Not applicable. | Provider. The chunk is the resumption unit, and resumable TS state is out of scope. |
| **Provider memory** | At most `maxChunkBytes` buffered per in-flight request. | Per request. | Provider. Buffering is forced, because the status line cannot be sent before the hash is known (the bazel-remote scar above). `maxChunkBytes` defaults to `DEFAULT_CHUNK_SIZE_BYTES` (4 MiB) and must lie in 1..2147483647. The host bounds concurrency (server `maxConnections` or a reverse proxy), and the README says so. |
| **Reader result / error** | A `Uint8Array` of the whole body, or one `CultMeshContentError` with `code` ∈ {`manifest-invalid`, `too-large`, `insecure-endpoint`, `webcrypto-unavailable`, `network`, `http-status`, `length-mismatch`, `chunk-hash-mismatch`, `content-hash-mismatch`}, plus `chunkHash` and `status` where they apply. A caller abort rejects with the signal's own `reason`, unwrapped. Bad options reject with `RangeError`. | Transient, held in memory only. No byte of a failed chunk or body is returned, in whole or in part (`verify-before-use`). | `fetchVerifiedContent` (and `fetchVerifiedChunk`) in `cultmesh-browser`. Concurrency defaults to 4 and is bounded 1..32 (the C# `CultMeshContentTransferOptions` bounds). `maxBytes` defaults to 256 MiB and is checked before any request. The scheme must be `https:`, or `http:` where `isLoopbackEndpoint` (F22) holds. Only status 200 is accepted. **`Content-Length` is not consulted** (F20). The body is read through a stream into the chunk's slot, a body that runs past the chunk's size is cancelled at the first excess byte, and one that ends short is refused. |
| **Endpoint** | A URL the caller resolved through CultMesh. | Per call; never persisted in content. | The caller. **Forbidden:** any manifest, chunk-ref or record field carrying it (`no-urls-in-content`). Error messages never echo it, because an endpoint query may carry a tenant or a token. |
| **Vector files** | `content-vectors.cs-written.json` (C#), `.rs-written.json` (Rust), `content-vectors.ts-written.json` (TS, manifests only, own inputs) and `cdn-record-key-vectors.json` (C#, record-key traps), all landed by C1. | Each is rewritten only by its writer under `CULTNET_WRITE_VECTORS=1`. CI never sets the variable: the committed files are the contract. | Each writer owns its file. C2 and C3 add no vector file. HTTP behaviour is pinned by tests and, in C4, by the C# transfer service driving the unmodified connector (F29). |
Two cells rest on reasoning rather than a probe: that the host, not the
provider, bounds concurrency, and the cost of hashing on HEAD. Both are
defaults a later cut can revise without changing any shape.
## 3. Rationale
**Where the CDN shapes live: `cultnet-ts/src/content.ts`, exported as
`cultnet-ts/content`.** C1 landed this at `b8b825e1`. Both consumers already
depend on `cultnet-ts`, and its subpaths are the repo's established
browser-safe entry points (F14). The placement matches Rust, where
`cultnet-rs/src/content.rs` owns the same rules.
**Hashing is WebCrypto in one place.** `crypto.subtle` exists in browsers
(secure contexts) and in Node 20+, so one hasher serves the packer, the
provider and the reader. WebCrypto has no incremental digest, so every hash
needs its bytes in memory. That fits all three users: the reader holds the
body (the target keeps artifacts in memory), the provider holds one chunk at a
time, and the packer holds the body.
**Decoding follows the siblings.** C1's report records two places where its
decoder is stricter than MessagePack-CSharp: an exact field count, and refusal
of nil strings. Ruling `leaf-acceptance-follows-reference` may make that a
finding. That belongs to C1's Soul pass, not to C2 or C3, which take manifest
values and never decode bytes.
**No TS CultCache document definition for manifests yet.** This is still
follow-up `ts-manifest-document-definition`.
**Provider: `cultmesh-ts/content`, a request-to-answer core, one store and one
Node adapter, in one file.** `createContentChunkHandler(store, options)`
returns `(request: { method, url }) => Promise<{ status, headers, body? }>`.
The request carries no headers, because the provider honours none: Range is
ignored, and conditional requests have no answer now that ETag is cut. The
core reads only the query, never the path, which is what makes
`chunk-only-route` structural rather than checked. `toNodeListener(handler)`
is about 15 lines for `node:http(s)`, and Express, Fastify or Next mount the
core the same way. The name says "chunk" because the handler answers chunks
only. StreamPixels' sketch name, `createHttpsContentHandler` (F23), implied it
does TLS, and it does not: the host terminates TLS. One file, not three,
because the store and adapter are about 50 lines between them and have no
consumer apart from the core. `cultmesh-ts` is already a Node package, so
`node:fs` in the file costs nothing.
**Store layout: per-chunk flat files.** This is the reference layout (F21),
and it needs no index that can go stale. It coincides with StreamPixels'
whole-artifact files, because every StreamPixels artifact is a single chunk
(F23). The alternative is a store indexed from manifests, mapping a chunk hash
to (artifact file, offset, size), so that multi-chunk artifacts could be
stored whole. It is not shipped, because it adds a derived index that must be
rebuilt when manifests change, and no consumer needs it. A host that wants it
writes about 30 lines against the port, and the handler still verifies every
byte. **Consumer note for StreamPixels B3:** its `catalog.cc` and
`character-artifacts.cc` manifests must stay single-chunk, so their files are
the stored chunk. Either they are packed with `chunkSizeBytes` at least the
file size and the provider's `maxChunkBytes` is raised to match, or B3
publishes the chunk files.
**Verify on every request, not once at boot.** The invariant says "serves
only bytes whose SHA-256 equals the requested chunk hash", and a boot check
cannot see a file replaced after boot. C# verifies per request (F21). A
"verified at boot" cache would be a cache pretending to be truth. StreamPixels'
boot check is its own deploy gate (missing blob means failed health), and it
stays useful beside per-request verification. The cost is one SHA-256 of at
most 4 MiB per request, and the immutable one-year caching keeps repeats away
from the origin.
**Reader: `fetchVerifiedContent(endpoint, manifest, options)` and
`fetchVerifiedChunk(endpoint, chunkHash, sizeBytes, options)` in
`cultmesh-browser`, over one internal per-chunk primitive.** The second is the
"fetch by hash" a general consumer expects for a single-chunk artifact. It
builds no manifest: it validates the hash and size, sends
`recordKey=chunkRecordKey(hash)` so its URL equals a C#-packed manifest's, and
runs the same primitive. The whole-body check is the chunk check, because for
a single chunk the two hashes are the same. Before any request,
`fetchVerifiedContent` runs `validateManifest`, `parseSha256Hex` on every
hash, the zero-size rule (F18) and the `maxBytes` cap. It then fills a private
buffer of `sizeBytes` through a sliding window of at most
`maxConcurrentChunkRequests` requests. It uses `credentials: "omit"` and an
internal `AbortController` chained to the caller's signal, so the first
failure cancels every request still in flight. It hashes each chunk in place
and returns the buffer only after the whole-body hash matches.
**Why the reader ignores `Content-Length`.** The target says the reader
"refuses a response whose length differs from the advertised size before
reading it all". The header cannot carry that duty. Fetch decodes content
codings, and a proxy that gzips in spite of `no-transform` turns a correct
1,000-byte chunk into a 29-byte `Content-Length` (F20, R §6). In a browser the
reader cannot even see `Content-Encoding` cross-origin, because it is not
safelisted, so it cannot tell that case apart. What the invariant protects is
bounded reading and no acceptance of a wrong length. The streaming cap
delivers both from the bytes actually received: the reader stops at the first
byte past the chunk's size, and refuses a short body at its end. The target
sentence should be reworded (reported to Self as a contradiction).
**C3 does not wait for C2.** C3's live test serves from a stub `node:http`
loopback server written in the test (about 30 lines). The stub sends what the
model page says, plus the misbehaviours: gzip, a short body, an endless body,
and wrong bytes. The real provider meets the real reader in C4. This keeps
the two cuts parallel, and it lets C3 test misbehaviours the real provider can
never produce.
**C4: the proof lives in `cultmesh-browser/test/interop`, with the C# half
in `GameCult.Mesh.Tests`.** The orchestrator is Node, following the QUIC
precedent (F13): it starts the provider, runs both consumers and judges what
the provider saw. It lives in `cultmesh-browser` and not `cultmesh-ts`,
because the workspace build order compiles `cultmesh-ts` first (F31). The
browser package can import `cultmesh-ts/content` with types. The reverse
would need either an untyped script or a build-order exception. The provider
runs in its own process rather than the orchestrator's, so each of the three
parties is a separate OS process, and the orchestrator never shares an event
loop with the server it judges. The C# half drives the connector through
`CultMeshContentTransferService` (F29), because the connector alone does not
verify (F6). A parity test that stopped at the connector would show that C#
receives bytes, not that C# refuses bad ones. TLS trust is a committed
fixture certificate, as the QUIC fixture is (F25). The C# side anchors it
through `CustomRootTrust`, which keeps chain and name validation and adds no
code path that can accept a different certificate. A hash-pinning
`ServerCertificateCustomValidationCallback` would skip name validation and
add a callback that has to be read to be trusted. Corruption comes in two
kinds. On disk, the real provider refuses it with 500 (C2's
verify-before-serve). In flight, a test wrapper flips bytes after the
provider has verified them. The in-flight leg is what proves that both
consumers verify.
**"Released"** is ruled: `release-shape-gitlink` (Defaulted by Self).
## 4. Cut sequence
C1 (`cut-c1-shapes.r1`, report `h1` under a Soul pass), C2
(`cut-c2-provider.r1`) and C3 (`cut-c3-reader.r1`) are specified in the mind.
C2 and C3 run in parallel once C1 merges, each in its own worktree from C1's
merge. Both are based on `b8b825e1`, and they touch disjoint files except
`cultmesh-portability.yml`, where their hunks lie 15 lines apart. What
remains:
4. **C4 `c4-parity`** (`cut-c4-parity.r1`, branch
`hands/cultmesh-ts-content-c4-parity` from `7adbf3cb` plus the C3 merge),
waiting on C2 and C3. The spec carries the plan. In one paragraph: a
node:test orchestrator in `cultmesh-browser/test/interop` packs three
artifacts (multi-chunk, single and empty) and spawns the C2 provider as its
own `node:https` process with three mounts: honest, tampered and corrupt
store. It runs the C3 reader in a child with `NODE_EXTRA_CA_CERTS`, once
more without it as a control, and then `dotnet test` over a new
`CultMeshHttpsContentInteropTests`. That suite drives the unmodified
connector through `CultMeshContentTransferService` and trusts the committed
fixture through `CustomRootTrust`. The orchestrator then judges URL parity
and the provider's fault log. CI gains the step on both OSes, and the
browser and `cultmesh-ts` steps swap to follow the workspace order (F31).
5. **C5 `c5-release`**, waiting on C4 and the Soul passes of C2 to C4. Ruling
`release-shape-gitlink`: merged to CultLib `main` with version bumps,
CHANGELOGs and git tags, and no npm publish. Line-level plan:
- Bumps (minor; every change is additive): `cultnet-ts` 0.2.0 to 0.3.0
(`./content`), `cultmesh-ts` 0.1.1 to 0.2.0 (`./content`), and `cultmesh-browser` 0.3.0 to 0.4.0 (the reader).
`@gamecult/cultcache-ts` gains only the `checkPayloadDepth` re-export:
a patch bump, 0.14.0 to 0.14.1 (its version at `b8b825e1`).
- Dependency ranges: `cultmesh-ts` and `cultmesh-browser` move
`cultnet-ts` to `^0.3.0`. `cultnet-ts`'s `^0.14.0` on
`@gamecult/cultcache-ts` already admits 0.14.1 and stays. C4 gives
`cultmesh-browser` a devDependency `cultmesh-ts: ^0.1.1`, which does not
admit 0.2.0, so it moves to `^0.2.0` in the same commit. Otherwise npm
looks for 0.2.0 on the registry and the install fails. `npm install
--no-package-lock`, the four workspace tests and the browser's
`test:interop` run after the bumps.
- A `CHANGELOG.md` per bumped package. None exists in any package at
`b8b825e1`, so C5 creates four, each naming the live surface and not the
history (Keep a Changelog headings).
- A Soul pass over the release diff, then four tags: `cultcache-ts-v…`,
`cultnet-ts-v0.3.0`, `cultmesh-ts-v0.2.0` and `cultmesh-browser-v0.4.0`.
These go in **two pushes** (at most three tags per push, SKILL.md):
`cultcache-ts` first, because its tag triggers `publish-packages.yml`
(F16), and the three plane tags second. Whether the `cultcache-ts` tag's
npm publish job still runs, and to where, is checked before that push;
follow-up `npm-publish-ts-packages` owns the rest.
- The consumer's pickup (StreamPixels bumping `vendor/CultLib` to the
tagged commit) is StreamPixels' B-cuts, not this campaign.
- Size: about 60 lines of version and CHANGELOG text; a short Hands pass.

View file

@ -0,0 +1,101 @@
# TypeScript CultMesh Content Plane Target
Campaign `cultmesh-ts-content`. Self wrote this on 2026-10-19 for the
StreamPixels M1 live character preview. That preview is a three.js page that
loads glTF bodies and accessories by content hash from Yggdrasil.
## Why now
CultMesh moves large immutable artifacts as content-addressed chunks described
by a `gamecult.mesh.cdn_artifact_manifest`. C# packs manifests, serves chunks
over TCP (`CultMeshContentServer`), fetches them over HTTPS
(`CultMeshHttpsContentTransportConnector`), and verifies each chunk and the
whole body (`CultMeshContentTransferService`). Rust speaks the chunk messages
through `cultnet-rs` `fetch_content` and `answer_content_chunk_request`.
TypeScript has none of this: no manifest type, no packer, no provider and no
reader. And no runtime has an HTTPS content *server*; the C# HTTPS connector
has never had a peer.
Standing ruling (StreamPixels `state/map.yaml`, "Content plane", operator
2026-09-15): HTTPS content transport serves both browser and overlay.
Yggdrasil plus SHA-256 verification is the trust boundary for now.
Distributed zero-trust distribution is future work. The catalog never stores
URLs; clients resolve content through CultMesh.
## Target state
- **One TypeScript owner of the CDN document shapes.** It holds the manifest
and chunk-ref types, their MessagePack wire form, `normalizeHash`, the
record-key grammar (`mesh:cdn:chunk:<hex>` and
`mesh:cdn:artifact:<token>:<version>:<hex>`), and the packer. A manifest
that TypeScript packs is byte-identical to the one C# packs from the same
bytes and options. The owner can be imported in a browser, because the
reader needs it there.
- **An HTTPS content provider in `cultmesh-ts`.** It answers
`GET <endpoint>?chunkHash=<hex>&recordKey=<key>` with exactly the chunk's
bytes and a `Content-Length` equal to the chunk's size: the request shape
the C# connector sends (`CultMeshHttpsContentTransport.cs` `BuildChunkUri`).
Its core is framework-neutral: a request maps to an answer, and a host can
mount it on `node:https` or on any HTTP framework. It reads from a content
store port. The store shipped with it serves content-addressed files from a
directory outside git. The provider never serves a byte it has not
verified against its hash, and it never serves anything but a chunk:
there is no generic static-file route.
- **A verifying reader in `cultmesh-browser`.** Given a resolved endpoint and
a manifest, it fetches the manifest's chunks over HTTPS with the C# query
shape. It checks each response's length, each chunk's SHA-256 and the whole
body's SHA-256 with WebCrypto, and returns the bytes or throws a typed
error. No byte that failed its hash reaches the caller, in whole or in part.
It runs in browsers and in Node 20+.
- **Endpoints come from CultMesh, not from content.** The reader takes the
endpoint as an argument that the caller resolved through CultMesh. Manifests
carry no URL.
## Invariants
- `wire-parity`: manifests, chunk refs and record keys are byte-identical
across C#, Rust and TypeScript for the same input. The C# runtime is the
reference. TypeScript writes and reads the shared vectors in
`contracts/cultmesh/`.
- `https-shape-parity`: the C# `CultMeshHttpsContentTransportConnector`
fetches content from the TypeScript provider unmodified, in a cross-process
test. The TypeScript reader fetches the same chunks from the same provider.
- `verify-before-use`: the reader releases no byte that failed its chunk hash
or the whole-body hash. It refuses a body longer than the advertised size at
its first excess byte, and a shorter one at its end. It does not trust
response length headers, because Fetch decodes content codings and a proxy
can rewrite them (research 2026-10-19, section 6).
- `verify-before-serve`: the provider serves only bytes whose SHA-256 equals
the requested chunk hash.
- `chunk-only-route`: the provider answers only chunk requests. A path, a
filename or a hash outside the store can never select a file.
- `no-urls-in-content`: no manifest, chunk ref or record carries an endpoint.
- `one-owner-per-shape`: one TypeScript module owns the CDN shapes, and both
packages consume it. Neither package keeps a private copy.
- `general-purpose`: the plane is judged by what a reasonable consumer
expects, not only by StreamPixels' slice (operator, 2026-09-30). Multi-chunk
artifacts, empty artifacts, cancellation, bounded concurrency and browser
cross-origin use all work.
- `released`: the npm packages StreamPixels vendors are published at versions
that contain this plane. The release is its own cut with a Soul pass before
the tag.
## Not in this campaign
- Odin route verification for content. Single-host trust is ruled for now.
- Resumable or durable transfer state in TypeScript. The browser holds
artifacts in memory.
- Distributed or zero-trust distribution, and peer serving.
- A TypeScript TCP or RUDP content server, and an HTTPS content server in Rust
or C#.
- Garbage collection of stored blobs.
- StreamPixels' own export, publish and service wiring (its B2 and B3 cuts).
StreamPixels is the consumer, not part of this change.
## Consumer
StreamPixels `map/web-customize` (dfbe2a6),
`docs/web-character-customization-map.md` section B1. Each option's artifact is
10-125 KB, and a full avatar is 0.5-0.8 MB. The browser fetches them. The
service serves them from `/srv/streampixels/content/` on Yggdrasil, outside
git, and hashes every referenced blob at boot.

View file

@ -0,0 +1,663 @@
# CultNet error contract: cut map
Status: cut map, not started. Imagination pass 1, 2026-09-30, against CultLib `main` at `8fc74c70`.
This document owns the means. There is no separate target doc: section 1 holds the ends until the
operator rules on section 3. After that, Self moves the rulings and invariants into
`contracts/cultnet/cultnet-distributed-database.md` ("Error Codes") and the schema's own description.
Open: operator questions Q1-Q8 (section 3). No cut is dispatched until Q1, Q2, Q3 and Q8 are ruled. Q4-Q7
change only single emit sites and can be ruled during Cut 1.
Depends on: `hands/cultmesh-put-serve-bound` merging first. It introduces `document_unservable` in Python
(`fd394d54`), a codeless unservable refusal in cultmesh-ts (`0eb7434a`) and `CultMeshRudpRejectionReason` in
cultmesh-rs (`c9cdad12`, `85288a5a`). The cuts below convert those sites. Their anchors are by commit, not by
line, because the branch is not on main.
Found by the put-serve Hands (batch 3, `38000efa`, 2026-09-30); they belong to this map's cuts:
- **Python codes outside the closed code list.** Python refuses with `document_unservable`,
`malformed_document_put` and `snapshot_*_exceeded`. None of these are in the closed `cultnet.error.v0` code
list, and Rust and TS peers refuse to decode an unknown code.
- **Two error-map shapes.** TS sends a two-key error map that its own peers cannot parse. Rust sends the five-key
contract shape. The two agree only at the message level.
- **The session after a refusal.** TS and Python keep the session open after a refusal; Rust sends a goodbye and
ends it.
- **New Rust refusal frame.** The put-serve branch adds a Rust refusal frame, `cultnet.error.v0` carrying the
reason text with no code and no details, sent before the goodbye and acknowledging nothing. Cut 1 gives it a
code.
- Found by the put-serve Soul (merge verdict, 2026-09-30):
- A store `OSError(ENOSPC)` in Python is refused as `malformed_document_put`, with the text "Raw put document could
not be stored." That is a server fault reported as the peer's fault.
- Over RUDP, Python's `document_unservable` details add `fragmentCount` and `maxFragmentCount`, and send
`maxSnapshotBytes: null` when it is unbounded. The map specifies `{responseBytes, maxSnapshotBytes}`.
- `response_unqueueable` (Python, P3) is a new code outside the closed list.
- A TS peer cannot tell "stored, receipt lost" from "refused" (put-serve Soul F4, pass 3).
Map branch: `main`.
---
## 0. What the Body does today (probed)
Every claim in this table was established by running code. The C#, Rust and TypeScript probes ran on
Yggdrasil through `ygg-verify.sh` at `8fc74c70`. The Python bytes came from `msgpack.packb` on the exact
dicts `cultmesh_py/server.py:573-589` and `cultmesh-ts/src/index.ts:5921-5926` build. The six probe inputs:
| Input | Bytes from |
|---|---|
| `cs_cursor_stale` | Rust `tests/error_message.rs` captured C# hex (`cursor_stale`, details `{asOf:1,current:2}`) |
| `cs_routing` | C# `new CultNetErrorMessage { Error, RoutingHint = { Reason="not_primary", Shard=... } }` serialized with `CultNetSchemaMessageSerialization` |
| `cs_codeless` | C# `new CultNetErrorMessage { Error = "boom" }` |
| `cs_unknown_code` | C# with `Code = "document_unservable"`, empty `CultNetErrorDetails` |
| `py_snapshot_byte_limit` | the Python `_error_response` shape: `messageId`, `code: "snapshot_byte_limit_exceeded"`, `details {responseBytes, maxSnapshotBytes}` |
| `ts_two_key` | the cultmesh-ts shape `{schemaVersion, error}` |
Decoded by:
| Input | C# `CultNetSchemaMessageSerialization.Deserialize` | Rust `decode_cultnet_message_from_slice` | TS `parseCultNetMessage` |
|---|---|---|---|
| `cs_cursor_stale` | ok | ok, typed | ok |
| `cs_routing` | ok | ok, **routingHint dropped** (`code: None, details: None`) | ok (untyped passthrough) |
| `cs_codeless` | ok | ok | ok |
| `cs_unknown_code` | ok (free string) | **Err: "not a recognized cultnet.error.v0 code"** | **throws: /code must be one of the allowed values** |
| `py_snapshot_byte_limit` | ok, but **details silently become an all-null object** (`responseBytes`, `maxSnapshotBytes` lost), `messageId` dropped | **Err: not a recognized code** | **throws** (missing routingHint, extra messageId, code enum, details keys) |
| `ts_two_key` | ok | ok | **throws: missing routingHint, code, details** — TypeScript cannot parse the errors TypeScript sends |
Captured C# bytes for the two shapes Rust's vector file does not hold (for Hands' reuse):
- `cs_codeless`: `85AD736368656D6156657273696F6EB063756C746E65742E6572726F722E7630A56572726F72A4626F6F6DAB726F7574696E6748696E74C0A4636F6465C0A764657461696C73C0`
- `cs_routing`: see the probe; the shard descriptor writes ten keys, nil included.
Other findings, from reading source at `8fc74c70`:
1. **The interop CI lane has been red for 40 consecutive `main` runs.** It fails at "Verify CultNet schema
contracts stayed consolidated" (`.github/workflows/cultnet-interop.yml:67-90`). That step forbids every
`*.schema.json` outside `contracts/cultnet/`, and R-AB deliberately vendors 22 copies into
`packages/cultnet-rs/contracts/cultnet/`. Those copies are held byte-equal by
`packages/cultnet-rs/tests/schema_discovery_contracts.rs:74-99`. Nothing after that step (every
cross-runtime interop test) has run on main in that time. Run `36714768445` is the latest.
2. **The schema contradicts itself.** `contracts/cultnet/cultnet.error.schema.json:29` says the code is
"Additive: a v0 peer that does not know this field ignores it" and, in the same sentence, "a closed
enum: Rust and TypeScript refuse". The closed-enum text came from Soul pass 4d (`3f942a57`). It
described what the code did; no operator ruled it. `docs/` holds no ruling on unknown codes.
3. **Emitters that break the schema:**
- cultmesh-ts sends `{schemaVersion, error}` only, at `packages/cultmesh-ts/src/index.ts:5875-5878`,
`5885-5888`, `5913-5916` and `5922-5925`, and on the branch at `0eb7434a`.
- cultmesh-py `_error_response` (`packages/cultmesh-py/src/cultmesh_py/server.py:573-589`) adds a
top-level `messageId`, omits `routingHint`, omits `code` and `details` rather than sending nil, and
sends eight out-of-enum codes: `unsupported_schema_version` (`:161`), `malformed_document_put`
(`:358`), `unregistered_document_put` (`:365`), `malformed_document_delete` (`:386`),
`unregistered_document_delete` (`:393`), `snapshot_document_limit_exceeded` (`:558`),
`snapshot_byte_limit_exceeded` (`:567`) and `simulation_observations_disabled` (`:596`). The branch
adds a ninth, `document_unservable` (`fd394d54`).
- `packages/cultnet-py/src/cultnet_py/interop_peer.py:409-417` also sends `unsupported_schema_version`.
R-2 (`docs/cultnet-selection-cut.md:2046-2053`) required that code, but it never entered the schema
enum.
4. **A second schema authority.** `packages/cultnet-py/src/cultnet_py/schema_catalog.py:17`, `:272` and
`:357-362` publish a hand-built `cultnet.error.v0` descriptor under the canonical `$id`. It has a
different shape: `messageId`, `code` as any string, `details` open, and only `schemaVersion` and `error`
required. The same file does this for every wire message (about 350 lines). This is recorded as
follow-up F-1, not cut here.
5. **Silent refusals.** cultmesh-rs's RUDP document server returns every rejection to the local caller only
(`packages/cultmesh-rs/src/rudp_document_server.rs:470-590`, `CultMeshRudpApplicationRejection`). The
peer gets nothing and times out. On the branch this includes the new `DocumentUnservable`.
6. **Receivers drop the code.**
- Every C# receiver turns an error into `new IOException(error.Error)` or
`new InvalidOperationException(error.Error)`, so a caller cannot branch on the code. The sites:
`src/GameCult.Mesh/CultMeshBodySessions.cs:310`, `CultMeshContentSessions.cs:178/256-260`,
`CultMeshPeerExchange.cs:430/466`, `CultMeshSnapshots.cs:91/107/222-226/743` and
`CultMeshVerseDiscovery.cs:273`, plus `src/GameCult.Networking/CultNetShardReplication.cs:440/574`.
- Rust reads `Error { error, .. }` at `packages/cultmesh-rs/src/lib.rs:823`.
- TS types `CultNetErrorMessage` as `{schemaVersion, error}` only (`packages/cultnet-ts/src/contracts.ts:167-170`).
- Kotlin has no error type. A request that gets an error back fails a
`require(response.schemaVersion == ...)` (`packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt:1278`,
`:1334`), and the peer's text is lost.
7. **No correlation.** Only Python echoes `messageId`.
- `cultmesh-browser` rejects an error only when exactly one operation is pending
(`packages/cultmesh-browser/src/index.ts:724-731`); otherwise the error is dropped and the operation
times out.
- C# `CultMeshContentSessions.OnError` (`:256-260`) fails every pending request on any error.
8. **The reference is inconsistent with itself.** `src/GameCult.Networking/CultNetDatabaseServer.cs:560`
refuses a variant change with a codeless error. The subscription server refuses the same condition with
`ForVariantUnsupported` (`CultNetDatabaseSubscriptionServer.cs:211/333/366`).
9. **routingHint is never consumed.** No receiver in any runtime reads it. C# builds it at
`CultNetDatabaseServer.cs:638-650`, and Rust's decode discards it.
10. **Consumer audit** (read-only, all of `F:\Projects` except CultLib; no git in Aetheria):
- Outside CultLib, nothing sets `code`, `details` or `routingHint`, and nothing branches on a code.
- Emitters:
- Huginn: `crates/huginn-daemon/src/serve.rs:288/307/312`, Rust struct literals
`Error { error, code: None, details: None }`, pinned at cultnet-rs `8fc74c70`.
- AetheriaEve: `Aetheria.State.Daemon/Program.cs:1737/1778`, C# object initializers.
- Decoders read `.error` only: Gjallar `Program.cs:574`, Mimir
`MimirOdinMoveProofEvidenceRingProvider.cs:80`, Hermodr `hermodr-daemon.cjs:1380` (hand-rolled),
AetheriaEve's smoke test (text match against its own emit text), and Huginn's tests (text match).
- Stale vendored TS:
- `StreamPixels/vendor/CultLib` carries the current closed schema, so any change here breaks its
TS error decode until it is re-vendored.
- `Heimdall/vendor/CultLib` carries a pre-R-X schema with `additionalProperties:false` and no
`routingHint`, so it already refuses every error C# sends today.
- Odin, Idunn, Muninn, Ghostlight and voidbot: zero hits.
## 1. Ends
- **One schema.** `contracts/cultnet/cultnet.error.schema.json` is the only authority on the shape and on the
code registry. C# embeds it (`GameCult.Networking.csproj:19`), TS mirrors it at build time, and Rust
vendors a copy held byte-equal by test. Nothing else publishes a `cultnet.error.v0` shape (F-1 covers
Python).
- **Every runtime emits only schema-valid errors.** This is proven per emit path against the schema, and
byte-equal to the C# reference for every registry code.
- **Every runtime decodes every error it can receive into a typed value.** A known code becomes its typed
variant with its typed details. An unknown code, if Q1 says so, becomes a typed "other" that keeps the
code string and the raw details. No well-formed error is ever refused, and no field is silently dropped
(routingHint, messageId).
- **Code and details have one owner.** The details a message carries are decided by its code. The types in
every runtime make "cursor_stale with a field" unrepresentable. No free-standing code string plus a
grab-bag details object.
- **A refusal is an answer, not a silence** (R-2 precedent, and Q4). A server that refuses a peer's request,
and can still send, sends a coded error that correlates to the request.
- **The public surface is complete in every runtime**, per the consumer-expectation doctrine:
- a typed error value;
- a constructor per registry code;
- a typed exception or error that request APIs raise when the peer refuses, carrying the typed value.
## 2. Identity, lifecycle and authority
| Kind | What names it | Lifecycle | Who decides |
|---|---|---|---|
| Error message | `schemaVersion: cultnet.error.v0`; correlated to its request by `messageId` (Q3) | Sent once, in reply to one request or subscription. Never persisted. | The server that refused. |
| Error code | A snake_case string in the schema's registry (`$defs/knownCode`) | Added by one coordinated change: schema, C# constructor, a C#-written vector, and a typed variant in every runtime. Never renamed and never reused. A retired code stays in the registry marked retired, so an old reader still decodes it. | The operator rules meaning; Self keeps the registry. |
| Details for a code | The code | Frozen with the code. Adding a key to an existing code's details is a new code (or a v1), never an in-place edit (see Q2). | Same as the code. |
| Unknown code, at a reader | The raw string | Preserved as typed `Other`. The reader acts on the `error` text and the absence of a known code. | The reader (Q1). |
| Vector file | `contracts/cultnet/interop/error-vectors.cs-written.json` | Regenerated only by the C# writer test under `CULTNET_WRITE_VECTORS=1` and committed. Every other runtime judges it read-only. | C#, the reference. |
| routingHint | Shard authority refusal | Sent with a null code (unchanged). Typed in every decoder. | CultNetDatabaseServer's authority path. |
## 3. Operator questions
**Rulings (operator, 2026-09-30):** "Q1: B / Q2: B / Q3: A / Q8: A".
- **Q1:** closed for emitters, open for readers. An unknown code decodes to a typed `Other`.
- **Q2:** per-code details.
- **Q3:** a nullable top-level `messageId`.
- **Q8:** v0 is corrected in place, with the semver bumps the policy requires.
- Q4-Q7 are still open.
- Cut 1 waits for `hands/cultmesh-put-serve-bound` to merge.
### Q1. What does a reader do with an error code it does not know?
Today Rust and TypeScript refuse the whole message. The probes above show the result: the peer's error text
is lost and the caller sees "not a recognized code". In cultmesh-rs's snapshot client the `?` at `lib.rs:812-815`
even turns a peer refusal into a local decode failure. So under a closed enum every new code breaks every
reader that has not upgraded. By `docs/semver-policy.md:86-91` that makes each new code a breaking wire
change, and nine codes are already in flight.
- **A. Closed enum (today).** Readers refuse unknown codes. Every code addition is a coordinated,
semver-major wire change across all runtimes and consumers.
- **B. Closed for emitters, open for readers.** The schema keeps the registry, and every runtime's emit
constructors can produce only registry codes (tests prove each emit path valid against the schema).
Readers decode an unrecognized code (matching `^[a-z][a-z0-9_]*$`) into a typed `Other { code, details }`.
A new code is an additive, minor change: old readers still surface the text and know "an error I cannot
classify". This is how gRPC status codes and protobuf open enums behave.
- **C. Fully open.** No registry; any string, emit and decode.
**Recommended: B.** A refusal must never fail to arrive because it is more specific than the reader
expected. B keeps the registry as the single meaning authority. C loses the meaning authority, and A makes
the error channel the most brittle message in CultNet.
Depends on this: schema `code` shape, every decoder, the doc text at `cultnet-distributed-database.md:107-115`.
### Q2. How are details shaped on the wire?
Today C# writes all four detail keys (`field`, `value`, `asOf`, `current`), nil included, because
`CultNetErrorDetails` (`CultNetSchemaMessages.cs:592-603`) is a MessagePack map-mode object. The codes in
flight need new keys: `responseBytes`, `maxSnapshotBytes`, `documentCount`, `maxSnapshotDocuments` and
`schemaVersion`.
- **A. One shared details record; grow it.** Every detail key of every code sits on one record, written as
nil when unused. Each new key changes the bytes of every existing error that has details, so all of the
C# vectors must be recaptured on every addition. It also breaks every strict reader: TS Ajv, and
StreamPixels' vendored schema, have `additionalProperties:false`. And the type cannot tie a key to its
code.
- **B. Per-code details.** `details` carries exactly the keys its code defines. A defined-but-optional key
is present as nil; another code's keys are absent. Keys are named for their meaning, which retires the
`field: "schemaId"` / `field: "subscriptionId"` indirection that exists only because the record is
shared. Adding a code never changes an existing code's bytes. In every runtime, code and details are
fused into one tagged type:
- Rust: an enum with data.
- C#: sealed subclasses, with a custom formatter for `CultNetErrorMessage`. There is precedent in
`src/GameCult.Mesh/CultMeshRouteRecordFormatter.cs`.
- TS: a discriminated union.
- Kotlin: a sealed class.
- Python: constructors per code over a frozen dataclass.
The churn is one time. The five existing C# cases with details change bytes, and all eight Rust hex
cases move into the shared vector file anyway (Cut 1).
- **C. Opaque string map.** Untyped; no runtime can offer a typed accessor.
**Recommended: B.** A is the churn the brief warned of, and it recurs on every future code. B pays it once,
at the moment every vector is being recaptured regardless.
Depends on this: the schema's `details`, the C# model and formatter, `encode_error_details`
(`packages/cultnet-rs/src/contracts.rs:1339-1366`), and every decoder.
### Q3. Does an error carry the `messageId` of the request it refuses?
Python already sends `messageId` (`server.py:583`); the schema forbids it. Without it:
- `cultmesh-browser` drops an error whenever two operations are pending (`index.ts:724-731`);
- C# content sessions fail every pending request on any error (`CultMeshContentSessions.cs:256-260`).
Adding a top-level key changes the bytes of every C# error. That happens once, in the same recapture as Q2.
- **A.** Add a nullable top-level `messageId`. A reply to a request echoes the request's id, and an error
with no request (a subscription push, an unsolicited refusal) sends nil. Receivers correlate by it and
fall back to their current behavior when it is nil.
- **B.** No correlation. Python deletes the field.
**Recommended: A.** Correlation is a capability any multiplexing client needs, and one runtime already
depends on it.
### Q4. Do servers send `document_unservable` (and every other refusal they can) to the peer?
Self ruled that "unservable" must be distinguishable as a typed code in every runtime. On the branch:
Python sends it coded, TypeScript sends it codeless, and Rust tells only its local caller, so the Rust
peer times out. The same Rust silence covers every other document-server rejection: a sink refusal, a
failed snapshot source, too many snapshot documents, and a snapshot response that is too large.
- **A.** Every refusal of a peer's request that the server can still send is answered with a coded error
carrying the request's `messageId`, and is also reported locally (Rust's `ApplicationRejected` stays as
the local report). The refusals that cannot be answered are the ones where the answer itself cannot be
queued: Rust `PayloadBudgetFull` and `ResponseQueueFailed`. Those stay local-only, and the doc says so.
- **B.** Local-only in Rust; peer replies only where a runtime already sends them.
**Recommended: A.** R-2 already ruled that a silence is worse than a missing feature
(`docs/cultnet-selection-cut.md:2049-2053`), and the operator's ruling that CultMesh "refuses" puts only
means something to the party that sent the put.
### Q5. Does Python's `unregistered_document_put` / `unregistered_document_delete` become `unowned_schema`?
Python sends these when the node has no registered document type for the record's schemaId
(`cultmesh_py/node.py:653-666`). C# `unowned_schema` means "no shard owns this schema and key"
(`CultNetSchemaMessages.cs:537-544`). To a peer both mean "this server will not take that schema here; route
elsewhere". The put and delete variants duplicate what the peer already knows (it sent the message).
- **A.** Both collapse into `unowned_schema { schemaId }`. The registry text broadens to "this server
stores no document for that schema (and key)".
- **B.** Join as a separate `schema_unregistered { schemaId }`.
- **C.** Join both codes as they are.
**Recommended: A.** It is one meaning at the peer, and there is one action to take.
### Q6. Does Python's `malformed_document_put` / `malformed_document_delete` become one `message_invalid { field }`?
These are sent when the request lacks a required field (`server.py:355-359`, `383-387`). C# has the same
condition, codeless, at `CultNetDatabaseServer.cs:373/415` and `CultNetDatabaseSubscriptionServer.cs:148`
("requires a subscriptionId or messageId").
- **A.** One `message_invalid { field }`, naming the missing or ill-typed field. It is used by Python's four
sites and C#'s three.
- **B.** Join both per-message codes and leave C# codeless.
**Recommended: A.** The per-message suffix repeats the request's own schemaVersion, and C# gains a code for
free.
### Q7. Does `simulation_observations_disabled` become `unsupported_schema_version`?
Python sends it when the observation hub is off (`server.py:593-598`), with
`details { schemaVersion: "cultnet.simulation_observation.v0" }`, which is exactly the
`unsupported_schema_version` shape. cultmesh-ts's "no cache for snapshot requests" and "no operation
handler" refusals (`index.ts:5875`, `5913`) mean the same thing: this server does not serve that message.
- **A.** Collapse. `unsupported_schema_version` means "this server does not serve messages of this
schemaVersion: unknown, a newer version, or not enabled here". The three TS sites and Huginn's "answers
only X, Y, Z" adopt it.
- **B.** Keep a separate `feature_disabled`-style code.
**Recommended: A.** A peer cannot act differently on "not enabled" and "not known". Python's
`verify.py:335` check follows the rename.
### Q8. Is this a correction of `cultnet.error.v0` in place, or a new `cultnet.error.v1`?
Under Q2-B and Q3-A, strict v0 readers (TS Ajv and vendored copies) refuse the new bytes. Tolerant readers
(C#, Rust, Python) keep working, because the probes show they ignore unknown keys and treat a missing key
as null.
- **A. In place.** The v0 contract has never been decodable across all five runtimes (section 0). No
consumer outside CultLib reads code or details (finding 10). Every publishing package takes the semver
bump its policy requires:
- `org.gamecult.cultlib` major, because the public `CultNetErrorDetails` properties change;
- `cultnet-py` and `cultmesh-py` major, because emitted codes change;
- Rust pins move deliberately.
StreamPixels and Heimdall re-vendor.
- **B. v1.** Add `cultnet.error.v1`, keep v0 emit for peers that did not advertise v1 in hello, and run
two error encoders in every runtime until v0 retires.
**Recommended: A.** B builds a version-negotiation path for a message nobody outside CultLib parses beyond
`.error`. The one thing every old reader does need, the `error` text, survives A in every tolerant runtime.
## 4. The registry (as recommended; rewrite on each ruling)
| Code | Meaning | Details (per Q2-B; `?` = present-as-nil when absent) | Emitted by after this map |
|---|---|---|---|
| `selection_invalid` | A selection field failed the door. | `{ field, value? }` | C#, Rust (selection) |
| `cursor_stale` | The cursor predates the server's retained history. | `{ asOf, current }` | C#, Rust |
| `cursor_invalid` | The cursor could not be parsed. | none (details nil) | C#, Rust |
| `reference_outside_target` | A hop reference left the declared target. | none | C#, Rust |
| `variant_unsupported` | The selection selects a document variant CultNet does not carry. | `{ subscriptionId? }` | C# (incl. `DatabaseServer.cs:560`) |
| `unowned_schema` | This server stores no document for that schema (and key). | `{ schemaId }` | C#, Python (Q5) |
| `unsupported_schema_version` | This server does not serve messages of this schemaVersion. | `{ schemaVersion }` | Python, TS, Huginn (Q7) |
| `message_invalid` | The request lacks a required field or has an ill-typed one. | `{ field }` | C#, Python (Q6) |
| `snapshot_document_limit_exceeded` | The snapshot answer would exceed the document bound. | `{ documentCount, maxSnapshotDocuments }` | Python, Rust (Q4) |
| `snapshot_byte_limit_exceeded` | The snapshot answer would exceed the byte bound. | `{ responseBytes, maxSnapshotBytes }` | Python, Rust (Q4) |
| `document_unservable` | A put was refused because no snapshot could ever return the stored document under the byte bound. | `{ responseBytes, maxSnapshotBytes }` | Python, TS, Rust (Q4) |
A null code is legal and means "unclassified". Examples: an exception message from a failed snapshot
source, an authority refusal carrying `routingHint`, and "not authorized"
(`CultNetDatabaseSubscriptionServer.cs:165`). This map does not invent codes for those.
`snapshot_byte_limit_exceeded` and `document_unservable` have identical details. That is deliberate, and it
is the fixture trap named in the verification rules.
---
## 5. Cuts
All cuts are made on one campaign branch, `hands/error-contract`, from `main` after
`hands/cultmesh-put-serve-bound` merges. The branch merges to `main` once, after Cut 5's Soul pass. The
reason: Cut 1 changes C# bytes, and Rust's static hex cases would stay green against the stale bytes
(finding: `tests/error_message.rs` pins constants, not the reference). A partial merge would ship a
wire split while every suite passed. Each cut gets its own worktree, created by Self. Heavy verification
runs through `ygg-verify.sh`, never on Starfire.
### Cut 0. Unblock the interop lane
- **Repo/branch:** CultLib, `hands/interop-guard` from `main`, merged alone ahead of the campaign. It
does not depend on the rulings.
- **Deletes first:** nothing.
- **Per-file changes:** `.github/workflows/cultnet-interop.yml:72`. The `$stray` filter also accepts
`^packages/cultnet-rs/contracts/cultnet/`, with a comment naming R-AB and
`packages/cultnet-rs/tests/schema_discovery_contracts.rs:74-99` as the equality guard. The namespace
check is unchanged.
- **Authority map:** no ownership change. The canonical owner stays `contracts/cultnet/`, and the Rust
mirror is derived from it, with drift judged by the Rust test.
- **Verification:**
- Push the branch and run the workflow. Report every step that goes red after the guard. None of those
steps has run on main for 40 runs, so expect more red, and treat each failure as its own finding;
do not fix it in this cut.
- Negative: edit one byte of a vendored copy locally and check that the Rust equality test fails.
Reach it on Yggdrasil with `cd packages/cultnet-rs && cargo test --test schema_discovery_contracts`.
- **Ledger:** +3 lines.
### Cut 1. The contract and the C# reference
- **Repo/branch:** CultLib `hands/error-contract`, worktree named by Self.
- **First:** capture the current C# bytes for all eight factory cases, plus `cs_codeless` and `cs_routing`,
into the Hands report. This is the before-image a Soul pass compares against.
- **Deletes first:**
- `CultNetErrorDetails` (`src/GameCult.Networking/CultNetSchemaMessages.cs:588-603`, 16 lines) as a
shared record.
- The `field`/`value`/`asOf`/`current` property block and the `[MessagePackObject]`/`[Key]` attributes
on `CultNetErrorMessage` (`:497-521`). Its serialization moves to one formatter.
- `SoulP4dProbeTests.Wire_UnownedSchemaBytes` (`tests/GameCult.Networking.Tests/SoulP4dProbeTests.cs:191-199`).
It is an `[Explicit]` hex printer, superseded by the vector writer.
- The closed-enum paragraph at `contracts/cultnet/cultnet-distributed-database.md:107-115` and
the `code`/`details` descriptions in the schema (`:28-42`).
- **Adds:**
- **Schema** (`contracts/cultnet/cultnet.error.schema.json`, rewritten; the Rust mirror is re-copied
byte-equal):
- Top level: `schemaVersion`, `error` and `routingHint` as today, plus `messageId: string|null` (Q3)
and `code`. All five are required, present-as-nil (R-X is unchanged at the top level).
- `$defs/knownCode` is the registry enum from section 4.
- `code` is `null`, or a string matching `^[a-z][a-z0-9_]*$` (Q1-B).
- `details` is decided by a top-level `oneOf`, one branch per known code, each fixing `code` by
`const` and `details` by exact `properties`/`required`/`additionalProperties:false`. Two more
branches: a null code with null details, and an unknown code (`not: {$ref knownCode}`) with details
`object|null`, open.
- Use `oneOf`/`not`/`const` only. `tests/GameCult.Networking.Tests/MiniJsonSchemaValidator.cs:30-35`
implements those and **not** `if`/`then`/`allOf`, and it refuses unimplemented keywords. TS
compiles with Ajv2020 (`packages/cultnet-ts/src/contracts.ts:1,674`), which handles both.
- **C# model** (`GameCult.Networking`):
- `CultNetErrorMessage { SchemaVersion, Error, MessageId?, RoutingHint?, Refusal? }`, where
`Code => Refusal?.Code` is derived and not settable.
- `abstract CultNetRefusal { string Code }`, with one sealed subclass per registry code carrying its
typed details (`CultNetCursorStaleRefusal(ulong AsOf, ulong Current)` and so on), plus
`CultNetUnknownRefusal(string Code, IReadOnlyDictionary<string, object?>? Details)` for Q1-B.
- `CultNetErrorCodes`: string constants, one per registry code.
- `CultNetErrorMessageFormatter : IMessagePackFormatter<CultNetErrorMessage>`. It writes the five
top-level keys in order (schemaVersion, error, messageId, routingHint, code, details), and details
as exactly the refusal's keys. It reads by key name, tolerates unknown keys at both levels, and
maps an unknown code to `CultNetUnknownRefusal`. Register it through `[MessagePackFormatter]` on
the type, the way `CultMeshRouteRecordFormatter` is wired.
- Factories: keep `ForSelectionInvalid`, `ForCursor`, `ForReferenceOutsideTarget`,
`ForVariantUnsupported` and `ForUnownedSchema` as thin constructors over the refusal types, and add
one per new code. Each takes an optional `messageId`.
- `CultNetPeerErrorException : IOException`, carrying the `CultNetErrorMessage`.
- **Vector writer:** `ErrorVectorTests.WriteErrorVectors`, gated on `CULTNET_WRITE_VECTORS=1` like
`SelectionParityVectorTests.cs:460-552`. It writes `contracts/cultnet/interop/error-vectors.cs-written.json`:
one entry per registry code (both optional-key states where a key is optional), `cs_codeless`,
`cs_routing`, one unknown code with open details, and one error with `messageId` set. Each entry
holds `{ name, messagePackBase64, expected: { error, messageId, code, details, routingHint } }`,
where `expected` is a JSON projection every runtime compares its typed decode against.
- **Per-file changes** (`8fc74c70`):
- `CultNetDatabaseServer.cs:560`: `ForVariantUnsupported(refusal.Message, subscriptionId)`.
- `CultNetDatabaseServer.cs:373`, `:415` and `CultNetDatabaseSubscriptionServer.cs:148` become
`message_invalid { field: "subscriptionId" }` (Q6-A).
- Every `SendCultNet(CultNetErrorMessage...)` that answers a request passes the request's `MessageId`.
- Each receiver listed in finding 6 throws `new CultNetPeerErrorException(error)` instead of
wrapping `error.Error`. `CultMeshContentSessions.OnError` (`:256-260`) fails only the pending entry
whose id matches `error.MessageId`, and falls back to all entries only when the id is nil.
- `CultNetSchemaContractTests.cs:165-214`: the per-factory tests are replaced by one test that validates
every vector's decoded JSON against the schema with `MiniJsonSchemaValidator`, and one test that
serializes each registry factory and asserts byte equality with its committed vector.
- Rewrite `contracts/cultnet/cultnet-distributed-database.md` "Error Codes" to the registry, and state
the Q1 and Q2 rules.
- **Authority map:**
- Owner: the schema's registry owns code meaning and detail shape. `CultNetErrorMessageFormatter` owns
C# bytes. The refusal subclass owns the code-to-details binding.
- Inputs: the refusal instance, messageId, routingHint.
- Outputs: bytes, and `cs-written.json` for the other runtimes.
- Derived state: `CultNetErrorMessage.Code` becomes derived; the refusal decides it.
- Forbidden writers: any `Code = "..."` assignment (it cannot compile); `[Key]` attribute serialization
of the message; hand-typed hex anywhere.
- Shared paths: every server send uses the factories; every receiver uses `CultNetPeerErrorException`.
- Deletion line: `CultNetErrorDetails` and the settable `Code`.
- **Verification** (`dotnet` image):
- Builds: `tests/GameCult.Networking.Tests`, `tests/GameCult.Mesh.Tests`.
- `ErrorVectorTests` with `CULTNET_WRITE_VECTORS=1`, then without, and check that the committed file
reproduces byte-for-byte.
- Tests and the rules they pin:
- vector round trip: bytes, decode, bytes are equal for every entry;
- a registry factory is byte-equal to its vector;
- every vector validates against the schema;
- an unknown code decodes to `CultNetUnknownRefusal` with its raw details intact;
- a Python-shaped error (`py_snapshot_byte_limit` above, with the Q2/Q3 shape) decodes typed with
`responseBytes` and `maxSnapshotBytes` intact. Today's C# silently empties those.
- content-session correlation: two pending requests, an error for the second, and the first still
completes.
- Mutation: Stryker.NET on the formatter and factories. The survivors that must die:
- swapping two known codes whose details are identical (`snapshot_byte_limit_exceeded` and
`document_unservable`);
- padding a nil key into details;
- dropping `messageId` on encode;
- mapping an unknown code to null.
- Negative greps (checked at HEAD, where they hit the sites being cut):
- `git grep -nE 'new (IOException|InvalidOperationException)\(error\.Error\)' -- src` must return 0
(9 hits at HEAD).
- `git grep -nE 'Field = "(schemaId|subscriptionId)"' -- src` must return 0 (2 at HEAD).
- Operator: none.
- **Ledger:** about -60 C# (the details record, the settable code, the probe printer, and the per-factory
schema tests), +250 (the formatter, the refusal types, the vector writer). The schema grows by about 90
JSON lines. The net is positive, bought by Q1/Q2 typing in the reference.
### Cut 2. Rust: cultnet-rs and cultmesh-rs
- **Deletes first:**
- `CultNetErrorCode` and `CultNetErrorDetails` as separate types (`packages/cultnet-rs/src/contracts.rs:252-310`,
59 lines);
- `require_error_details` (`:1121-1146`);
- `encode_error_details` (`:1339-1366`);
- the eight hex `Case`s and the header comment in `packages/cultnet-rs/tests/error_message.rs:1-160`.
That file is rewritten to read the vector file.
- Keep `require_legacy_optional_u64` (`:1149`). Check with `git grep` for other callers first; at HEAD,
only the error details use it.
- **Adds:**
- `CultNetMessage::Error { error: String, message_id: Option<String>, routing_hint: Option<CultNetShardRoutingHint>, refusal: Option<CultNetRefusal> }`.
- `enum CultNetRefusal`, with one variant per registry code and its typed fields, plus
`Other { code: String, details: Option<rmpv::Value> }`.
- `CultNetRefusal::code(&self) -> &str`.
- A constructor `CultNetMessage::error(text)` and builder-style `with_refusal` / `with_message_id`, so
consumers stop writing struct literals. Huginn has three.
- Decode reads every top-level key by name and routes unknown codes to `Other` (Q1-B).
- Encode writes the Cut 1 key order.
- `CultNetShardRoutingHint` is typed, reusing the crate's existing shard descriptor type.
- **Per-file changes:**
- `packages/cultnet-rs/src/contracts.rs:361-368` (variant), `:1097-1115` (decode), `:1300-1336` (encode).
- `packages/cultnet-rs/src/selection.rs:1210-1255`: the four refusals build `CultNetRefusal` variants.
- `packages/cultnet-rs/src/content.rs:316`: uses `CultNetMessage::error`.
- `packages/cultmesh-rs/src/lib.rs:812-827`: a peer error becomes a typed error carrying the refusal,
instead of `anyhow!` text. Name it `CultMeshPeerError`, next to the crate's existing error types.
- `packages/cultmesh-rs/src/rudp_document_server.rs` (Q4-A): each `CultMeshRudpRejectionReason` that
can be answered sends the matching coded error, correlated by the request's message id, before
returning `ApplicationRejected`. `PayloadBudgetFull` and `ResponseQueueFailed` stay local.
- **Authority map:**
- Owner: `CultNetRefusal` owns code and details. The schema owns the registry.
- Forbidden writers: string codes (`from_wire_str` survives only inside decode); struct literals of
details.
- Shared paths: selection refusals, document-server refusals and content errors all go through the one
encoder.
- **Verification** (`rust` image, `cd packages/cultnet-rs` then `cd ../cultmesh-rs`):
- `cargo test --workspace`.
- `tests/error_message.rs` pins: every vector decodes to `expected`; encoding the decoded value
reproduces the vector's bytes; the unknown-code vector decodes to `Other`; the routing vector keeps
`routing_hint`.
- cultmesh-rs: a put over the bound makes the peer receive `document_unservable` with the request's
message id, and details equal to the computed size and the bound. Run it at two bounds so that a
constant or an offset in `maxSnapshotBytes` dies.
- A peer that sends a Python-shaped `snapshot_byte_limit_exceeded` produces a `CultMeshPeerError`, not
a decode failure.
- Mutation: `cargo mutants --in-diff` on the cut's diff. Triage every survivor by name.
- Negative: `git grep -n 'CultNetErrorDetails' -- packages` must return 0.
- **Consumer note:** Huginn's `serve.rs:288/307/312` and its tests stop compiling at the next pin bump.
The migration is mechanical (`CultNetMessage::error(...)`), and `:312` adopts `unsupported_schema_version`
(Q7). Self routes it as a Huginn change; it is not part of this branch.
### Cut 3. TypeScript: cultnet-ts, cultmesh-ts and cultmesh-browser
- **Deletes first:**
- the two-field `CultNetErrorMessage` interface (`packages/cultnet-ts/src/contracts.ts:167-170`);
- the four literal error constructions in `packages/cultmesh-ts/src/index.ts:5875-5878`, `5885-5888`,
`5913-5916` and `5922-5925`, and the branch's codeless unservable site (`0eb7434a`);
- `rejectCorrelated`'s single-pending heuristic (`packages/cultmesh-browser/src/index.ts:724-731`).
- **Adds:**
- `CultNetErrorMessage` with every field.
- `type CultNetRefusal` as a discriminated union on `code`, with an `{ code: string; details: Record<string, unknown> | null; known: false }` arm.
- `createCultNetError(text, { messageId, refusal })` as the only constructor.
- `class CultNetPeerError extends Error { message: CultNetErrorMessage }`.
- `parseCultNetMessage` keeps Ajv validation against the canonical schema; it now accepts every schema
branch, including unknown codes.
- **Per-file changes:**
- cultmesh-ts: the three "does not serve this" sites (`5875`, `5913`, `5922`) send
`unsupported_schema_version` (Q7-A); the snapshot catch (`5885`) sends a null code; the unservable site
sends `document_unservable` with details.
- cultmesh-ts `6527-6529` throws `CultNetPeerError`.
- cultmesh-browser `163`, `584` and `690`: correlate by `messageId`, falling back to today's behavior only
for a nil id.
- `packages/cultnet-ts/src/idunn-odin-presence-publisher.ts:118-120` throws `CultNetPeerError`.
- The legacy `gamecult.networking.v0` path (`contracts.ts:840-844`, `:899-900`) is unchanged. That is a
separate legacy contract.
- **Verification** (`kotlin` image, which carries Node 24; `npm install`, then build cultcache-ts before
cultnet-ts):
- A new `test/error-vectors.test.ts` pins that every vector parses and deep-equals `expected`, and that
`encode(createCultNetError(...))` is byte-equal to each registry vector.
- The cultmesh-ts document-server tests assert that each refusal parses with TypeScript's own
`parseCultNetMessage`. That is the probe's `ts_two_key` failure, pinned.
- Browser: two pending operations, an error for the second, and the first still resolves.
- Mutation: StrykerJS on the diff.
- Negative: `git grep -nE 'schemaVersion: "cultnet\.error\.v0",' -- packages/cultmesh-ts/src packages/cultmesh-browser/src`
must return 0. Exclude `cultnet-ts/src/schema-discovery.ts:302` (a descriptor) and
`contracts.ts:842` (legacy decode); both are legitimate hits for the unscoped pattern.
### Cut 4. Python: cultnet-py and cultmesh-py
- **Deletes first:**
- `_error_response` (`packages/cultmesh-py/src/cultmesh_py/server.py:573-589`);
- `unsupported_schema_version_error` (`packages/cultnet-py/src/cultnet_py/interop_peer.py:409-417`);
- every string literal code in `server.py` (lines in finding 3).
- **Adds:**
- `cultnet_py/errors.py`, holding:
- `CultNetErrorCode(StrEnum)`, the registry;
- a frozen `CultNetError` dataclass (`error`, `message_id`, `routing_hint`, `code: CultNetErrorCode | str | None`,
`details: Mapping | None`);
- one constructor function per code, taking exactly that code's keys;
- `to_wire()`, which writes the five top-level keys in the Cut 1 order, nil included;
- `parse_cultnet_error(dict)`.
- `CultNetPeerError` (`client.py:46-50`) keeps `.response` and gains `.error: CultNetError`.
- **Per-file changes:**
- Sites `server.py:158-163`, `355-394`, `555-569` and `593-598` and the branch's `document_unservable`
site use the constructors, with the Q5-A/Q6-A/Q7-A codes.
- `interop_peer.py` uses the constructor.
- `verify.py:335` reads `error.error.code`.
- Tests `packages/cultmesh-py/tests/test_cultmesh.py:2088`, `3791`, `4323` and `4363-4371`, and
`packages/cultnet-py/tests/test_cultnet.py:2342`, follow the renames.
- **Verification** (the image the campaign already uses for Python; state it in the report):
- A vector test pins that every vector parses to `expected` and that each constructor's `msgpack.packb`
is byte-equal to its vector.
- Mutation: mutmut on `errors.py` and the changed server sites.
- Negative: `git grep -nE '"schemaVersion": "cultnet\.error\.v0"' -- packages/cultnet-py/src packages/cultmesh-py/src`
returns only `errors.py`.
- **Not here:** F-1 (`schema_catalog.py` publishes its own shapes).
### Cut 5. Kotlin: cultmesh-kotlin
- **Adds:**
- A `CultNetErrorMessage` data class and a `sealed class CultNetRefusal` (known codes plus `Other`),
with a `CultNetMessage.asError(): CultNetErrorMessage?` view.
- `class CultNetPeerException(val error: CultNetErrorMessage) : IOException`.
- At `CultMesh.kt:1278` and `:1334`, and at every `receiveSchemaMessage` consumer that expects a
response, an error response throws `CultNetPeerException` before the `require`.
- **Verification** (`kotlin` image, `pwsh -File build.ps1`):
- A vector test pins that every vector decodes to `expected`.
- Kotlin emits no errors today, so encode parity applies only if a Kotlin server path is added. Say
so in the report; do not invent one.
- There is no mutation tool. Behavioural tests cover it: an error reply to a schema-catalog request
raises `CultNetPeerException` carrying the code.
### Cut 6. Merge, release, consumers
- Soul runs over the whole branch before the merge:
- rerun every vector suite;
- decode each runtime's live emissions with every other runtime. The interop lane from Cut 0 is the
harness; add an error round to `tests/GameCult.Networking.InteropPeer` and
`packages/cultnet-ts/test/interop` if Soul finds a runtime pair unreached.
- Release per `docs/semver-policy.md`, with a Soul pass before tags:
- `org.gamecult.cultlib` major, and the tracked Unity DLLs rebuilt, since they embed the schema;
- `cultnet-py` and `cultmesh-py` major.
- Consumers, routed by Self as separate repo changes:
- Huginn: pin bump plus Cut 2's note.
- StreamPixels and Heimdall: re-vendor. Heimdall is already broken by R-X.
- AetheriaEve, Gjallar and Mimir compile against the live CultLib path and read only `.Error`, so they
need no change. That is inferred from the audit; the next build of each confirms it.
- Hermodr: none.
## 6. Subtraction ledger
| Cut | Removed (estimate) | Added (estimate) | Formats / targets |
|---|---|---|---|
| 0 | 0 | 3 | none |
| 1 | ~60 C#, schema text | ~250 C#, ~90 schema | + `error-vectors.cs-written.json`; - `[Explicit]` hex printer |
| 2 | ~150 (types, two helpers, 160-line hex test) | ~200 | hex constants gone; vectors read from file |
| 3 | ~40 | ~120 | none |
| 4 | ~40 | ~120 (`errors.py`) | 8 ad-hoc codes → registry |
| 5 | 0 | ~80 | none |
The net is positive, by roughly 500 lines, across five runtimes. It buys typed decode where there was
none (Kotlin, TS and Python), a correlation capability, and one vector file in place of hand-copied hex.
The pressure point: if Cut 1's formatter passes 150 lines, report it rather than trimming typing.
## 7. Follow-ups outside this map
- **F-1.** `packages/cultnet-py/src/cultnet_py/schema_catalog.py` publishes about 350 lines of hand-built
schemas under canonical `$id`s. Python should package and serve `contracts/cultnet/*.schema.json`, the
way Rust vendors them. It is a separate cut, because it covers every message, not errors. It can wait
because the catalog is advisory: no runtime validates a peer's message against a published descriptor.
- **F-2.** Codes for the remaining codeless refusals (authorization, failed snapshot sources, routing).
Wait for a consumer that branches on them.
- **F-3.** Whatever the Cut 0 lane reveals once its later steps run again.

View file

@ -1,5 +1,28 @@
# CultNet RUDP: Acknowledged means delivered, one watermark owns ordered delivery, and a write belongs to its session
**PIVOT, 2026-09-30 (operator): RUDP is retired in favour of QUIC.** Operator: "RUDP really ballooned into
maintaining a robust custom transport across runtimes, when it started as just 'hey, let's do LiteNetLib but
better'. Is there any point maintaining it when we have QUIC?" and then "Yeah, pivot to QUIC adoption, I ain't
gonna let sunk costs take the wheel."
- **Landed 2026-09-30: Cut D, merged at `2f06ef84`** (Soul passes 1-4, batch 5 closing pass 4). It fixes live crashes: a spoofed datagram crashing Node, a malformed frame
killing Python's server, C# stale peers corrupting sessions.
- **Parked, not built:** Cut F, Cut 2 (held on F), Cut 4, Cut K and K1 (including the Kotlin resend-loop gap), Cut 7
and the receipts port. Nothing of theirs exists to tag, so this map is their record.
- Defects found in RUDP from here on are fixed only if they are live crashes or data loss, until the QUIC campaign
sets RUDP's retirement.
- **Data-loss item from Odin Soul S-5 (2026-09-30).** At Odin's pin `3bf1c0ce`, cultnet-rs records a reliable
packet as received when it arrives, and `create_ack` reports it while ordered delivery still holds an earlier
packet.
- Probe: two pipelined puts, with the first datagram dropped. The second put was ACKed but not on disk. If the
first never arrives, the ACKed second put is never delivered.
- Sessions that carry a single put are safe.
- **Checked (Odin Q5 Imagination, probe P1 at `e4e2a0b9`): still open after ack Cuts 1-1c.** A held, undelivered
put is ACKed, so the pin bump does not close it, and it is a data-loss fix under the pivot rule. Related, P2: a
Pong ACKs a put received before the Ping; P3: a reliable reply created after a put ACKs it. The Odin writer-thread
cut (C5) adds `acknowledgement_withheld` to the document server; the transport fix belongs in cultnet-rs.
- The "CultNet over QUIC" campaign is new, so it opens as a typed Eureka campaign in the Huginn-backed session, not
as a prose map here.
Status: cut map, Imagination pass 3, 2026-09-30. Pass 3 maps the four rulings below, re-maps Cuts 1c and
4, and adds Cut F (FORWARD-TSN), Cuts 6a-6c (receipts), Cut 7 (expiry, if Q-A8), Cut K (Kotlin), Cut 2c (the
C# fragment bound) and Cut D (a failed send to one peer stays that peer's loss). The text of Cuts 1, 1b, 2
@ -730,7 +753,7 @@ The ack cuts build on these behaviours and do not alter them:
| Rust hub `CultNetRudpServerHub` | `poll_resends` returns on the first failure (`rudp.rs:1762-1777`); the receive path's sends to one peer abort `receive_event_once` (`:1644`, `:1673`, `:1708`, `:1711`, `:1714`, `:1759`, through `send_packet` `:1799-1804`) | Cut D |
| C# `CultNetRudpSocketTransportServer` (and `RudpCultNetSchemaServer` over it) | `PollResends` walks every peer and the first `SocketException` from `SendTo` (`CultNetTransport.cs:2829-2834`) escapes the loop (`:2796-2805`); `TryReceiveOnce`'s sends are unguarded except the refusal goodbye (`:2687`, `:2704`, `:2749-2750`, `:2788`; guarded `:2739-2745`) | Cut D |
| cultmesh-py server | worse: an `OSError` from `sendto` (`server.py:302-307`) in `_poll_rudp_resends` (`:277-285`) or the receive path escapes `_rudp_loop` (`:203-…`, which catches only `recvfrom`'s errors), and the RUDP thread dies for every peer | Cut D |
| cultmesh-ts document server | none: Node's `socket.send` is asynchronous and reports failures on the socket's `error` event (`cultmesh-ts/src/index.ts:5858`), which `reportError` logs (`:5778-5785`) | none |
| cultmesh-ts document server | none. **Corrected 2026-09-30:** Node does not report send failures on the socket's `error` event. With a callback they go to the callback; without one, libuv drops them silently (Node 24). Only port 0 throws synchronously. Cut D routes every send through `sendRudpDatagram`. | none |
| Kotlin interop server | none that matters: a catch-all logs and continues (K:4025), and it is interop-only | none (Cut K1 reviews it) |
@ -1048,8 +1071,8 @@ Design in §10. Independent of every other cut; it touches send sites, not sessi
(`:2739-2745`) becomes redundant and goes;
- cultmesh-py `server.py`: `_send_rudp_packet` (`:302-307`) catches `OSError` and counts it.
- **Not changed:** single-peer socket transports (a failed send already concerns only their one peer, and
their callers, flush among them, get the error); `cultmesh-ts`'s document server (Node reports send
failures on the socket's `error` event, `index.ts:5858`).
their callers, flush among them, get the error); `cultmesh-ts`'s document server. (Corrected 2026-09-30: Node reports send failures only to a send
callback, never on the socket's `error` event; see the table above.)
- **Authority map:**
- Owner: each peer's session owns its peer's fate (idle timeout, lifetime, refusal, Disconnect). A send
failure is an input to nothing but a counter.
@ -1549,3 +1572,66 @@ Each question stands alone.
`packetsDropped` until K1. Record the stats shape in the parity doc once both land.
- **The P4 probe at the old pins** predates its fix; rerun `09318d13`'s P4 against a pinned sender if
old-sender evidence is needed.
**Follow-up (media FEC Cut 3 Hands, 2026-09-30):** `channel_send_options` in `rudp.rs` has no test for the
`"schema"` and `"latest"` channel arms. Deleting either falls to the default and every test stays green. Pin their send
flags on the wire the way the media arm is now pinned (raw peer socket), in whichever cut next touches channel
profiles.
**Rulings after the Cut 1/1b/1c Soul pass (Self, 2026-09-30):**
- **Stale data after a reconnect.** A reliable sequence at or before the generation's seeded `R` (serial arithmetic
within the receive window) is a duplicate: acknowledged, not delivered. This is what §6/§8 already promised.
- **Stale Connect (RFC 5961's challenge ACK).** A Connect whose sequence precedes the current generation's Connect
within the receive window is stale: it is answered with an Ack naming the current generation and does not reset.
A client whose Connect gets no Accept before its connect attempt times out starts a fresh attempt with a newly
drawn initial sequence, so a restarted client whose random sequence lands in that window still connects.
- **Server mode.** A Connect from a different endpoint is never a repeat; the session predicate stays sequence-only.
(Hands' deviation 5 was rejected: pinned clients restarting on a new port were locked out.)
- **The random default belongs to the session.** A session draws its own initial sequence when none is set, so one
options object reused across sessions or reconnects never repeats a sequence.
- **Connect attempts (Self, 2026-09-30, after the Cut 1c fix-batch Soul pass).** While a Connect awaits its Accept, only an
Accept the client honours retires it; an Ack never does (an Ack naming the pending Connect had wedged the client for
good). A fresh attempt after the connect-attempt timeout uses exactly the abandoned sequence + (receive window − 1),
not a random draw: that keeps a late copy of the abandoned Connect inside the new one's stale window while escaping the
server's stale window (unless the server sits exactly there, in which case the next attempt escapes). Only a client's
first Connect draws randomly.
- **Recorded divergence (not this campaign's to fix yet).** Only Rust has a receiver receive-ahead window (1,024). TS,
C# and Python acknowledge a far-ahead reliable frame by name and hold it behind an ordered gap indefinitely, so a
stranded ordered channel is silent loss there and "still owed" in Rust. Map a receive window for the three runtimes.
- **Residuals after the final Cut 1c Soul pass (recorded 2026-09-30, low, not fixed):**
- A late copy of a Connect from two or more attempts back (reordered > ~3 s) still restarts the server; the +window−1
rule keeps only the immediately previous attempt stale. Rust then times out and reconnects; TS/C#/Python hit the
receive-window divergence above (silent loss behind the gap).
- A client pinning `initial_sequence` that restarts while its old data is still in flight after the restarted Connect
can be matched to the old session's resent Accept (byte-identical; the client cannot tell). Pinning forfeits restart
detection; the random default closes it. Document on the option.
- Repeated failed attempts walk the sequence up 4095 per 3 s and clamp at `u32::MAX−1` after ~18-27 days on one session;
make exhaustion a hard error the caller sees (the clamp removal mutant survives).
- **Cuts 3 and D Soul pass (Self, 2026-09-30).** Cut 3: the 4 MiB bound counts every byte sent above the lowest
unacknowledged sequence, acked or not (the spec's wording; the first implementation counted only pending packets).
Cut D: a send failure is either permanent (the datagram can never be sent as built, e.g. `EMSGSIZE`) or transient;
a permanent failure is returned from a caller-directed send and, inside a poll, ends only that peer's session with a
typed reason; transient failures stay counted losses. Windows `ConnectionResetError` on receive is idle, never fatal,
in every server. Cut D's negative grep `send_packet(...)?;` is obsolete: the `?` now propagates encode errors only.
- **Cut 3 merged at `1210fdc7` (2026-09-30).**
- Batch 2 fixed Rust `send`, which checked the window before purging expired packets. It also added a
new-generation test in all four runtimes.
- Soul pass on batch 2: nothing found. 18 mutants were killed. Interop was 27/27, run in `ack1d-interop:2`,
whose Linux wrappers let the Windows-only Kotlin build run.
- Residual: `packages/cultmesh-rs/Cargo.lock` is stale.
- Also recorded: `packages/cultmesh-kotlin/build.ps1` is Windows-only, so Kotlin interop runs off Windows only
through an image with wrappers.
- P2 (the expiry hole) stays with Cut F.
- **Cut D batch 2, Soul pass 2: fix first.**
- cultnet-ts emits transient send failures as `"error"`, which crashes processes with no listener. Section 10's
claim that "Node reports send failures on the socket's error event" is false on Node 24 without a callback:
they are dropped silently.
- C# stale peer objects write into the replacing session.
- cultmesh-py dies on a non-ValueError per-peer failure.
- Six guards are unpinned.
- Batch 3 is in Hands.
- **K1 gains:** Kotlin's idle resend loop (`CultMesh.kt:4033-4036`) is one `try`, so one peer's permanent failure
stops resends for every later peer. That is in addition to a refused packet not ending that peer's session.
- **Recorded (liveness, not fixed):** a lowest reliable packet lost forever stalls the sender 1,023 sequences ahead
while the peer stays alive; session timeout doesn't end it. TCP ends such a connection after a retransmission
limit (R2). Decide with Cut F (abandonment) whether a reliable packet outstanding past a bound ends the session.

View file

@ -271,6 +271,15 @@ Current progress:
flush where the runtime supports one, so callers that require confirmed
transport delivery do not substitute a fixed sleep. TCP-framed profiles do
not publish the field because they do not own the RUDP pending-reliable queue.
- The receive window is a contract, not a discovery. A receiver must hold 1,023
sequences and 4 MiB of payload beyond its contiguous received watermark, and
a conforming sender never exceeds either. The sender's flow window enforces
it without a wire field: a reliable packet is admitted only while its
sequence is at most 1,023 above the lowest unacked sequence and the payload
above that sequence stays within 4 MiB. The first packet is always
admissible, so a payload larger than 4 MiB goes out alone, and packets wait
in the FIFO in order, so a small packet never overtakes a large one. C#,
Rust, TypeScript, and Python enforce it; Kotlin does not yet.
- TypeScript, C#, Rust, Python, and Kotlin now share a portable
`cultnet.reconnect_policy.v0` document and deterministic exponential delay
helper, and RUDP transport profiles advertise that policy under

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,272 @@
# Element-id sweep list (C2a)
Data for the per-consumer sweep in docs/document-variants-cut.md, C2. Every object type below sits in a list (`List<T>`, `T[]`, `IEnumerable<T>`, and the like) reachable from a registered document type, and needs one string member with `[Key(n)]` and `[CultElementId]` before that consumer can take a CultLib that requires ids. A union base takes the member on the base; its subclasses inherit it, so put the slot above the subclasses' highest key.
Method: a static scan of `F:\Projects\<consumer>` C# files for types marked `[CultDocument(`, walked through keyed members, base classes, dictionary values and `[Union]` subtypes. Read-only; no consumer was edited. Worktree copies (`Aetheria-catalog`, `CultLib-*`, and so on) are excluded. Struct elements are values and need no id. Non-C# consumers carry the id as an ordinary member and are not listed. The scan is text-based: a document registered without a literal `[CultDocument(` attribute, or a type whose members lack an integer `[Key]`, is missed, so the sweep pass should confirm each repo by registering its types once.
Lines: `Type file:line (via Owner.Member)`, paths relative to the consumer root.
## Aetheria (13 element types)
- BehaviorData Assets/Scripts/ServerShared/Behaviors/Behaviors.cs:190 (via ConsumableItemData.Behaviors)
- EntityPack Assets/Scripts/ServerShared/EntitySerializer.cs:171 (via ZonePack.Entities)
- ProductRole Assets/Scripts/ServerShared/FactionProduct.cs:38 (via FactionProductData.Roles)
- InputLayoutRow Assets/Scripts/ServerShared/InputLayout.cs:32 (via InputLayout.Rows)
- InputLayoutColumn Assets/Scripts/ServerShared/InputLayout.cs:52 (via InputLayoutKeyRow.Columns)
- ItemRole Assets/Scripts/ServerShared/ItemData.cs:325 (via CraftedItemData.Roles)
- AudioStat Assets/Scripts/ServerShared/ItemData.cs:441 (via EquippableItemData.AudioStats)
- HardpointData Assets/Scripts/ServerShared/ItemData.cs:553 (via HullData.Hardpoints)
- StatTerm Assets/Scripts/ServerShared/ItemData.cs:630 (via PerformanceStat.Terms)
- LoadoutSlot Assets/Scripts/ServerShared/Loadout.cs:23 (via Loadout.Slots)
- RoleFill Assets/Scripts/ServerShared/Provenance.cs:84 (via Lot.Roles)
- SavedActionBarBinding Assets/Scripts/ServerShared/SavedGame.cs:168 (via SavedGame.ActionBarBindings)
- Asteroid Assets/Scripts/ServerShared/ZoneData.cs:133 (via AsteroidBeltData.Asteroids)
## AetheriaEve (115 element types)
- AetheriaCorporationAllegiance Aetheria.State/Documents/AetheriaCorporation.cs:67 (via AetheriaCorporation.Allegiances)
- AetheriaShapeCell Aetheria.State/Documents/AetheriaItemDefinition.cs:178 (via AetheriaItemDefinition.ShapeCells)
- AetheriaItemHardpoint Aetheria.State/Documents/AetheriaItemDefinition.cs:188 (via AetheriaItemDefinition.Hardpoints)
- AetheriaCurveKey Aetheria.State/Documents/AetheriaItemDefinition.cs:222 (via AetheriaItemDefinition.ThermalPerformanceCurveKeys)
- AetheriaItemAudioStat Aetheria.State/Documents/AetheriaItemDefinition.cs:238 (via AetheriaItemDefinition.AudioStats)
- AetheriaBehaviorPayload Aetheria.State/Documents/AetheriaItemDefinition.cs:267 (via AetheriaItemDefinition.BehaviorPayloads)
- AetheriaBehaviorField Aetheria.State/Documents/AetheriaItemDefinition.cs:286 (via AetheriaBehaviorPayload.Fields)
- AetheriaBehaviorMapEntry Aetheria.State/Documents/AetheriaItemDefinition.cs:296 (via AetheriaBehaviorValue.MapEntries)
- AetheriaBehaviorValue Aetheria.State/Documents/AetheriaItemDefinition.cs:306 (via AetheriaBehaviorValue.Children)
- AetheriaLegacyCatalogFile Aetheria.State/Documents/AetheriaLegacyCatalogQuarantine.cs:41 (via AetheriaLegacyCatalogQuarantine.NameFiles)
- AetheriaMigrationCount Aetheria.State/Documents/AetheriaMigrationLedger.cs:35 (via AetheriaMigrationLedger.Counts)
- AetheriaLegacyCatalogEntrySummary Aetheria.State/Documents/AetheriaMigrationLedger.cs:45 (via AetheriaMigrationLedger.LegacyCatalogEntries)
- AetheriaStoryFileHash Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:42 (via AetheriaPlayerSettings.StoryFileHashes)
- AetheriaInputBindingOverride Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:85 (via AetheriaPlayerInputSettings.BindingOverrides)
- AetheriaEntityLoadout Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:119 (via AetheriaEntityLoadout.Children)
- AetheriaLoadoutItemSlot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:186 (via AetheriaEntityLoadout.Equipment)
- AetheriaCargoBayLoadout Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:199 (via AetheriaEntityLoadout.CargoContents)
- AetheriaFactionZoneState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:268 (via AetheriaRunState.HomeZones)
- AetheriaAgentTaskState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:275 (via AetheriaRunState.AgentTasks)
- AetheriaDroppedPickupSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:346 (via AetheriaZoneState.DroppedPickups)
- AetheriaOrbitSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:362 (via AetheriaZoneState.Orbits)
- AetheriaBodySnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:384 (via AetheriaZoneState.Bodies)
- AetheriaBodyResource Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:449 (via AetheriaBodySnapshot.Resources)
- AetheriaAsteroidSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:459 (via AetheriaBodySnapshot.Asteroids)
- AetheriaAsteroidMiningAccumulatorSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:484 (via AetheriaAsteroidSnapshot.MiningAccumulators)
- AetheriaColor Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:545 (via AetheriaGasGiantVisualState.Colors)
- AetheriaFogFieldEmitterSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:691 (via AetheriaEntitySnapshot.FogFieldEmitters)
- AetheriaLoadoutGenerationSelection Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:723 (via AetheriaLoadoutGenerationReceipt.Selections)
- AetheriaEntityContactSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:734 (via AetheriaEntitySnapshot.Contacts)
- AetheriaEntityItemSlot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:751 (via AetheriaEntitySnapshot.Equipment)
- AetheriaWeaponGroupSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:782 (via AetheriaEntitySnapshot.WeaponGroups)
- AetheriaEntityStatGrid Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:789 (via AetheriaEntitySnapshot.StatGrids)
- AetheriaActiveConsumableSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:805 (via AetheriaEntitySnapshot.ActiveConsumables)
- AetheriaConsumableBehaviorStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:828 (via AetheriaActiveConsumableSnapshot.BehaviorStates)
- AetheriaBehaviorProgressSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:844 (via AetheriaEntitySnapshot.BehaviorProgress)
- AetheriaWeaponStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:863 (via AetheriaEntitySnapshot.WeaponStates)
- AetheriaBehaviorStateSnapshot Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:924 (via AetheriaEntitySnapshot.BehaviorStates)
- AetheriaFactionRelationshipState Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs:1093 (via AetheriaRunState.FactionRelationships)
- AetheriaItemRarityTier Aetheria.State/Documents/AetheriaTradeValuePolicy.cs:29 (via AetheriaTradeValuePolicy.Tiers)
- AetheriaRuntimeAgentTaskCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs:35 (via AetheriaRuntimeRunCheckpointCommit.AgentTasks)
- AetheriaRuntimeAssetManifestEntry Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssetDocuments.cs:102 (via AetheriaRuntimeAssetManifestDocument.Assets)
- AetheriaRuntimeCurveKey Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:446 (via AetheriaRuntimeShotReceiptCommit.GuidanceCurve)
- AetheriaRuntimeCorporation Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:620 (via AetheriaRuntimeCatalogSnapshot.Corporations)
- AetheriaRuntimeNameFile Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:669 (via AetheriaRuntimeCatalogSnapshot.NameFiles)
- AetheriaRuntimeLoadoutTemplateSnapshot Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs:796 (via AetheriaRuntimeLoadoutTemplatesDocument.Templates)
- AetheriaRuntimeDaemonCommandBoundaryEntry Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs:462 (via AetheriaRuntimeDaemonCommandBoundaryDocument.Commands)
- AetheriaRuntimeDaemonSoaIdentityDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:143 (via AetheriaRuntimeDaemonSoaViewDocument.Identities)
- AetheriaRuntimeDaemonRenderGroupDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:156 (via AetheriaRuntimeDaemonSoaViewDocument.RenderGroups)
- AetheriaRuntimeDaemonSoaBufferDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:222 (via AetheriaRuntimeDaemonSoaViewDocument.Buffers)
- AetheriaRuntimeDaemonSoaColumnDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:253 (via AetheriaRuntimeDaemonSoaViewDocument.Columns)
- AetheriaRuntimeDaemonSoaDirtyRangeDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaDocuments.cs:284 (via AetheriaRuntimeDaemonSoaViewDocument.DirtyRanges)
- AetheriaRuntimeEveSurfaceAdvertisement Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs:11 (via AetheriaRuntimeDaemonProviderAdvertisementDocument.EveSurfaces)
- AetheriaRuntimeEvePluginRequirement Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEveSurfaceCatalog.cs:58 (via AetheriaRuntimeEveSurfaceAdvertisement.RequiresPlugins)
- AetheriaRuntimeZoneTargetRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:410 (via AetheriaRuntimeZoneContactsDocument.Targets)
- AetheriaRuntimeZoneContactRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:441 (via AetheriaRuntimeZoneContactsDocument.Contacts)
- AetheriaRuntimeStationStockItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:551 (via AetheriaRuntimeStationRefitEntityOption.CargoItems)
- AetheriaRuntimeStationRefitEntityOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:585 (via AetheriaRuntimeStationRefitDocument.AvailableEntities)
- AetheriaRuntimeStationDockingBayRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:620 (via AetheriaRuntimeStationRefitDocument.DockingBays)
- AetheriaRuntimeStationLoadoutRestoreOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:655 (via AetheriaRuntimeStationRefitDocument.LoadoutRestoreOptions)
- AetheriaRuntimeStationCargoTargetRow Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:674 (via AetheriaRuntimeStationRefitDocument.CargoTargets)
- AetheriaRuntimeSectorMapZone Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:765 (via AetheriaRuntimeSectorMapDocument.Zones)
- AetheriaRuntimeSectorMapLink Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:802 (via AetheriaRuntimeSectorMapDocument.Links)
- AetheriaRuntimeZoneRenderAdjacentZone Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1197 (via AetheriaRuntimeZoneRenderDocument.AdjacentZones)
- AetheriaRuntimeZoneRenderWormholeExit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1210 (via AetheriaRuntimeZoneRenderDocument.WormholeExits)
- AetheriaRuntimeZoneRenderBodyPose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1229 (via AetheriaRuntimeZoneRenderDocument.BodyPoses)
- AetheriaRuntimeZoneRenderAsteroidBeltPose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1260 (via AetheriaRuntimeZoneRenderDocument.AsteroidBeltPoses)
- AetheriaRuntimeZoneRenderAsteroidInstancePose Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1286 (via AetheriaRuntimeZoneRenderAsteroidBeltPose.InstancePoses)
- AetheriaRuntimeViewportObject Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1382 (via AetheriaRuntimeGameViewportDocument.Objects)
- AetheriaRuntimeInventoryItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1458 (via AetheriaRuntimeViewportObject.Inventory)
- AetheriaRuntimeGravityInfluence Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1493 (via AetheriaRuntimeGameViewportDocument.GravityInfluences)
- AetheriaRuntimeBodyView Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs:1530 (via AetheriaRuntimeGameViewportDocument.Bodies)
- AetheriaHangarShip Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:119 (via AetheriaHangarState.Ships)
- AetheriaHangarItemStack Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:129 (via AetheriaHangarState.Inventory)
- AetheriaHangarCurrency Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:136 (via AetheriaHangarState.Currencies)
- AetheriaDeploymentReceipt Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeHangarDocuments.cs:155 (via AetheriaHangarState.Deployments)
- AetheriaProgressionVerseOption Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeProgressionSourceDocuments.cs:45 (via AetheriaProgressionSourceDocument.AvailableVerses)
- AetheriaRuntimeStoryFileHashDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSettingsDocuments.cs:87 (via AetheriaRuntimePlayerSettingsDocument.StoryFileHashes)
- AetheriaRuntimeInputBindingOverrideDocument Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSettingsDocuments.cs:108 (via AetheriaRuntimePlayerSettingsDocument.BindingOverrides)
- AetheriaRuntimeEntityLoadoutCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:78 (via AetheriaRuntimeEntityLoadoutCommit.Children)
- AetheriaRuntimeLoadoutItemSlotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:143 (via AetheriaRuntimeEntitySnapshotCommit.Equipment)
- AetheriaRuntimeCargoBayLoadoutCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:159 (via AetheriaRuntimeEntitySnapshotCommit.CargoContents)
- AetheriaRuntimeGameEventCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:490 (via AetheriaRuntimeRunCheckpointCommit.GameEvents)
- AetheriaRuntimeFactionZoneCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:509 (via AetheriaRuntimeRunCheckpointCommit.HomeZones)
- AetheriaRuntimeShotReceiptCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:539 (via AetheriaRuntimeRunCheckpointCommit.ShotReceipts)
- AetheriaRuntimePickupContactReceiptCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:591 (via AetheriaRuntimeRunCheckpointCommit.PickupContactReceipts)
- AetheriaRuntimeDamageCellCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:603 (via AetheriaRuntimeShotReceiptCommit.DamageCells)
- AetheriaRuntimeCorporationSurveyCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:614 (via AetheriaRuntimeRunCheckpointCommit.CorporationSurveys)
- AetheriaRuntimeFactionRelationshipCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:623 (via AetheriaRuntimeRunCheckpointCommit.FactionRelationships)
- AetheriaRuntimeZoneSnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:636 (via AetheriaRuntimeRunCheckpointCommit.Zones)
- AetheriaRuntimePhysicalPayloadCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:699 (via AetheriaRuntimeZoneSnapshotCommit.PhysicalPayloads)
- AetheriaRuntimeDroppedPickupCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:771 (via AetheriaRuntimeZoneSnapshotCommit.DroppedPickups)
- AetheriaRuntimeOrbitSnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:805 (via AetheriaRuntimeZoneSnapshotCommit.Orbits)
- AetheriaRuntimeBodySnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:830 (via AetheriaRuntimeZoneSnapshotCommit.Bodies)
- AetheriaRuntimeBodyResourceCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:901 (via AetheriaRuntimeBodySnapshotCommit.Resources)
- AetheriaRuntimeAsteroidCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:911 (via AetheriaRuntimeBodySnapshotCommit.Asteroids)
- AetheriaRuntimeAsteroidMiningAccumulatorCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:936 (via AetheriaRuntimeAsteroidCommit.MiningAccumulators)
- AetheriaRuntimeColorCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1008 (via AetheriaRuntimeGasGiantVisualCommit.Colors)
- AetheriaRuntimeEntitySnapshotCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1024 (via AetheriaRuntimeZoneSnapshotCommit.Entities)
- AetheriaRuntimeFogFieldEmitterCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1198 (via AetheriaRuntimeEntitySnapshotCommit.FogFieldEmitters)
- AetheriaRuntimeEquipmentStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1240 (via AetheriaRuntimeEntitySnapshotCommit.EquipmentStates)
- AetheriaRuntimeLoadoutGenerationSelectionCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1264 (via AetheriaRuntimeLoadoutGenerationReceiptCommit.Selections)
- AetheriaRuntimeEntityContactCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1275 (via AetheriaRuntimeEntitySnapshotCommit.Contacts)
- AetheriaRuntimeEntityStatGridCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1291 (via AetheriaRuntimeEntitySnapshotCommit.StatGrids)
- AetheriaRuntimeActiveConsumableCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1307 (via AetheriaRuntimeEntitySnapshotCommit.ActiveConsumables)
- AetheriaRuntimeConsumableBehaviorStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1330 (via AetheriaRuntimeActiveConsumableCommit.BehaviorStates)
- AetheriaRuntimeBehaviorProgressCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1355 (via AetheriaRuntimeEntitySnapshotCommit.BehaviorProgress)
- AetheriaRuntimeWeaponStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1374 (via AetheriaRuntimeEntitySnapshotCommit.WeaponStates)
- AetheriaRuntimeBehaviorStateCommit Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs:1444 (via AetheriaRuntimeEntitySnapshotCommit.BehaviorStates)
- AetheriaRuntimeStarbridgeStationStockItem Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:71 (via AetheriaRuntimeStarbridgeScenarioDocument.StationStock)
- AetheriaRuntimeStarbridgeWaveDefinition Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:83 (via AetheriaRuntimeStarbridgeScenarioDocument.Waves)
- AetheriaRuntimeStarbridgeWaveForecast Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:94 (via AetheriaRuntimeStarbridgeSessionSummaryDocument.WaveForecast)
- AetheriaRuntimeStarbridgeRuntimeRole Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStarbridgeDocuments.cs:104 (via AetheriaRuntimeStarbridgeScenarioDocument.RuntimeRoles)
- AetheriaRuntimeAuthorityRuntimeRole Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:194 (via AetheriaRuntimeVerseAuthorityPolicyDocument.RuntimeRoles)
- AetheriaRuntimeAuthorityRule Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:201 (via AetheriaRuntimeVerseAuthorityPolicyDocument.Rules)
- AetheriaRuntimeArenaSeat Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseAuthorityPolicy.cs:255 (via AetheriaRuntimeArenaRosterDocument.Seats)
## AquaSynth (0 element types)
None.
## Aquarium (1 element types)
- AquariumFaceVoiceEndpointState src/Aquarium.Epiphany/State/AquariumLiveState.cs:61 (via AquariumLiveState.FaceVoiceEndpoints)
## Brokkr (4 element types)
- BrokkrGameObjectSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:42 (via BrokkrHostSnapshot.sceneObjects)
- BrokkrComponentSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:62 (via BrokkrGameObjectSnapshot.components)
- BrokkrSerializedPropertySnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:73 (via BrokkrComponentSnapshot.properties)
- BrokkrAssetSnapshot surfaces/unity/Packages/com.gamecult.brokkr/Runtime/BrokkrHostSnapshot.cs:84 (via BrokkrHostSnapshot.assets)
## Delvehold (0 element types)
None.
## Eve (17 element types)
- EveAssetCatalogEntry packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:52 (via EveAssetCatalogDocument.Assets)
- EveAssetVariant packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:89 (via EveAssetCatalogEntry.Variants)
- EveEntitySoaBuffer packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:33 (via EveEntitySoaViewDocument.Buffers)
- EveEntitySoaColumn packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:41 (via EveEntitySoaViewDocument.Columns)
- EveEntitySoaDirtyRange packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:55 (via EveEntitySoaViewDocument.DirtyRanges)
- EveEntityIdentity packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:64 (via EveEntitySoaViewDocument.Identities)
- EveEntityRenderGroup packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:77 (via EveEntitySoaViewDocument.RenderGroups)
- EveInputActionDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:22 (via EveInputCapabilityDocument.Actions)
- EveInputProfileDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:51 (via EveInputCapabilityDocument.DefaultProfiles)
- EveInputBindingDocument packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:59 (via EveInputProfileDocument.Bindings)
- EveProviderWitness packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:249 (via EveProviderAdvertisementDocument.Witnesses)
- EveAdvertisedSurface packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:265 (via EveProviderAdvertisementDocument.Surfaces)
- EveAdvertisedCommand packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:333 (via EveProviderAdvertisementDocument.Commands)
- EveSurfaceComponent packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:106 (via EveSurfaceComponent.Children)
- EveEmbeddedDocumentSlot packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:230 (via EveSurfaceComponent.EmbeddedDocuments)
- EveStyleToken packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:277 (via EveSurfaceTree.Styles)
- EveCommandTemplate packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:295 (via EveSurfaceDocument.Commands)
## EvePlugins (1 element types)
- EveFieldsSplatLayer plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields/Runtime/EveFieldsDocuments.cs:38 (via EveFieldsSplatsDocument.Layers)
## Ghostlight (17 element types)
These types are a vendored copy of Eve (`vendor/eve`); they change when Eve does and the pin bumps.
- EveAssetCatalogEntry vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:52 (via EveAssetCatalogDocument.Assets)
- EveAssetVariant vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs:89 (via EveAssetCatalogEntry.Variants)
- EveEntitySoaBuffer vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:33 (via EveEntitySoaViewDocument.Buffers)
- EveEntitySoaColumn vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:41 (via EveEntitySoaViewDocument.Columns)
- EveEntitySoaDirtyRange vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:55 (via EveEntitySoaViewDocument.DirtyRanges)
- EveEntityIdentity vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:64 (via EveEntitySoaViewDocument.Identities)
- EveEntityRenderGroup vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs:77 (via EveEntitySoaViewDocument.RenderGroups)
- EveInputActionDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:22 (via EveInputCapabilityDocument.Actions)
- EveInputProfileDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:51 (via EveInputCapabilityDocument.DefaultProfiles)
- EveInputBindingDocument vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs:59 (via EveInputProfileDocument.Bindings)
- EveProviderWitness vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:249 (via EveProviderAdvertisementDocument.Witnesses)
- EveAdvertisedSurface vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:265 (via EveProviderAdvertisementDocument.Surfaces)
- EveAdvertisedCommand vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs:333 (via EveProviderAdvertisementDocument.Commands)
- EveSurfaceComponent vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:106 (via EveSurfaceComponent.Children)
- EveEmbeddedDocumentSlot vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:230 (via EveSurfaceComponent.EmbeddedDocuments)
- EveStyleToken vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:277 (via EveSurfaceTree.Styles)
- EveCommandTemplate vendor/eve/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs:295 (via EveSurfaceDocument.Commands)
## Gjallar (3 element types)
- GjallarAdvertisedSchema src/Gjallar/VerseState.cs:462 (via GjallarProviderAdvertisementRecord.Schemas)
- GjallarAdvertisedWitness src/Gjallar/VerseState.cs:470 (via GjallarProviderAdvertisementRecord.Witnesses)
- GjallarAdvertisedSurface src/Gjallar/VerseState.cs:480 (via GjallarProviderAdvertisementRecord.Surfaces)
## Heimdall (12 element types)
These types are a vendored copy of CultLib (`vendor/CultLib`); C2a already adds the ids in CultLib, so they resolve when the pin bumps. The witness bundle types are the fork reported with C2a.
- CultGeometryDomainNode vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:61 (via CultGeometryDomainNode.Children)
- CultGeometryFeatureClaim vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:106 (via CultGeometryDomainNode.Claims)
- CultGeometryContributionRow vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:262 (via CultGeometrySelectedCutManifest.Diagnostics)
- CultMeshBodyDescriptor vendor/CultLib/src/GameCult.Mesh/CultMeshBodies.cs:35 (via CultMeshBodyPublicationDocument.Representations)
- CultMeshCdnChunkRef vendor/CultLib/src/GameCult.Mesh/CultMeshCdn.cs:73 (via CultMeshCdnArtifactManifest.Chunks)
- CultMeshDiscoveryCandidateDocument vendor/CultLib/src/GameCult.Mesh/CultMeshDiscoveryService.cs:418 (via CultMeshDiscoveryStateDocument.Candidates)
- CultMeshEntityPrefabComponent vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:81 (via CultMeshEntityPrefabNode.Components)
- CultMeshEntityPrefabAssetRef vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:96 (via CultMeshEntityPrefabPackage.Assets)
- CultMeshEntityPrefabNode vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:127 (via CultMeshEntityPrefabPackage.Nodes)
- CultWitnessContractPin vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:105 (via CultWitnessArtifactBundle.Contracts)
- CultWitnessArtifactEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:136 (via CultWitnessArtifactBundle.Artifacts)
- CultWitnessTimingEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:179 (via CultWitnessArtifactBundle.TimingWitnesses)
## Mimir (0 element types)
None.
## StreamPixels (20 element types)
`CultGeometryDomainNode` is the vendored CultLib copy; C2a already adds its id.
- ActivationCondition apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ActivationCondition.cs:18 (via CharacterConditions.Conditions)
- BattleEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/BattleEffect.cs:8 (via BattleAction.Effects)
- GearStatEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/GearStatEffect.cs:7 (via GearData.StatEffects)
- OverlaySceneIntent apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ServiceLink/OverlayFrameDocuments.cs:33 (via OverlaySceneIntentWindow.Intents)
- PlayedIntentRecord apps/overlay-unity/Assets/Scripts/Assembly-CSharp/ServiceLink/PlayedIntentLedger.cs:25 (via OverlayPlayedIntents.Entries)
- StatModifier apps/overlay-unity/Assets/Scripts/Assembly-CSharp/StatModifier.cs:7 (via GearStatEffect.Modifiers)
- StatusEffect apps/overlay-unity/Assets/Scripts/Assembly-CSharp/StatusEffect.cs:10 (via StatusData.Effects)
- WeaponDamageTypeMultiplier apps/overlay-unity/Assets/Scripts/Assembly-CSharp/WeaponDamageTypeMultiplier.cs:7 (via WeaponData.DamageMultipliers)
- CultGeometryDomainNode vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:61 (via CultGeometryDomainNode.Children)
- CultGeometryFeatureClaim vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:106 (via CultGeometryDomainNode.Claims)
- CultGeometryContributionRow vendor/CultLib/src/GameCult.Geometry/CultGeometryDocuments.cs:262 (via CultGeometrySelectedCutManifest.Diagnostics)
- CultMeshBodyDescriptor vendor/CultLib/src/GameCult.Mesh/CultMeshBodies.cs:35 (via CultMeshBodyPublicationDocument.Representations)
- CultMeshCdnChunkRef vendor/CultLib/src/GameCult.Mesh/CultMeshCdn.cs:73 (via CultMeshCdnArtifactManifest.Chunks)
- CultMeshDiscoveryCandidateDocument vendor/CultLib/src/GameCult.Mesh/CultMeshDiscoveryService.cs:418 (via CultMeshDiscoveryStateDocument.Candidates)
- CultMeshEntityPrefabComponent vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:81 (via CultMeshEntityPrefabNode.Components)
- CultMeshEntityPrefabAssetRef vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:96 (via CultMeshEntityPrefabPackage.Assets)
- CultMeshEntityPrefabNode vendor/CultLib/src/GameCult.Mesh/CultMeshEntityPrefabs.cs:127 (via CultMeshEntityPrefabPackage.Nodes)
- CultWitnessContractPin vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:105 (via CultWitnessArtifactBundle.Contracts)
- CultWitnessArtifactEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:136 (via CultWitnessArtifactBundle.Artifacts)
- CultWitnessTimingEntry vendor/CultLib/src/GameCult.Networking/CultWitnessArtifactBundle.cs:179 (via CultWitnessArtifactBundle.TimingWitnesses)
## Ymir (1 element types)
- YmirTransportLoweringDocument src/Ymir.Core/YmirServicePublicationDocument.cs:63 (via YmirProviderAdvertisementDocument.CommandLowerings)
## CultLib itself
C2a adds ids to: `CultGeometryDomainNode`, `CultGeometryFeatureClaim`, `CultGeometryContributionRow` (`src/GameCult.Geometry/CultGeometryDocuments.cs`); `CultMeshBodyDescriptor` (`src/GameCult.Mesh/CultMeshBodies.cs:35`); `CultMeshDiscoveryCandidateDocument` (`CultMeshDiscoveryService.cs:418`); `CultMeshEntityPrefabAssetRef`, `CultMeshEntityPrefabComponent`, `CultMeshEntityPrefabNode` (`CultMeshEntityPrefabs.cs`); `CultWitnessContractPin`, `CultWitnessArtifactEntry`, `CultWitnessTimingEntry` (`src/GameCult.Networking/CultWitnessArtifactBundle.cs`).
`CultMeshCdnChunkRef` (`src/GameCult.Mesh/CultMeshCdn.cs:73`) has a derived id, `[CultElementId(nameof(Offset))]`: the decimal text of its offset, never random, so packing the same body encodes the same manifest bytes. `contracts/cultmesh/content-vectors.*.json` and the Rust runtime carry it. A consumer with content-addressed elements declares the same way.

View file

@ -11,9 +11,12 @@ CultNet, and CultCache ownership boundaries.
powershell -ExecutionPolicy Bypass -File .\scripts\pack-nuget.ps1
```
The command creates a local feed under `artifacts/nuget`, verifies that every
internal dependency package exists at the same version, then restores and runs
a clean .NET consumer using only `PackageReference Include="GameCult.Mesh"`.
The command packs `GameCult.Mesh`, `GameCult.Networking.WebSockets`, and every
project they reach through `ProjectReference` (including `GameCult.Math`, which
carries its own version) into a local feed under `artifacts/nuget`. It verifies
that each of those packages exists at the identity MSBuild reports for it, then
restores and runs a clean .NET consumer using only
`PackageReference Include="GameCult.Mesh"`.
The Unity distribution remains `org.gamecult.cultlib`, built by
`scripts/build-unity-package.ps1`, because Unity consumes the managed assembly
@ -22,5 +25,5 @@ closure through UPM rather than NuGet restore.
Install the Unity package from the immutable release tag:
```text
https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.57
https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v2.0.0
```

View file

@ -0,0 +1,170 @@
# Content-addressed HTTP answers: what the specs and sources say
Fork served: **cultmesh-ts-content C2 provider HTTP answers** (session label
self-2026-10-19-cm-content). Eyes pass, 2026-10-19. Facts with evidence pointers
only; no recommendation. Line numbers refer to the raw files fetched on
2026-10-19 (master/main at that date), so they drift.
Source shorthands:
- **TG** = IPFS Trustless Gateway spec, https://specs.ipfs.tech/http-gateways/trustless-gateway/ (source: https://raw.githubusercontent.com/ipfs/specs/main/src/http-gateways/trustless-gateway.md)
- **PG** = IPFS Path Gateway spec, https://specs.ipfs.tech/http-gateways/path-gateway/ (source: https://raw.githubusercontent.com/ipfs/specs/main/src/http-gateways/path-gateway.md)
- **OCI** = OCI Distribution Spec, https://raw.githubusercontent.com/opencontainers/distribution-spec/main/spec.md
- **Fetch** = WHATWG Fetch Standard, https://fetch.spec.whatwg.org/
- **RFC 9110/9111/9112**, https://www.rfc-editor.org/rfc/rfc9110.txt (and 9111, 9112, 8246)
Two things the draft model page said "from memory" are checked at the end
(section 8).
## 1. IPFS Trustless Gateway (`application/vnd.ipld.raw`)
| Case | What the spec says | Pointer |
|---|---|---|
| Found | Body is "An opaque bytes matching the requested block CID"; "The Body hash MUST match the Multihash from the requested CID." | TG "Block Responses (application/vnd.ipld.raw)" (src l.341-346) |
| Unknown/absent hash | `404 Not Found` MUST be returned when the root block is not in the gateway's storage; applies to HEAD for any CID and GET of raw blocks. Body not specified. For non-recursive gateways it "definitively signals" the content is not in the dataset. | TG "Response Status Codes / 404 Not Found" (src l.274-283); "Recursion" (l.606-612) |
| Malformed request | TG defines no status for a malformed CID. It says servers SHOULD "validate CID format and encoding before processing requests". PG says `400 Bad Request` is a "generic client error", example "malformed CID". TG gives 400 for missing `Accept` in strict trustless mode (l.108) and for an out-of-range `entity-bytes` (l.210). | TG "Security Considerations / HTTP Servers" (src l.616-628); PG "Response Status Codes" |
| Stored object failing its digest | **Not specified for servers.** The only requirement is client-side: clients MUST verify block digest against the CID before processing or storing. TG `500` is "only for genuine server errors, not for content unavailability" (storage failure, resource exhaustion, unexpected internal errors); corruption is not named. | TG "HTTP Clients / Block Validation" (src l.629-631); "500 Internal Server Error" (l.294-296) |
| Range | TG defines no Range support for raw blocks. PG says Range is "for requesting specific byte range of UnixFS files and raw blocks"; gateways SHOULD support single-range, multi-range optional; success is `206` with `Content-Range`; `Accept-Ranges` is optional, `none` is the example for non-deterministic CAR order. | PG "Range (request header)" (src l.184-193), "Content-Range" (l.626), "Accept-Ranges" (l.632-640) |
| HEAD | `HEAD /ipfs/{cid}` is GET without payload; missing root block is 404 for HEAD too. PG: gateways SHOULD do minimal work to produce headers such as `Content-Length`. For the probe path, all HEAD headers are optional. | TG "HEAD /ipfs/{cid}" (src l.68-72), "404" (l.274-283), "Dedicated Probe Paths" (l.525-548); PG HEAD text |
| CORS | **TG defines none.** PG only says gateways serving cross-origin traffic SHOULD list `Ipfs-Uri` (and `X-Ipfs-Path`) in `Access-Control-Expose-Headers`. Kubo's config doc says its default `Gateway.HTTPHeaders` is `{}` "+ implicit CORS headers from `boxo/gateway#AddAccessControlHeaders` and ipfs/specs#423"; the values themselves are not in that file. | PG header sections; https://raw.githubusercontent.com/ipfs/kubo/master/docs/config.md l.1484-1488 |
| Cache-Control | Response value is in PG, not TG: `Cache-Control: public, max-age=29030400, immutable` "MUST be returned for every immutable resource under `/ipfs/` namespace". (Request-side `Cache-Control: only-if-cached` returns `412` when the root block is not immediately available.) | PG "Cache-Control (response header)" (src l.488-500); TG "Cache-Control: only-if-cached" (l.120-126) |
| ETag | TG: "MUST be returned and follow the recommendations in" PG. PG default is the requested CID in quotes: `Etag: "bafy…foo"`; format-specific variants add a suffix (`"bafy…foo.raw"`); non-deterministic streams use `W/`. PG: gateway MUST compare `If-None-Match` against its `Etag` (answer 304). TG: clients SHOULD NOT assume ETags are portable across gateways. | TG "Etag (response header)" (src l.324-338); PG l.130-136, l.453-486 |
| Content-Type | "MUST be returned"; for raw blocks the type is `application/vnd.ipld.raw` (IANA registered). No charset parameter is specified. | TG "Content-Type (response header)" (src l.300-308); IANA entry linked at l.343 |
| Other headers | `Content-Disposition` MUST be `attachment` with `filename="<cid>.bin"` for raw blocks, "to ensure requested bytes are not rendered by a web browser". PG: `X-Content-Type-Options: nosniff` should be returned with `application/vnd.ipld.raw` and `.car` (from the PG fetch summary; line not re-grepped). | TG "Content-Disposition" (src l.310-316); PG "X-Content-Type-Options" |
| Content-Encoding / Length | PG: `Content-Length` "Represents the length of returned HTTP payload", and warns "the value may differ from the real size of requested data if compression or chunked `Transfer-Encoding` are used", citing ipfs/specs#461 (an open TODO in the spec). | PG "Content-Length (response header)" (src l.613-624) |
| Size cap | Clients SHOULD cap block size; the spec names 2 MiB as the p2p block limit (see Block Limits). | TG "Block Limits" (src l.558-573) and "HTTP Clients" (l.629-640) |
## 2. OCI Distribution Spec, blob `GET`/`HEAD /v2/<name>/blobs/<digest>`
| Case | What the spec says | Pointer |
|---|---|---|
| Found (GET) | "MUST provide the expected blob, with a response code that MUST be `200 OK`." Response MUST contain `Docker-Content-Digest`; "If present, the value of this header MUST be a digest matching that of the response body." Clients MAY ignore it, but if used MUST verify it; "Clients SHOULD verify that the response body matches the requested digest." | OCI "Pulling blobs" (src l.180-195) |
| Unknown | `404 Not Found` MUST be returned (GET and HEAD). The endpoint table lists only `200` success and `404` failure. Error code names `BLOB_UNKNOWN` (code-1) exist in the error table; this section does not require a JSON body. | OCI l.194, l.209-216; Endpoints table l.846 (end-2); Error Codes table l.891 |
| Malformed digest | **Not specified for blob pull.** The table lists only 404. (Other endpoints: PUT blob 400 on digest problems; DELETE blob 404/400/405.) `DIGEST_INVALID` is defined as "provided digest did not match uploaded content" (upload semantics). | OCI table l.846-858; code-4 l.894 |
| Stored object failing its digest | **Not specified for servers.** Client-side verification only (above). | OCI l.188-193 |
| Range | "A registry SHOULD support the `Range` request header in accordance with RFC 9110 (section 14)." No `206`/`416`/`Accept-Ranges` text for blob pull (416 appears only in the upload flow, l.392, 421). | OCI l.196 |
| HEAD | HEAD on an existing blob MUST return `200`; MUST contain `Docker-Content-Digest` and `Content-Length` ("the size in bytes"); not found is `404`. Note: older registries may omit the digest header; clients "can reasonably assume the digest algorithm used is sha256". | OCI "Checking if content exists" (src l.198-216) |
| CORS | **Not in spec.** | whole OCI spec (no `Access-Control` occurrence found in the `grep` of the file) |
| Cache-Control/ETag | **Not in spec.** | OCI spec |
| Content-Type | **Not specified for blob GET**; `application/octet-stream` appears only for upload requests. | OCI upload sections |
| Redirects | Registries may answer blob GET with a redirect; clients must not forward `Authorization` across hosts. | OCI pull text (from the WebFetch of the same file; not re-grepped) |
Known failures near this area (NVD records, fetched 2026-10-19):
- CVE-2024-3727 (containers/image, Red Hat): "integrity-check flaw" allowing unexpected authenticated registry access, resource exhaustion, local path traversal. It affects the client library, not a registry server. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-3727
- CVE-2026-85747 (distribution/distribution, fixed 3.1.2): the client's manifest `Get()` does not hash the body against the requested digest, so a malicious registry can substitute content. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-85747 (status "Awaiting Analysis")
- CVE-2026-93317 (Docker build): a registry or OCI-layout blob source could supply content not matching the claimed digest and have it cached under that digest. https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-93317 (status "Awaiting Analysis")
- Searched for a path traversal CVE in a registry server's digest-to-path mapping: none surfaced.
## 3. Nix binary cache (HTTP)
The manual's Binary Cache page defines layout only (a `nix-cache-info` file, a
`.narinfo` per object with "a (usually relative) URL to the corresponding
compressed NAR", the NAR). It states no status codes, headers, CORS, Range or
Cache-Control. https://nix.dev/manual/nix/2.35/protocols/binary-cache/ (page head).
The behavior below is from the Nix client/writer source.
| Case | What the source shows | Pointer |
|---|---|---|
| Found | Writer stores NARs at `nar/<FileHash nix32>.nar<ext>`, i.e. named by the hash of the compressed file; narinfo records `FileHash`, `FileSize`, plus `NarHash`. | https://raw.githubusercontent.com/NixOS/nix/master/src/libstore/binary-cache-store.cc l.156-181 |
| Absent | Client maps HTTP `404` and `410` to `NotFound` ("The file is definitely not there"); `403` to `Forbidden`; `408`/`429` to `Transient`. `fileExists` issues a **HEAD** and treats `NotFound` **and `Forbidden`** as absent, "S3 buckets return 403 if a file doesn't exist and the bucket is unlistable". | https://raw.githubusercontent.com/NixOS/nix/master/src/libstore/filetransfer.cc l.826-838; .../http-binary-cache-store.cc l.184-202 |
| Malformed request | Not specified. | n/a |
| Failing digest | Client side only; the writer's `FileHash`/`NarHash` are checked by the consumer. No server answer defined. | binary-cache-store.cc l.175-181, l.313 |
| Range | Client records `acceptRanges` for retries (client-side use of Range). No server rule. | filetransfer.cc l.219 |
| HEAD | Used for existence probing (above). | http-binary-cache-store.cc l.190 |
| CORS / Cache-Control / ETag | Not in spec or the files read. | n/a |
| Content-Type | Writer uploads `text/x-nix-narinfo` (narinfo), `text/x-nix-cache-info`, and `application/x-nix-nar` (NAR). | binary-cache-store.cc l.57, l.135, l.270 |
| Content-Encoding | Writer can upload with a `Content-Encoding` header naming the compression (http-binary-cache-store.cc l.232). Client sets `CURLOPT_ACCEPT_ENCODING ""` (accept any supported) and tracks a `hasContentEncoding` flag. The NAR itself is stored already compressed (named by compressed-file hash), so transport encoding and stored compression are separate. | filetransfer.cc l.223-224, l.616 |
## 4. Bazel remote cache, HTTP
| Case | What the source/spec says | Pointer |
|---|---|---|
| Protocol | Bazel docs: "Binary data (BLOB) is uploaded via PUT requests and downloaded via GET requests"; action results under `/ac/`, files under `/cas/`; any "HTTP/1.1 server that supports PUT and GET" can serve. The doc page states **no** status codes, no digest verification on PUT, no Content-Type, no compression, no Range. HEAD is not mentioned there. | https://bazel.build/remote/caching "HTTP caching protocol" |
| bazel-remote README | CAS key is "the lowercase SHA256 hash of the entry"; `/cas/<key>` or `/<instance>/cas/<key>` (instance "optional but ignored"); "HEAD requests can be used to confirm whether a key exists"; the empty blob is "always available, even if the cache is empty" (HEAD example: `200` with `Content-Length: 0`); GET with `Accept-Encoding: zstd` "may" return zstd data; PUT of zstd requires `Content-Encoding: zstd` and `X-Digest-SizeBytes`. | https://raw.githubusercontent.com/buchgr/bazel-remote/master/README.md (HTTP section) |
| Found | `200`, `Content-Type: application/octet-stream`, `Content-Length` set to the stored size (uncompressed path). | https://raw.githubusercontent.com/buchgr/bazel-remote/master/server/http.go l.275-282 |
| Absent | `404`, body "Not found" via `http.Error`. | http.go l.268-271 |
| Malformed | Path must match `^/?(.*/)?(ac/\|cas/)([a-f0-9]{64})$` (lowercase 64-hex); otherwise `400` with the escaped URL echoed in the message (`html.EscapeString`). | http.go l.33, l.101-112, l.216-223 |
| Failing digest (read) | No digest check on the GET path in this handler (reader returned by `cache.Get`, `io.Copy` to the client). A failure after headers are sent is logged and cannot change the status ("we've already started writing data"). | http.go l.284-297 |
| Digest check (write) | PUT checks size match (`400` "sizes don't match") and `Content-Encoding` is `identity`/`zstd`/absent (else `400`); the remaining digest verification is in code not read here. | http.go l.306-345, l.372-388 |
| Compression | When zstd is chosen the handler sets `Content-Encoding: zstd` and **omits `Content-Length`** ("TODO: calculate Content-Length for compressed blobs too"). | http.go l.246-250, l.275-281 |
| HEAD | `200` with `Content-Length` of the size, or `404`. | http.go l.468-491 |
| Range / CORS / Cache-Control / ETag | Not present in the handler excerpts read. | n/a |
## 5. Git over HTTP
Smart HTTP does not address single objects. Dumb HTTP does: URL
`$GIT_URL/objects/XX/YYYY…` for a loose object.
https://git-scm.com/docs/http-protocol (introduction example).
| Case | What the spec/source says | Pointer |
|---|---|---|
| Absent | Server "MUST NOT respond with `200 OK`"; "SHOULD respond with `404 Not Found`, `410 Gone`, or any other suitable HTTP status code which does not imply the resource exists". Access denied MUST be `403`. | git http-protocol "General Request Processing" |
| Caching | Clients MAY reuse a cached entity per `Cache-Control`/`Expires` (follow RFC 2616); servers MAY return `304`; clients treat `304` as `200`. Smart services must prevent caching. The page states no Cache-Control value for object files. | same section |
| Content-Type | Specified only for `info/refs` (`text/plain; charset=utf-8` SHOULD, clients MUST NOT validate); nothing for object files. | same page |
| Failing digest | Client side: the dumb walker compares the received object's computed id with the requested id and errors `File %s has bad hash`. | https://raw.githubusercontent.com/git/git/master/http-walker.c l.540-541 |
| Missing target | The client treats a missing-target result as `HTTP_MISSING_TARGET` (http.c l.1944). | https://raw.githubusercontent.com/git/git/master/http.c |
| Content-Encoding | Client sets `CURLOPT_ENCODING ""` at http.c l.2284 (accept any encoding curl supports, curl decodes transparently) and sends `Pragma: no-cache` (l.1452). The http-protocol page says nothing about gzip for objects. | http.c |
| Range, CORS, ETag | Not in the page. | n/a |
## 6. Browser fetch facts (verifying reader)
| Fact | Evidence |
|---|---|
| CORS-safelisted response-header names are exactly `Cache-Control`, `Content-Language`, `Content-Length`, `Content-Type`, `Expires`, `Last-Modified`, `Pragma`. So `Content-Length` is readable cross-origin from a CORS response without `Access-Control-Expose-Headers`. `ETag` is **not** on the list; it needs `Access-Control-Expose-Headers` to be visible. | Fetch 2.2.2 "Headers", CORS-safelisted response-header name (https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) |
| The CORS-exposed header-name list comes from `Access-Control-Expose-Headers`; a CORS filtered response exposes safelisted names plus that list. | Fetch 2.2.6 "Responses" (https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list); 3.3.3 |
| CORS check: failure if `Access-Control-Allow-Origin` is absent; "If request's credentials mode is not "include" and origin is `*`, then return success." With credentials mode `include`, `*` is not enough (needs exact origin plus `Access-Control-Allow-Credentials: true`). | Fetch 4.10 "CORS check"; 3.3.5 |
| A CORS-preflight happens for non-safelisted methods/headers (`GET`/`HEAD` with no custom headers are simple requests). | Fetch 3.3 (general); not re-quoted here |
| Fetch decodes content codings before exposing the body. In HTTP-network fetch: it records encoded size from transmitted bytes, sets `bytes` to "the result of handling content codings", then increases decoded size by the decoded length. Unsupported codings: bytes are returned as-is; decoding error: failure. | Fetch 4.7 "HTTP-network fetch"; 2.2.4 "handle content codings" (https://fetch.spec.whatwg.org/#handle-content-codings) |
| The spec itself notes: "This makes the `Content-Length` header unreliable to the extent that it was reliable to begin with." (the header describes the encoded body; the body the page reads is decoded.) | Fetch 4.7 (fetch_t.txt l.9670) |
| When a request has a `Range` header, the browser appends `Accept-Encoding: identity`, "This avoids a failure when handling content codings with a part of an encoded response." | Fetch 4.6 "HTTP-network-or-cache fetch" (l.8760-8767) |
| HTTP framing: with a valid `Content-Length`, a response is incomplete if fewer octets arrive than `Content-Length`; a client MUST record it as incomplete. `Content-Length` "refers specifically to the amount of data enclosed" (encoded form on the wire). | RFC 9112 section 8 "Handling Incomplete Messages"; RFC 9110 section 8.6 |
| `Cache-Control: no-transform`: an intermediary "MUST NOT transform the content". | RFC 9111 section 5.2.2.6 |
| `immutable` extension: identifies responses that will not be updated during freshness lifetime so clients need not revalidate. | RFC 8246 section 2 |
## 7. HTTP core facts that apply to all five systems
| Fact | Evidence |
|---|---|
| Range: "A server MAY ignore the Range header field"; a server MUST ignore Range for methods without defined range handling (GET is the only one). | RFC 9110 section 14.2 "Range" (l.6476-6484) |
| HEAD: "server SHOULD send the same header fields" as for GET; a `Content-Length` on HEAD MUST equal the GET length if sent. | RFC 9110 section 9.3.2 (l.3993), section 8.6 (l.3225) |
| `304`: a `Content-Length` in 304 MUST equal what a 200 would carry, if present. | RFC 9110 section 8.6 |
| `Content-Length` MUST NOT be sent on `204`. | RFC 9110 section 8.6 |
## 8. Cross-check against the draft model page's from-memory claims
| Draft claim (map l.44-49) | Finding |
|---|---|
| IPFS trustless gateway answers raw blocks with `Cache-Control: public, max-age=29030400, immutable` | In **PG**, not TG: the `/ipfs/` immutable value is a MUST there; TG itself does not state the response value. |
| IPFS uses the CID as `ETag` | PG: default `Etag: "<cid>"` quoted; variants add suffixes (raw: `"<cid>.raw"`). |
| IPFS returns 400 for an unparseable CID | PG lists `400` as "generic client error" with malformed-CID as the example. TG itself lists no malformed-CID rule. |
| OCI `GET`/`HEAD` returns 200 with the digest header | Confirmed (`Docker-Content-Digest` MUST on GET and HEAD; `Content-Length` MUST on HEAD). |
| OCI 404 `BLOB_UNKNOWN` | 404 confirmed; `BLOB_UNKNOWN` exists as a code name; the blob-pull text does not mandate a JSON body. |
| OCI 400 `DIGEST_INVALID` on a bad digest | **Not in the blob-pull text.** Endpoint table lists only 404 for blob GET/HEAD; `DIGEST_INVALID` means "digest did not match uploaded content". |
| OCI Range (206/416) optional | Spec says "SHOULD support" Range per RFC 9110; no 206/416/Accept-Ranges text for pull. |
| Nix `<hash>.narinfo` 200 or 404, probed with HEAD | Client source: HEAD for existence; 404 and 410 both map to NotFound; 403 also treated as absent in `fileExists`. |
| Map's own claims (open `*` CORS, 500 on corrupt, Range ignored, Cache-Control immutable) | `*` with non-`include` credentials passes the CORS check (Fetch 4.10). No system surveyed specifies a status for a corrupt stored object. Ignoring Range is permitted (RFC 9110 14.2). |
## 9. Where the systems agree and differ (matrix of what was found)
| Item | IPFS TG/PG | OCI | Nix | Bazel (bazel-remote) | Git dumb |
|---|---|---|---|---|---|
| Absent | 404 (MUST, TG) | 404 (MUST) | 404/410 -> NotFound; 403 also "absent" in HEAD probe | 404 "Not found" | 404/410/other non-200 |
| Malformed id | PG 400 generic; TG silent | silent for blob pull | silent | 400 with message | silent |
| Corrupt stored object (server) | silent; client MUST verify | silent; client SHOULD verify | silent; client verifies hashes | no read-path check in handler | silent; client errors "bad hash" |
| Range | PG: 206 supported (raw blocks); TG silent | SHOULD support | client-side retry only | not in handler | not in spec |
| HEAD | same as GET minus body; 404 | 200 + digest + Content-Length MUST | used to probe | 200 + Content-Length or 404 | not in spec |
| CORS | none in TG/PG (Kubo adds implicit headers) | none | none | none | none |
| Cache-Control on content | PG: `public, max-age=29030400, immutable` MUST | none | none | none | defers to RFC 2616 |
| ETag | PG: quoted CID | none | none | none | 304 allowed |
| Content-Type | `application/vnd.ipld.raw` | unspecified (pull) | `application/x-nix-nar` etc. | `application/octet-stream` | unspecified (objects) |
| Content-Length vs encoding | PG warns they may differ | n/a | transport `Content-Encoding` separate from stored compression | zstd path omits Content-Length | client auto-decodes |
| nosniff / attachment | TG: Content-Disposition attachment MUST; PG: nosniff | none | none | none | none |
## 10. Limits of this pass
- Fetch spec 4.7 was read from the HTML text; the exact algorithm line that fails the fetch on a body shorter than Content-Length was not found in the Fetch text (framing is RFC 9112 section 8, which says the client records the message incomplete).
- bazel-remote digest verification on PUT was not read to the end; the read-path (GET) has no digest check in the handler.
- Kubo's actual default CORS header values were not retrieved (only the pointer to boxo and ipfs/specs#423).
- git CVEs were not searched; Nix and Bazel CVEs were not searched.
- OCI "CORS: not in spec" is from a search of the spec file, not from a registry implementation (distribution/distribution's own headers were not read).

View file

@ -0,0 +1,59 @@
# Consumer pinning at a breaking land: prior art and internal state
Serves fork `variants:question:unpinned-consumers-at-land` (campaign `variants`, session `self-2026-10-22-eureka-c2a`).
Eyes pass, 2026-10-22. Facts and evidence pointers only; no recommendation.
Question as admitted: Eve.Surface and Brokkr build against an unpinned sibling `..\CultLib` source checkout. C2a (refuse element types with no id) lands on main. Options: pin-then-land, sweep-then-land, land-and-break.
## Sources
| # | Source | URL | Status |
|---|---|---|---|
| S1 | Abseil, About / philosophy | https://abseil.io/about/philosophy | fetched |
| S2 | Abseil, compatibility guidelines | https://abseil.io/about/compatibility | fetched |
| S3 | Software Engineering at Google, ch. 21 Dependency Management | https://abseil.io/resources/swe-book/html/ch21.html | fetched |
| S4 | Software Engineering at Google, ch. 16 Version Control and Branch Management | https://abseil.io/resources/swe-book/html/ch16.html | fetched (does not cover live at head) |
| S5 | Russ Cox, Minimal Version Selection | https://research.swtch.com/vgo-mvs | fetched |
| S6 | Cargo book, Cargo.toml vs Cargo.lock | https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | fetched |
| S7 | npm docs, package-lock.json | https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json | fetched |
| S8 | Chromium src, dependencies doc (DEPS, gclient, roll-dep) | https://chromium.googlesource.com/chromium/src/+/main/docs/dependencies.md | fetched |
| S9 | Pro Git, Submodules | https://git-scm.com/book/en/v2/Git-Tools-Submodules | fetched |
| S10 | chromium infra dependencies.md | https://chromium.googlesource.com/infra/infra/+/main/docs/dependencies.md | 404, not retrieved |
| L1 | `F:\Projects\Aetheria\Directory.Build.targets`, `Directory.Build.props`, git history | local files | read |
| L2 | `F:\Projects\Aetheria\docs\cultcache-migration-cut.md` (Q6 table, lines ~565-580; pin guard notes ~1900-1925) | local file | read |
| L3 | `F:\Projects\Eve` and `F:\Projects\Brokkr` csprojs and git file lists | local files | read |
| L4 | voidbot `search_history` (two queries), `search_sources` (one query) | MCP | queried |
Anything not in a "fetched" row above is not claimed from the web.
## 1. Established practice: who pays for a breaking upstream change, and when
- **Abseil / live at head (S1, S2).** Recommends users track the latest code. Says it will provide a tool to perform an API refactor for well-behaved code (S1). Requires of downstream: show how you call it, stay within the compatibility guidelines; otherwise fork or use periodic supported branches (about every 6 months, patched 2 years) (S1). Says every change is potentially breaking for someone; breaks are announced in commit messages and LTS notes, and users are asked to report undetected breaks on GitHub (S2). Users should be able to temporarily pin to an earlier commit if a newer change is a problem (S2). S1/S2 do not mention Rosie, the Beyonce rule, or caller-fixing; the fetched pages describe tooling and a feedback channel only.
- **Live at Head as described in SWE at Google ch. 21 (S3).** Providers are expected to test changes against downstream dependents before committing; breaking changes wait until dependents are updated or an automated migration tool exists. Consumers must keep meaningful tests. Stated preconditions: unit tests across dependencies run in CI, a visible reverse-dependency graph, substantial compute, no pinning of dependencies, provider acceptance of the downstream-testing duty. Called "costly but effective" inside Google and "largely unproven at scale." Notes that under SemVer a major bump costs maintainers little and many indirect users a lot. Ch. 16 (S4) does not address live at head; it states large-scale changes across trunk are costly and tooling-supported.
- **Go MVS (S5).** Selects the oldest version satisfying all declared minimums. Publishing a new version changes nobody's build; a consumer gets it only when a requirement names it, directly or through another module. Upgrades are explicit edits to `go.mod` by the top-level module. Compatibility rule: newer versions inside one major version are expected to work where older did; a major version is a different module identity. Cost of a breaking change falls on whoever opts into the new version, at the time they opt in.
- **Cargo (S6).** Git dependency without `rev` takes the default-branch tip at first build and fixes that commit in `Cargo.lock`; changes arrive only on `cargo update`. A `rev` in `Cargo.toml` pins explicitly; the Cargo book calls hand-managed SHAs "tedious and error prone" and prefers the lockfile. Consumer pays at `cargo update` time.
- **npm (S7).** `package-lock.json` records the exact tree; installs stay on it until the lockfile changes and is committed. It is not published with a package. The page gives no timeline for when updates reach a consumer.
- **Chromium DEPS (S8).** `DEPS` stores dependency URLs and commit hashes (often in `vars`). Developers roll with `gclient setdep`, `roll-dep`, or manual `git update-index`; some projects (Skia) are autorolled. The page says manual rolling after an upstream change is good practice to confirm the roll compiles and tests pass. It does not describe revert procedure. Cost is paid by the roller at roll time.
- **Git submodules (S9).** The superproject records one commit SHA (gitlink, mode 160000). Upstream commits never move the pin; it moves when someone updates the checkout and commits the new SHA in the superproject. Collaborators receive the pin via pull and must run `git submodule update`.
## 2. Internal precedent
- **Aetheria pin (L1).** `Directory.Build.props` holds `CultLibRevision` (currently `45c2f4006f5f7dab357426cabd3197e8d0a17f66`) and `CultMathRevision` (`9c1b06cd703bfa44352050e819b0d0659cf5978a`). `Directory.Build.targets` target `VerifyCultLibRevision` (BeforeTargets `ResolveProjectReferences`) errors if the sibling checkout's `git rev-parse HEAD` differs from the pin, or if `git status --porcelain` is non-empty; escape hatch `SkipCultLibRevisionCheck=true`. File is 39 lines; the pin is one pair of properties per pinned package.
- **Why added (L1 git).** Commit `4f4beb0f` (2026-09-14, "Pin CultLib for headless builds"): "Directory.Build.targets carries Delvehold's VerifyCultLibRevision guard, checking for CultMath.csproj: HEAD must equal the pin and the worktree must be clean." Commit `e91ba369` (2026-09-24, "Pin CultLib per package, as Unity's manifest does"): headless build must compile the revisions Unity consumes; a single pin would have compiled six commits of newer CultCache under headless tests while Unity stays on its tag. The Aetheria cut doc (L2, ~line 1905) calls the guard "the only working pin for a sibling checkout." Aetheria's `Packages/manifest.json` pins Unity packages by git tag.
- **Pin guard already in Delvehold (L4/search_sources hit, `Delvehold:Directory.Build.targets`)** and a variant in AetheriaEve (archived, `Aetheria.State.Dependencies.targets`, also checks a Unity package tree hash). Delvehold's guard checks `GameCult.Mesh.csproj` exists, HEAD equals `CultLibRevision`, tree clean. Date of Delvehold's first guard: not determined.
- **Consumer table (L2, Q6 in Aetheria `docs/cultcache-migration-cut.md`).** Aquarium, Ymir, AquaSynth, Mimir, Gjallar: ".NET sibling, unpinned", compile against main at once. Brokkr, Eve, EveUnity, EvePlugins: "Unity package / sibling", pin "various". Delvehold: sibling with revision guard at `334e60f`, 52 behind, "Not until it re-pins." Heimdall: submodule `b6b1d9c`. Rust consumers (Idunn, Epiphany, Odin, Ghostlight, Muninn, Ratatoskr, CodexConnector): Cargo by commit.
- **Operator discussion (L4).** voidbot `search_history` ("pin CultLib revision vs track main for consumers"; "Brokkr Eve sibling CultLib checkout build pin") returned no relevant operator statement on pinning versus tracking; hits were unrelated Discord chatter. Nearest operator words found in the Discord index: Metacrat, 2026-05-09, "Oh, anyone using CultLib will still be paying the blood price, you can count on that" (message 1502671320406687894; context not fetched, so its subject is not confirmed). No operator quote on pinning versus tracking was found in memory files grepped (`C:\Users\Meta\.claude\projects\F--Projects\memory`, pattern pin/track main/unpinned/sibling) beyond a retired Idunn pin line and Odin/Muninn pin-branch coordination notes.
- Aetheria cut doc operator mentions near pins are about content (Schematic-shape drawer, 2026-09-14; Studio grouping, 2026-09-17), not pinning policy.
## 3. Current state of the two consumers (L3)
- **Eve.** `packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj:16` `CultLibRoot` defaults to `$(MSBuildThisFileDirectory)..\..\..\CultLib`; lines 20-21 ProjectReference `$(CultLibRoot)\src\GameCult.Caching\...` and `...\GameCult.Mesh\...`. `tests/GameCult.Eve.Surface.Serialization.Tests/*.csproj:19` references `$(CultLibRoot)\src\GameCult.Mesh\...` (and Eve.Surface by relative path). `git ls-files` shows only `packages/org.gamecult.eve.surface/Directory.Build.props` (sets only BaseOutputPath/BaseIntermediateOutputPath/MSBuildProjectExtensionsPath, no revision). No `Directory.Build.targets`, no `global.json`, no `.gitmodules`, no revision property or check. Eve also holds Cargo.toml/Cargo.lock (Rust side, not examined).
- **Brokkr.** `brokkr-command/Brokkr.Command.csproj:7`, `brokkr-contracts.Tests/Brokkr.Contracts.Tests.csproj:7`, `brokkr-contracts/Brokkr.Contracts.csproj:8`: `CultLibRoot` defaults to `..\..\CultLib`. ProjectReferences: Caching, Caching.MessagePack, Mesh, Networking (by project). No `Directory.Build.*`, no `.gitmodules` in `git ls-files`; no revision property or check.
- Neither repo contains `CultLibRevision`, a `VerifyCultLibRevision` target, or any HEAD check. Both build whatever `F:\Projects\CultLib` currently holds (including dirty or other-branch states).
- Fork text claims (not re-verified here): Eve.Surface has 0 `[CultElementId]`; Brokkr `[CultDocument]` files show no object lists by grep.
## Gaps
- S10 (Chromium infra) 404; Chromium autoroller behavior beyond S8 not fetched.
- Rosie / Beyonce rule not found in fetched pages; not cited from memory.
- No operator statement on pin-versus-track found in voidbot or the memory grep.

View file

@ -0,0 +1,74 @@
Serves the C2a fork on finding variants:finding:cut-land-c2a-gate.s1.reflist-record-read-only-at-head (campaign variants), 2026-10-23.
# Prior art: read with an old writer schema, then write back
Facts only. Pages were fetched 2026-10-23 through a summarising fetcher; "not stated" means the fetched page did not say it, not that no source does. Items marked (memory) are my background knowledge, not verified from a fetched page.
## 1. Apache Avro
- Source: https://avro.apache.org/docs/current/specification/ (Schema Resolution; summarised via apache/avro doc source), Java `DataFileWriter` javadoc (1.11.1).
- Model: the writer schema travels with the data. The reader resolves it against its own reader schema. Resolution is a read-time projection; the spec defines no write-back.
- Data loss on projection: "the writer's value for that field is ignored" when the reader schema lacks the field. A reader-only field takes its default, or the read errors if it has none.
- Write-back: the spec states no rule. Object container files store one schema in the header ("Each datum in a file is of the same schema"). `appendTo` is documented only as "Open a writer appending to an existing file"; it does not say which schema the append uses. (memory: Java `appendTo` reuses the file's header schema, so an append never restamps.)
- Stated reason for the read-time design: readers and writers evolve separately (fields added or removed in a different software version).
## 2. Protocol Buffers (proto3)
- Source: https://protobuf.dev/programming-guides/proto3/ (Unknown Fields).
- On write: unknown fields are "included during parsing and in the serialized output" (matches proto2). Round-trip keeps fields the current type does not know.
- Loss cases the page lists: serializing to JSON, and copying a message field by field into a new message (it recommends `CopyFrom`/`MergeFrom`).
- No schema id or version is written. The wire format carries only field numbers, so "restamp" has no meaning here.
- Not stated: behaviour in versions before 3.5 (memory: proto3 dropped unknown fields before 3.5, then reversed). The Java reference page says nothing on preservation.
- Stated reason: preserve data across a mixed-version fleet.
## 3. Confluent Schema Registry
- Source: https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html
- BACKWARD (default): consumers on the new schema read data written with the new or the previous version. FORWARD: data written with the new schema is readable by consumers on the old one. FULL: both.
- Write: "Each schema version gets a unique ID and an incremented version number." A producer writes under the schema it holds (its own current version). The id is stamped per message at serialisation, from the producer's schema.
- Upgrade order is how the rules prevent loss. BACKWARD means upgrade consumers before producers. FORWARD means upgrade producers first, and keep old-schema data away from consumers until they are upgraded.
- Not stated on the page: what a consume-modify-produce app writes. (memory: it re-serialises under its own latest schema, so fields it does not model are lost unless a compatibility rule kept them.)
- The registry gates registration. It does not gate individual rewrites.
## 4. Document DBs and ORMs with stored versions
### MongoDB schema-versioning pattern
- Sources: https://www.mongodb.com/blog/post/building-with-patterns-the-schema-versioning-pattern ; https://www.mongodb.com/docs/manual/data-modeling/design-patterns/data-versioning/schema-versioning/
- Documents carry `schema_version` (absent means 1). The app keeps a handler per version and writes new documents at the current version.
- Migration strategy is the app's choice: migrate all, migrate "when accessed" (lazy), or never.
- Not stated: whether an update of an old document restamps it. The manual's update example branches on `schemaVersion` and writes into each version's own shape. It notes its `else: null` branch adds stray null fields to the other shape.
- Stated reasons: no downtime, failed migrations easier to handle, timing flexibility. Cost: handler code per live version.
### Realm (Atlas Device SDKs, now deprecated)
- Source: https://www.mongodb.com/docs/atlas/device-sdks/sdk/swift/model-data/change-an-object-model.md
- Per-file schema version. A changed schema needs an incremented `schemaVersion`, plus a `migrationBlock` for non-trivial changes. The file is migrated whole at open, before any read or write, and Realm "will throw an error" if the version is lower or the same but the schema differs.
- No lazy per-object restamp. Writes happen only on a migrated file.
- Data loss: the manual says "Deleting a property will not impact existing objects", i.e. removal is automatic and needs no developer step. Required new properties and renames need a manual block. (memory: removed-property values are dropped from the file.)
### Core Data
- Source: web search results only (Apple forum threads 745114 and 785427, WWDC22-10120, "staged migrations" doc). The Apple docs URL I tried returned 404.
- A store whose model hash differs from the current model fails to open with an incompatible-version error. Opening migrates the whole store when `NSMigratePersistentStoresAutomaticallyOption` and `NSInferMappingModelAutomaticallyOption` are set. Lightweight migration covers only obvious patterns (add entity, remove relationship, required to optional, rename). Anything else needs a staged or custom migration.
- Migration is store-wide at open. It does not happen per record on write.
### EF Core
- Not fetched. (memory: no per-row schema stamp. Migrations are explicit, ordered DDL, tracked in a `__EFMigrationsHistory` table.)
## 5. Old-schema data held read-only until explicit migration
### Elasticsearch archive indices
- Sources: https://www.elastic.co/docs/deploy-manage/upgrade/deployment-or-cluster/reading-indices-from-older-elasticsearch-versions ; https://www.elastic.co/guide/en/elasticsearch/reference/current/archive-indices.html ; 8.0 migration notes (7.0+ indices readable, 6.x or earlier must be reindexed).
- Indices from an older major are readable through search and field-caps. "They do not support the Get API or any write APIs."
- The route to a writable copy is reindex into a new index that is "fully compatible with the current Elasticsearch version". `_source` is readable, so no data is lost on reindex.
- Stated reason: the page does not say why they are read-only. It describes archive access as "slower read-only access", with limited query capability. Some legacy mappings cannot be imported (the original is kept in `_meta`), and that is the one place the page shows a lossy translation.
- Third-party (Search Guard blog, unverified against Elastic): in 9.x a plain `index.blocks.write` does not satisfy the boot check; `index.verified_read_only` is needed.
- This is the closest match to option (b) in the fetched set: an old format, readable but not writable, left until the owner reindexes it.
## Where restamp-on-write loses data (from the sources above)
- Avro: a field present in the writer schema and absent from the reader schema is dropped at read, "ignored". Any rewrite from the resolved value would omit it.
- Protobuf: unknown fields survive a binary round trip. They are lost via JSON, or when copied field by field.
- Realm: removed properties are deleted by the automatic migration.
- MongoDB: a handler that writes only the current shape can lose or relocate old-shape fields (the manual's example adds nulls).
- Confluent: loss is prevented by compatibility rules at registration time, not at each write.
## Pattern across the set
- No fetched system restamps a single record on write when the record's writer schema differs from the application's type, on an app-visible, per-record basis.
- Avro, protobuf and Confluent carry the schema per file or message.
- Realm, Core Data and Elasticsearch move or block the whole store or index at open.
- MongoDB leaves it to the app.
- Whole-store migration at open is the usual explicit step (Realm, Core Data). Elasticsearch is the one case of read-only-until-reindex.
- Not found: a system that declares per-schema-id compatibility, as in option (b). Avro schema resolution and Confluent compatibility rules are the nearest analogues. Both are read-side or registration-side declarations.

View file

@ -0,0 +1,53 @@
Serves question variants:question:store-hold-user-code (campaign variants), 2026-10-23.
# Prior-art check: user code under a lock
Facts and pointers only. Each source was fetched or searched this session; what was not reachable is marked.
## 1. Goetz et al., Java Concurrency in Practice (Addison-Wesley, 2006), ch. 10 "Avoiding Liveness Hazards"
Status: EXISTS, SECTION NUMBER CORRECTED.
- "Open Calls" is **10.1.4**, not 10.1.3. 10.1.3 is "Deadlocks Between Cooperating Objects"; 10.1.2 is "Dynamic Lock Order Deadlocks"; 10.1.5 is "Resource Deadlocks". Source: text extracted from a PDF copy of the book (its TOC lists Listing 10.6 "Using Open Calls to Avoiding Deadlock Between Cooperating Objects", p. 133). The copy is an unofficial one at library.megu.edu.ua; a publisher link was not checked.
- (a) 10.1.3 gives the warning sign: an "alien method" (defined p. 40 as a method whose behavior the calling class does not fully specify) called while holding a lock. Paraphrase: such a call may take other locks (deadlock risk) or block a long time, stalling threads that need the lock you hold. 10.1.4 defines an open call as a call made with no locks held (credited to Lea, CPJ 2.4.1.3) and says programs restricted to open calls are far easier to analyze for deadlock-freedom than ones that allow alien calls under lock.
- Cost it names: moving a call out of a synchronized block makes an atomic operation non-atomic (its getImage example stops being a snapshot). Where atomicity is needed it suggests structuring so only one thread can run the code path after the open call (service shutdown example).
- (b), (c): not addressed in this section.
- Pointer: JCIP 10.1.3 and 10.1.4, pp. ~132-134 (chapter 10 starts p. 128).
## 2. Sutter, "Effective Concurrency: Avoid Calling Unknown Code While Inside a Critical Section"
Status: VERIFIED, with date caveat.
- Dr. Dobb's column in the "Effective Concurrency" series. Sutter's blog teaser is dated 2007-11-06: https://herbsutter.com/2007/11/06/effective-concurrency-avoid-calling-unknown-code-while-inside-a-critical-section/ . Original link ddj.com/architect/202802983; Dr. Dobb's mirror https://drdobbs.com/cpp/avoid-calling-unknown-code-while-inside/202802983 (4 pages). The Dr. Dobb's issue date was not shown in anything fetched; the blog date is the only date confirmed. www.drdobbs.com did not resolve from this machine, so the column body comes from search-result summaries and the blog teaser, not a full first-hand read.
- (a) Core claim: locks do not compose; two separately correct modules can deadlock when combined. Trigger: caller inside a critical section, callee enters another critical section or blocks, another thread can take the same two in the opposite order. "Unknown code" listed as: library functions, plug-ins, callbacks/function pointers, virtual methods, generic types.
- Mitigations listed: move the call outside the critical section; pass copies instead of shared data; narrow the critical section (finer locks, reader-writer); require callees not to block (documentation/discipline only).
- (b), (c): not addressed.
## 3. Linux lockdep
Status: VERIFIED, with a nuance.
- Source: kernel docs "Runtime locking correctness validator", https://docs.kernel.org/locking/lockdep-design.html .
- (b) Lockdep tracks lock classes, not instances, and records dependencies between classes as sequences are observed. It reports a circular order as a potential deadlock even if the deadlock never happened. Its "Proof of 100% correctness" section says that once each simple single-task locking sequence has occurred at least once, no timing of them can deadlock. Each new lock chain is validated once and hashed; repeats skip validation.
- Nuance: it flags the dependency edge that closes a cycle, so both orders must have been exercised at some point (possibly by different, non-concurrent runs). A lone nesting is flagged only if it breaks a single-lock rule (IRQ-safe vs unsafe, same-class recursion). "Refuses on first sight" fits the closing edge or a single-lock rule violation only. It warns; it does not block the acquisition.
## 4. ConcurrentHashMap re-check after locking
Status: VERIFIED.
- Source: OpenJDK java/util/concurrent/ConcurrentHashMap.java, `putVal` and `computeIfAbsent`: head node `f` is read unlocked, `synchronized (f)` is taken, then `tabAt(tab, i) == f` is re-checked; on failure nothing is done and the loop retries. The overview comment gives the reason: the locked node can be replaced, so first-in-bin must be confirmed after locking. (Only the first 100k of 269k chars were read; both methods were reported found there.) Empty-bin `computeIfAbsent` uses a ReservationNode plus CAS instead.
- (c) This is the pattern: choose the lock from an unlocked read, validate the read after acquiring.
## 5. LMDB
Status: VERIFIED (paraphrase). Not about alien code or lock order.
- Source: lmdb.h, `mdb_txn_begin` and `mdb_env_open` (https://github.com/LMDB/lmdb, libraries/liblmdb/lmdb.h). A thread may hold one transaction at a time (plus children); a transaction and its cursors belong to one thread; one write transaction at a time, so writers cannot deadlock each other; MDB_NOTLS ties reader slots to the txn object instead of the thread, and the application must then serialize writers itself because write locking is unaware of user threads.
- Relevance: "one hold per thread" as a deliberate rule, parallel to CultCache's one-cache-gate-per-thread.
## 6. SQLite btmutex.c
Status: VERIFIED (summary of trunk source).
- Source: https://github.com/sqlite/sqlite/blob/master/src/btmutex.c (read via raw.githubusercontent.com; sqlite.org served a bot check).
- (b) Prevention by ordering, not detection: sharable B-trees on a connection sit in a list sorted by BtShared address and are always locked ascending (`sqlite3BtreeEnterAll`). `sqlite3BtreeEnter` try-locks first; on failure it releases later-ordered locks, waits, then reacquires in order. `wantToLock` is a count that makes a non-recursive mutex behave recursively.
- No alien-code or callback discussion in these comments.
## Counter-positions and how they stay safe
- Callbacks under lock, documented: Java `ConcurrentHashMap.compute`/`computeIfAbsent`. Javadoc (Java 21): the function should be short and simple because other threads' updates may block meanwhile, and must not modify the map. The javadoc does not say "internal lock"; that it runs under the bin lock is inferred from the source in item 4. Safety rests on documented restrictions, not enforcement.
- Opposite choice: .NET `ConcurrentDictionary.GetOrAdd(key, valueFactory)`. Writes use fine-grained locks but valueFactory runs outside them, "to avoid the problems that can arise from executing unknown code under a lock" (doc remarks). Price: not atomic; the factory may run more than once; a recheck decides the winner (another thread's insert wins). https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.getoradd . Same shape as option `encode-first` (user code outside the lock, then recheck and commit) and as item 4.
- JCIP and Sutter both name the cost of leaving the lock: lost atomicity. Sutter also offers "pass copies" and "callee must not block" as alternatives to leaving.
- Not researched (no source fetched): Linux notifier chains, Java `synchronized` event listeners, other systems that call back under lock by design. No verified citation for them.
## Unverified / not done
- JCIP: section numbers and text come from one PDF copy, not the publisher edition.
- Sutter: full column body not read first-hand.
- No Discord or voidbot content read.

View file

@ -0,0 +1,90 @@
# Prior art: a process-global default registry deciding write ownership
Serves `variants:question:registryless-write-reads-shared` (campaign `variants`). Eyes pass, session `self-2026-10-27b-eureka`, 2026-10-27. Facts only; items marked "summary" rest on search summaries not opened.
# Prior art: process-global type registries and write/overwrite ownership
Fork: `variants:question:registryless-write-reads-shared` (viewed in full via eureka-state, status InForce, options `shared-decides` / `written-type-only`, recommended `shared-decides` at ~65%).
Facts only. No conclusion. Date 2026-10-27.
Method note: web pages were read through WebFetch (a small model summarises the page) and WebSearch summaries. Quotes below are what those tools returned; where only a summary was available it is marked "summary". Nothing was run.
## A. Local mechanics (CultLib, read-only, `C:\cl-wt\c2a-land\src\GameCult.Caching\CultCache.cs`)
- `CultDocumentRegistry.Shared` is `Lazy<CultDocumentRegistry>` (line 639-645). Its public constructor calls `Refresh()` (647-650), which scans `ReflectionExtensions.GetAttributedDocumentTypes()` (688; defined in `ReflectionExtensions.cs:10`) and rebuilds the indexes under `_registrationGate`. So Shared's initial content is whatever `[CultDocument]` types are in loaded assemblies at first touch of `Shared`.
- `GetRequired(Type)` (697-711) registers an unregistered `[CultDocument]` type on demand. So Shared also grows whenever any caller asks for a type. `Refresh()` is public and rebuilds the index from currently loaded assemblies.
- `ForTypes(IEnumerable<Type>)` (658-669) builds a registry with `discoverLoadedDocuments:false`, containing only the given types (plus those `GetRequired` pulls in).
- `CultCache(registry = null, ...)` sets `_registry = registry ?? CultDocumentRegistry.Shared` (2202-2205).
- `WriteDocument<TDocument>(key, doc, storedAt, registry = null)` (4515-4545): `resolvedRegistry = registry ?? Shared`; `catalogEntry = resolvedRegistry.GetRequired<TDocument>().ToCatalogEntry()`; then `Cacheless(resolvedRegistry, ...)` lends that registry to the store for the call. XML doc (4510) says Shared "can only refuse more".
- `WriteRecord(..., registry = null)` (4491-4497): XML doc (4489) says that with registry omitted "it decides nothing for copied records and the one entry is the only claimant".
- `OwnsOnDisk(upserts)` (4741-4759): when the store is cacheless and upserts are given, ownership of an on-disk record is decided by `CultDocumentRegistry.Owns(id, catalog, declared, new CatalogEntryLookup(writes, AttachedRegistry), ..., claimant => writes.Any(entry => entry.SchemaId == claimant.SchemaId))`. The comment (4731-4735, 4738-4740) says written entries shadow the lent registry's type for their ids, the registry adds other claimants, and the record is owned only when the claimant is a written entry, "a registry can only refuse more". `CatalogEntryLookup` ctor at 604 takes `(entries, CultDocumentRegistry? others)`.
- Net: with `registry == null` the lent registry's claimant set is Shared's current contents, which depend on assemblies loaded and on every prior `GetRequired` call in the process.
## B. Q1. Process-global, lazily filled registries in established libraries
### MongoDB C# driver, `BsonClassMap`
- Global: yes. `BsonClassMap.RegisterClassMap` is process-wide static; unregistered types are auto-mapped on first use. Docs: "You must register a class map _before_ it's needed in your code." and "We recommend registering class maps before initializing a connection with MongoDB." https://www.mongodb.com/docs/drivers/csharp/current/serialization/class-mapping/ (WebFetch; the page itself says nothing on freezing or late-registration errors).
- Behaviour depends on what has been touched: EF Core provider issue EF-88 reports "Unknown discriminator value 'List`1'" when the first operation after startup is a read; reporter links it to a class map that is created only on write. Status Closed/Fixed in 8.0.0; page has no maintainer comment. https://jira.mongodb.org/browse/EF-88 (WebFetch). Reporter says explicit `RegisterClassMap` at startup did not fix it for them.
- A MongoDB community answer (via search summary, not opened): discriminator mappings persist once registered, so a read after an insert works; fix offered is `[BsonKnownTypes]`. https://mongodb.com/community/forums/t/c-driver-why-is-bsonknowntypes-attribute-needed-for-polymorphism/2183 (summary only).
- Register-once contract: CSHARP-4424 states the three `RegisterClassMap` methods are "meant to only be called once for a given type"; requested because several libraries share data models and do not control init order. Closed Done, fix version 2.19.0; the page does not say which API shipped. https://jira.mongodb.org/browse/CSHARP-4424 (WebFetch). Community workaround named in search summary: `BsonClassMap.IsClassMapRegistered`.
- CSHARP-1879 (search summary): `RegisterClassMap` accepted an unfrozen map and failed later on use; fixed 2.4.2. https://jira.mongodb.com/browse/CSHARP-1879 (not opened).
- CSHARP-5597 (search summary): `BsonSerializer.RegisterDiscriminator` has no effect; discriminator dictionary populated at class-map registration. https://jira.mongodb.org/browse/CSHARP-5597 (not opened).
- Polymorphic docs: "You must specify hierarchical discriminators in root-to-leaf order." and the relationship of all classes in a hierarchy must be documented before deserialization. https://www.mongodb.com/docs/drivers/csharp/current/serialization/polymorphic-objects/ (WebFetch).
- Particular docs report an order-dependent GuidSerializer failure ("incorrect order of class mappings"). https://docs.particular.net/persistence/upgrades/mongodb-4to5 (search summary, not opened).
- Did they move away: not found. Class maps remain global static; mitigation is documentation plus the IfNotRegistered API.
### protobuf-net `RuntimeTypeModel.Default`
- Global: yes. README: "The Serializer.* methods are basically just shortcuts to RuntimeTypeModel.Default.*". https://cdn.jsdelivr.net/gh/protobuf-net/protobuf-net@main/README.md (WebFetch).
- Lazy fill, freezing, order-dependent bugs: NOT verified. README is silent; a direct docs fetch (docs.protobuf-net.dev/runtime) was redirected and not followed. No issue found.
### MessagePack-CSharp
- Global mutable default exists: `MessagePackSerializer.DefaultOptions`. README (WebFetch): labelled "Global state; Not recommended when writing library code." Safe if you control all MessagePack code; libraries should pass options explicitly because others "may change this static property." Libraries "must not use StaticCompositeResolver - as it is global state". https://github.com/MessagePack-CSharp/MessagePack-CSharp/blob/master/README.md
- Resolvers are an ordered list (`CompositeResolver`): order matters by construction.
- Whether it moved away from the global: not found.
### System.Text.Json
- `JsonSerializerOptions.Default`: "Gets a read-only, singleton instance"; remarks give a performance reason: shared instance avoids "redundant recomputation of converters". https://learn.microsoft.com/dotnet/api/system.text.json.jsonserializeroptions.default (WebFetch). The page states no rationale about cross-component interference; any such reading is inference. Design-discussion issue in dotnet/runtime: searched, not found. Whether Default was ever mutable: not verified.
- Type-info resolvers: not researched beyond the above.
### Json.NET
- `TypeNameHandling` is a security concern, not an order concern: CA2326 says "An attacker could modify the serialized data to include unexpected types"; mitigation is a custom `ISerializationBinder` restricting types. https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2326 (WebFetch).
- `JsonConvert.DefaultSettings` global behaviour/guidance: the Newtonsoft page fetched says nothing about global effects. Not verified.
### Orleans
- Serializer discovery is build-time source generation (`[GenerateSerializer]`, `[Id]`), not a runtime scan; the docs' older code-generation section lists scan of assemblies referencing the core library plus `KnownType`/`KnownAssembly` attributes. Aliases: "Type aliases are globally scoped, and you cannot have two aliases with the same value in an application." https://learn.microsoft.com/en-us/dotnet/orleans/host/configuration-guide/serialization (saved locally: `C:\Users\Meta\.claude\projects\F--Projects-CultLib\0840c7c7-0e31-410e-9012-afb79c3c0742\tool-results\toolu_0148m7no1AbrJzWmNLicn6N9.txt`, lines 134, 839-841).
- Type manifest / codec registry runtime behaviour, issues: not researched.
### EF Core
- Model is built per `DbContext` type and cached (IMemoryCache; "the built model has a cache size of 100"); initialised at first operation: "just creating a DbContext instance does not cause the EF model to be initialized." https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics
- Assembly scan: `ApplyConfigurationsFromAssembly` — "The order in which the configurations will be applied is undefined, therefore this method should only be used when the order doesn't matter." https://learn.microsoft.com/en-us/ef/core/modeling/ ; and "`EntityTypeConfigurationAttribute` types will not be automatically discovered in an assembly."
- So EF's registry is per context type, not process-global; assembly scan is opt-in and documented as order-undefined.
### Marten
- Storage is a table and upsert function per document type, alias `mt_doc_<alias>`; alias collisions are resolved with `DocumentAlias`. https://martendb.io/schema/storage (WebFetch). Hierarchies share one table via `AddSubClass<T>()`; subclass docs are "stored as the parent type" in the identity map. https://www.martendb.io/documents/hierarchies (WebFetch). Schema is configured in `StoreOptions` per store, not a process-global.
### Java ServiceLoader
- "Service providers are loaded and instantiated lazily, that is, on demand." Providers on the class path load in the order `getResources` finds config files; "The ordering of modules in a module layer is not defined." "Instances of this class are not safe for use by multiple concurrent threads." Caches cleared by `reload`. https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ServiceLoader.html (WebFetch). Visibility is class-loader dependent ("must be visible to the class loader").
## C. Q2. Write/overwrite decisions against an existing record of another type
- RavenDB: REST put docs, request body section: updates must "include its collection name in the metadata" and "A document's collection cannot be modified." Server error code for mismatch: the first fetch summary said HTTP 500; not confirmed on a second fetch. https://docs.ravendb.net/7.1/client-api/rest-api/document-commands/put-documents (WebFetch twice). Ayende, RavenDB 4.0 storage: document key namespace is global, same key cannot exist in two collections (summary). https://ayende.com/blog/173954/the-design-of-ravendb-4-0-physically-segregating-collections The check is against the collection named in the write (derived from the writer's type by the client), not a registry; the "client maps entity type to collection" part is inference from the search summary, not a quoted source. `PutDocumentCommand` page: a missing collection means `@empty`; concurrency via `changeVector`, null "to skip the check". https://docs.ravendb.net/client-api/commands/documents/put
- Marten: per-type tables, so the same id under two types are two rows in two tables; no cross-type check. Same id across subclasses of one hierarchy table: not covered by docs fetched; unverified. Marten docs: upsert is used so users "don't have to check whether a document is new or existing" (search summary of https://www.martendb.io/documents/storing, not opened).
- MongoDB: the server has no type; `_id` unique per collection; the C# driver writes a `_t` discriminator from the written object's class map. Whether the driver checks an existing document's `_t` on replace: not researched; nothing found stating it does.
- Couchbase: key unique within a collection; upsert replaces; no server-side type field enforcement; guidance is one type per collection. https://docs.couchbase.com/server/7.0/learn/data/scopes-and-collections.html (search summary, not opened).
- Orleans: grain identity is type plus key ("type/key"), so same key under two grain classes is two identities, not a conflict. https://learn.microsoft.com/en-ca/%20dotnet/orleans/grains/grain-identity (search summary, not opened). How storage providers key state: not found.
- Result of this pass: no store found in which a process-global type registry decides whether a write may replace an existing record of another type. Found: collection/type fixed by metadata of the writer (RavenDB), type as part of the key (Orleans, Marten tables), or no check (Couchbase upsert). This is a negative search result, not proof of absence. Not searched: Cosmos DB, DynamoDB, EF Core TPH update of an existing row with another discriminator, Realm, LiteDB, Akka.Persistence.
## D. Q3. Guidance on ambient/global registries and determinism
- Mark Seemann (ploeh, 2019): after discussion with co-author van Deursen, "Ambient Context really is an anti-pattern." Reasons given: he has never used it and superior alternatives exist; he says the claim is unproven. https://blog.ploeh.dk/2019/01/21/some-thoughts-on-anti-patterns/ (WebFetch). Examples he cites as found in the BCL (search summary, not verified on page): `Thread.CurrentPrincipal`, `CultureInfo.CurrentCulture`, `HttpContext.Current`. DI book 2nd ed. chapter 5 lists it among DI anti-patterns: https://www.oreilly.com/library/view/dependency-injection-principles/9781617294730/OEBPS/Text/c05.xhtml (not opened).
- MessagePack README (above) is itself a library-author statement on global mutable defaults. MongoDB CSHARP-4424 (above) states the shared-library init-order problem. EF Core docs (above) state assembly-scan order is undefined.
- No .NET guidance specifically about AppDomain-wide/assembly-scan registries and write decisions found.
## E. Not verified / gaps
- protobuf-net lazy registration, freezing and bug history.
- Why System.Text.Json made Default immutable; no maintainer statement found.
- Json.NET DefaultSettings guidance.
- RavenDB exact error text and HTTP status; whether the stored document is unchanged after refusal.
- Marten same-id-across-subclasses behaviour.
- MongoDB driver behaviour on replacing a document whose `_t` differs.
- Several items above rest on WebSearch summaries I did not open (marked "summary").

View file

@ -0,0 +1,90 @@
# Prior art: structural versus name-inclusive schema ids
Serves the store-identity question raised 2026-10-27 in campaign `variants` (operator: member and declared names vary with runtime naming conventions). Eyes pass, session `self-2026-10-27b-eureka`. Facts only; items marked "summary" rest on search summaries not opened.
# Prior art: structural vs name-inclusive schema identity (campaign variants)
Eyes pass, 2026-10-27. Facts and evidence pointers only. "(fetched)" = page opened via WebFetch (the answer is a small-model extraction of the page, not raw text). "(summary)" = WebSearch result summary, page not opened.
## 0. Local facts (read-only)
- `F:\Projects\CultLib\src\GameCult.Caching\CultCache.cs:717-718` schemaId = Sha256(BuildSemanticFingerprint(...)). `:735-761` fingerprint = `schemaName|schemaVersion` then per member ordered by Slot: `slot:MemberName:TypeName:ref|value:TargetSchemaName:one|many`. `contentHash` (`:716`) is a separate Sha256 of canonical JSON (`:763+`).
- `TypeName` = `CultSchemaTypeNames.FromType`: CLR `Type.FullName` (namespace-qualified), generics as `Name<args>`, arrays `T[]` (`src\GameCult.Caching\CultSchemaTypeNames.cs:9-31`). So the fingerprint contains .NET type names as well as member names.
- Contract `Contracts\cultcache-persistence-format.md:257-284` lists what canonicalization must preserve: slots, types, required/default, nullability, collection semantics, enum constraints, encoding shape. Member names and declared schema name are not in that list. `:171-179` says the id is not derived from C#/Rust/TS type names and the same shape should give the same id "regardless of origin language". `:180-190` defines schemaId = "same semantic persistence shape?" vs contentHash = "exact same canonical schema document?". Example catalog entry (`:140-170`) carries optional `supersedesSchemaIds`, `migrationFamily`; its example canonicalSchema fields do carry `"name": "Name"` (PascalCase). Soft-migration reader flow `:286-345` (exact_match / soft_migrate / reject).
- Runtimes do not derive ids; they emulate C# names:
- Python: `packages\cultcache-py\src\cultcache_py\documents.py:326` `member_name=_pascal_case(field.name)`; `:516-517` splits on `_` and capitalises. `schema_id` defaults to `self.type` (`:218`); `compatible_schema_ids` field (`:188,242`).
- TypeScript: `packages\cultcache-ts\src\codec.ts:226-232` `pascalCase` (splits on non-alphanumerics), `:267` `memberName: pascalCase(field.name)`; throws if a codec has no C# type name (`:~262`).
- Rust: `packages\cultcache-rs\src\lib.rs:295,308` `serde(rename_all = "camelCase")` on envelope structs; `schema_id: Some(T::TYPE.to_string())` at `:2856`. I did not trace how Rust document types obtain a sha256 id.
- Python test pins a literal id from shared vectors: `packages\cultcache-py\tests\test_cultcache.py:808` (`tests\vectors\document-variants-c0`).
- C# also has `CompatibleSchemaIds` (`CultCache.cs:28,313,570`): a record whose persisted id is in the list is accepted (an alias-list mechanism; I saw only the grep lines, not the logic).
- Ruling `variants:ruling:wire-id-is-version` (eureka-state, InForce, ruled 2026-09-30, operator quote "Agreed on schema identity"): wire schema id = declared version string `<name>.vN`; every runtime stamps/resolves it exactly; a wire binding without `.v<digits>` is refused; "Store ids are unchanged." Cited source: document-variants-cut.md:552-555 (not opened).
## 1. How established formats define identity/compatibility, and whether names participate
Protocol Buffers (fetched https://protobuf.dev/programming-guides/proto3/ and /json/)
- Field number identifies the field in the binary wire; changing it = delete + add. Field names appear only in TextProto/JSON encodings; reserve deleted numbers and, for JSON/text, names. The proto3 page has no explicit safe/unsafe entry for renaming; names matter only for JSON/text.
- JSON mapping: names become lowerCamelCase unless `json_name` is set; "Parsers accept both the lowerCamelCase name" and the original proto name (/programming-guides/json/).
- Message type identity in `Any` is by name: default type URL `type.googleapis.com/<package>.<message>`, "a globally unique identifier" (proto3 page, #any). Type identity is nominal; compatibility is by field numbers.
Apache Avro (fetched https://avro.apache.org/docs/1.11.1/specification/)
- Parsing Canonical Form (PCF): STRIP keeps only attributes relevant to parsing (type, name, fields, symbols, items, values, size); doc and aliases dropped; FULLNAMES replaces short names with fullnames and drops namespace attributes. Textually equal PCF means "the same" for any reader. Names ARE in the canonical form (record and field names), as is field declaration order. Defaults are absent from the kept list (extractor's inference; spec text not confirmed).
- Fingerprints: CRC-64-AVRO in single-object encoding (marker C3 01 + 8-byte LE fingerprint); spec recommends at least a 64-bit Rabin, with SHA-256 / MD5 options for larger sets.
- Name rules: `[A-Za-z_][A-Za-z0-9_]*`, dot-separated namespaces, equality on case-sensitive fullname.
- Resolution: records match on same unqualified name; "Named types and fields may have aliases"; implementations "may optionally" use aliases to map writer to reader. So in Avro names participate in identity (fingerprint) and resolution; aliases are the rename mechanism and are excluded from the fingerprint.
Cap'n Proto (fetched https://capnproto.org/language.html)
- Files and types carry 64-bit IDs; `capnp id` generates them randomly. Default ID for a declaration without one = first 8 bytes of MD5 of (parent scope ID, declaration name). "You cannot change the name of a type that doesn't have an explicit ID"; symbolic names can change if ID/ordinals stay. Stated rationale for IDs: short, unambiguous, full names waste wire space. The page does not say why structural hashes were rejected; I found no Varda statement (search pointed at a mailing-list thread "Schema Refactoring and Unique IDs", Aug 2023, unopened).
- Encoding page (https://capnproto.org/encoding.html, fetched): adding fields is backward compatible because field positions depend only on lower-numbered fields; nothing on type identity.
FlatBuffers (fetched https://flatbuffers.dev/evolution/ and /schema/)
- Field positions by declaration order or explicit `id: n` (contiguous from 0, on all fields if used). Renaming "generally OK" because names are not in the binary. `file_identifier` is exactly 4 chars at bytes 4-7; docs frame it as a file-level check and recommend unions for distinguishing messages.
Thrift (fetched https://raw.githubusercontent.com/apache/thrift/master/doc/specs/thrift-binary-protocol.md)
- Binary protocol: field header = type + field id; "the field name is not encoded". (IDL page gave nothing on omitted ids.)
MessagePack-CSharp (fetched https://github.com/MessagePack-CSharp/MessagePack-CSharp, first 100k of 105k chars)
- `[Key(int)]` on all members => array encoding; string keys => map. Keys should be sequential from 0 (gaps cost nulls); do not reuse indexes of removed members; keep obsolete members until clients update. Union keys are ints, unique, serialized as [key, object]. No type identity in the payload.
Microsoft Bond (fetched https://microsoft.github.io/bond/manual/bond_cpp.html)
- Changing field ordinals breaks wire compat; no explicit versioning; Simple JSON matches by field name so a rename breaks it. No schema hash/GUID documented there. Bebop: wiki page empty (points to docs.bebop.sh, not opened).
Unison (fetched https://www.unison-lang.org/docs/the-big-idea/ and /docs/language-reference/unique-types; hash size from search summary)
- Definitions identified by hash of syntax tree; names are "separately stored metadata"; hash excludes names (512-bit SHA3 per search summary, unopened).
- Structural types: "Unison does not uniquely identify a type by its name"; same-shape types are the same type (Suit/Direction, four nullary constructors, interchangeable). `unique` types: compiler generates a UUID that feeds the hash, so identity becomes nominal. Which is the default keyword was not confirmed.
IPLD (fetched https://ipld.io/docs/schemas/)
- Matching is structural: "the names of types aren't expected to be found in the serialized data"; match is by structural outline. The page does not say schemas are themselves content-addressed. Versioning is left to developers; "Migrations" subpage not opened.
## 2. Failure modes of purely structural identity
- Unison documents it directly: structurally identical enums are one type unless declared `unique` (unison-lang.org/docs/language-reference/unique-types, fetched). Its mitigation is a per-declaration UUID mixed into the hash (identity re-nominalised).
- Language literature (summary, pages not opened): structural typing is prone to accidental conformance/type equivalence; nominal typing prevents it at the cost of rigidity (Wikipedia "Nominal type system"; arXiv 1007.2123 lecture notes; Erlang nominals doc https://www.erlang.org/doc/system/nominals.html says nominal types exist to prevent misuse of same-structure types). TypeScript "brand" = phantom discriminator field. No serialization-specific source found.
- Formats that avoid the collision by carrying a name or tag outside the shape: protobuf Any type URL (full name), Avro fullname in PCF, Cap'n Proto explicit IDs, Unison `unique`, MessagePack-CSharp union key ints, FlatBuffers file_identifier/unions.
- Not found: a documented incident of two same-shape serialized types colliding under a structural id (one search, nothing).
## 3. Failure modes of name-inclusive identity across languages
- Protobuf: default camelCase conversion is "always valid but not necessarily unique"; proto3 compiler rejects JSON-name conflicts; serialized duplicate keys parse differently by runtime (Edition Zero design doc, summary: https://flutter.googlesource.com/third_party/protobuf/+/HEAD/docs/design/editions/edition-zero-json-handling.md). Snake_case vs camelCase JSON mismatches in practice: https://github.com/fullstorydev/grpcurl/issues/105 (summary); https://github.com/open-telemetry/opentelemetry-proto/issues/476 (summary: spec contradicted itself on camelCase vs original names). Protobuf's mitigation: parsers accept both forms.
- Serde (fetched https://serde.rs/container-attrs.html): `rename_all` accepts lowercase, UPPERCASE, PascalCase, camelCase, snake_case; per-field `rename`.
- Avro restricts names to `[A-Za-z_][A-Za-z0-9_]*` with case-sensitive fullname equality, so snake_case and PascalCase spellings are distinct names; renames go through aliases, which PCF drops.
- Cap'n Proto: auto-derived IDs depend on the declaration name, so a rename without an explicit ID changes the ID.
- Bond Simple JSON: rename breaks compatibility (name matching).
- Local: Python/TS already convert to PascalCase to reproduce C#'s fingerprint (section 0); the fingerprint's TypeName is the CLR full name (`CultSchemaTypeNames.cs:30`) and TS throws when a codec lacks a C# type name.
- Registry normalisation (summary): Confluent Schema Registry `normalize.schemas=true` maps semantically-equal schemas to one id (https://www.confluent.io/blog/best-practices-for-confluent-schema-registry/); sources disagree on whether JSON Schema property order is normalised.
## 4. Migration precedents for changing identity derivation
- Git SHA-1 -> SHA-256 (fetched https://git-scm.com/docs/hash-function-transition): per-repo format marker (`extensions.objectFormat`, `compatObjectFormat`, `repositoryFormatVersion=1` makes old git refuse); bidirectional SHA-256<->SHA-1 mapping (pack idx v3, `loose-object-idx`); four transition modes (dark launch, early, late, post); conversion at the network boundary. It is a design doc, not a statement of what has shipped.
- IPFS CIDv0 -> CIDv1 (fetched https://docs.ipfs.tech/concepts/content-addressing/): v0 (46 chars, "Qm") and v1 (self-describing version/codec/multihash) coexist; `ipfs cid format -v 1` converts; v1->v0 only for dag-pb.
- Docker manifest schema 1 -> 2 (summary, mirror pages unopened): digests differ per manifest format; pull-by-digest gets no translation, pull-by-tag may be translated; repush converts. https://pulp.plan.io/issues/2099.
- Warpcast API hash v1 -> v2 (summary, https://warpcast.notion.site/Warpcast-API-Hash-Migration-Public-ccdd6de8ac604783b9596cfd623ddbac): two-week dual-hash period, then only v2.
- Cap'n Proto explicit-ID escape hatch for renamed/moved declarations; Avro aliases as the rename bridge in resolution; MessagePack-CSharp keep-obsolete-members (all fetched, above).
- Local: contract `supersedesSchemaIds` / `migrationFamily` (`cultcache-persistence-format.md:~140-170`) and C#/Py `CompatibleSchemaIds` exist as alias lists.
- Not found: Unison changing its hash derivation (one search, no docs); any serialization store that switched from name-inclusive to structural ids.
## Could not verify / not done
- Cap'n Proto designer's rationale against structural hashing (not in docs; mailing list unopened).
- Avro: whether PCF drops defaults (spec text) and any JIRA on it; spec page was summarised by the fetch model.
- Bebop, Thrift IDL id-omission rule, Unison default struct/unique keyword, IPLD Migrations page, Edition Zero doc (summary only), Confluent blog (summary only), nominal/structural papers (titles only).
- Rust runtime's sha256 derivation path; `document-variants-cut.md:552-555` not opened; `CompatibleSchemaIds` logic not read.
- eureka-state worked (whoami ok); only the one ruling was queried.

View file

@ -0,0 +1,28 @@
# Prior art: presence provider as a library, launcher as a consumer
Served: `cultlib-gaps:cut_spec:cut-cultnet-rs-presence-provider-a.r1` and the
Defaulted rulings `presence-provider-network-free-core`,
`presence-credentials-both-idunn-drivers` and `presence-provider-consumers-adopt`,
applying the operator's `gamecult-chat:ruling:presence-one-owner-library-launcher-consumer`.
Imagination pass, 2026-10-29, from recall of the cited manuals (not re-fetched).
- libsystemd `sd_notify(3)`: a service links a small library and reports its
own state (READY, STATUS, WATCHDOG); programs that cannot link it use
`systemd-notify(1)` or a wrapper. Same split as cultnet-rs provider plus
idunn-launch as the sidecar consumer.
- `sd_listen_fds_with_names(3)`: checks LISTEN_PID against the process, sets
close-on-exec on passed descriptors, finds them by name, and can unset the
LISTEN_* environment. This set is the descriptor rule the provider adopts.
- systemd watchdog (`WATCHDOG=1` within `WatchdogSec`): the model for the
warming heartbeat.
- Consul: TTL checks updated by the service through the client API, versus
script/HTTP checks the agent runs for services that cannot. In-process
versus sidecar again.
- Kubernetes: kubelet-run probes observe from outside; readiness gates are
reported by the app. Idunn's route challenge is the outside observer, the
signed presence the app's own report.
Existing copies compared: CodexConnector `src/idunn_health.rs` (two
descriptors, strict PID check), Idunn launcher (two descriptors, no PID check),
cultnet-ts `idunn-runtime-authority.ts` (descriptors by name, systemd private
PID case). The provider takes the strictest union.

View file

@ -0,0 +1,286 @@
# Analytic noise integration along warped rays: feasibility probe
Campaign `cultmath-tapes`, Imagination probe, 2026-10-03. This is a measurement,
not a spec. The tape target stays parked under ruling
`cultmath-tapes:ruling:tape-target-unparks-when-asura-stable`.
**Verdict.** Exact integration of the noise does not beat sampling for optical
depth on either shipped field.
- The exact line integral of CultMath `snoise` is cheap and correct. Affine warp
spans are long, at 100 to 300 world units.
- The exact noise integral is not the optical depth, because both densities are
nonlinear in the noise.
- The void's wall profile is `pow(·, 0.40)` with two clamps.
- Aetheria's fog is clamped at zero and goes opaque within a few world units.
- Given the *exact* span mean of the noise for free, the resulting transmittance
is still 3.6x worse than the shipped march on the void and 22x worse on the
wells.
- Exact integration wins only on the linear functional `∫ n dt` of octaves the
march undersamples. That gain does not survive the density's nonlinearity.
- Aetheria's triangle noise is integrable exactly as piecewise linear, but costs
3x to 22x the march, and its shipped form does not meet the preconditions.
## Scenarios and method
- **Void:** the shipped void (`NoiseBoundTests.VoidField.Shipped()` at
`hands/cultmath-interval-ops` 2c8585bd).
- Rh 198, S 50, Rc 248, K 1/30, A 20, F0 0.01 (cells of 100 and 25 world), warp
D 10.
- The shipped orbiting camera, N = 8 tiles at f = 935, one jittered pixel ray per
tile.
- **Wells:** the test's Aetheria wells at warp D = 60 (`FogField.Wells(60)`).
- F0 1/414.2167, A 20, B 10, extinction 0.5, flow frequency 1/512, period 8.
- Like the test, this uses the snoise field shape (r1's), not Aetheria's own
triangle noise. Triangle noise is measured separately below, on the same rays.
- **Rays:** 200 per scenario, and 120 for the triangle basis.
- The wells give 147 rays with an active interval; the rest look up and miss
the fog.
- The active interval is where the envelope leaves the density to the noise.
It is cut where the true optical depth reaches 6.
- Active length per ray: void median 140 world (p10 114, p90 193); wells median
268 (p10 137, p90 623).
- **March steps:** the void's march step (LodStep, 12.5 in z) is a median 14.9
world along the ray. The wells' quadratic grid gives a median of 9.1 in the
active region.
- **Warp:** `w(p) = flow(p) · shift`, so the affine error scales exactly with
`|shift|`.
- Spans are grown at the worst phase shift, Period / 2. A span is therefore
valid at every frame's phase, for any camera that crosses the same world
segment.
- Each span is anchored at p0 with J·d from a central difference. It grows in
1-unit (void) or 2-unit (wells) steps until `|w − (w0 + J·d·Δ)| > τ`.
- **Exact simplex integral:** CultMath `snoise(float3)` has kernel radius² 0.5,
so its support never leaves the incident simplices.
- Along a line it is therefore a sum over the lattice points within √0.5 of the
line. Each point contributes `∫ (0.5 − |x|²)⁴ (g·x) dt` on its chord, a
closed-form degree-9 polynomial.
- Validation:
- The lattice sum matches `snoise` within 2e-4 (float32 rounding at
coordinates near 300).
- The segment integral matches a 5e-4-step quadrature within 2.5e-5 per
unit length.
- **Run:** probe code was in a scratch clone (probe-clone@91c8cbf1, never merged)
and ran on Yggdrasil through the verify stopgap, in a dotnet 10 container with
4 CPUs. Timings are single-threaded C# on that host and are relative.
## 1. Affine-valid span length (ray length, world units)
| τ (× coverage cell) | Void p10 / median / p90 | Wells p10 / median / p90 |
|---|---|---|
| 0.05 | 94 / 129 / 211 | 71 / 103 / 169 |
| 0.10 | 141 / 197 / 324 | 108 / 151 / 253 |
| 0.25 | 263 / 420 / 1244 | 184 / 275 / 715 |
| 0.05 × detail cell | 44 / 63 / 97 | 34 / 47 / 75 |
- One affine span at τ = 0.1 covers about 13 void march steps and about 16 wells
march steps.
- The void shell is shorter than one span. The tiled spans are clipped by the
active interval, at a median of 27 world and 2 spans per ray.
- On the wells, the tiled spans run a median of 128 world, with 2 per ray (p90 4).
- The tangent anchor is the worst affine choice. A secant or Chebyshev fit over
the span cuts the error bound by 2x to 8x, so the spans could be about 1.4x to
2.8x longer at the same τ. That was not measured.
## 2. Simplex cells per span, per octave
Octave `xm` is the coverage octave's frequency times m, warped like the coverage
octave. The shipped detail octave is x4 and is *unwarped* in both fields, so it
needs no affine span at all.
Counts are for τ = 0.1 tiled spans with both warp phases, given as the median and
then the p90. The rows are simplex cells crossed, then lattice points touched,
which is the exact work.
| Octave | Void cells | Void lattice points | Wells cells | Wells lattice points |
|---|---|---|---|---|
| x1 | 6 / 16 | 10 / 16 | 4 / 8 | 8 / 10 |
| x2 | 8 / 27 | 10 / 26 | 8 / 12 | 10 / 14 |
| x4 | 13 / 52 | 14 / 44 | 12 / 20 | 14 / 20 |
| x8 | 20 / 100 | 20 / 80 | 22 / 38 | 22 / 34 |
| x16 | 40 / 201 | 35 / 154 | 44 / 72 | 38 / 60 |
| x32 | 75 / 396 | 62 / 302 | 86 / 144 | 70 / 112 |
| x64 | 151 / 793 | 118 / 592 | 172 / 287 | 132 / 218 |
- On the same spans, the march takes a median of 2 samples per span on the void
(p90 10) and 12 on the wells (p90 25).
- A straight line touches 3.44 lattice points per noise unit.
## 3. Cost: exact against the march
- **Measured cost.**
- One snoise is 530 to 610 ns in C# on this host.
- One lattice-point integral (hash, gradient, closed-form polynomial) is
85 ns, or 0.16 snoise.
- With this probe's naive box-scan enumeration it is 1.24 snoise. That is an
upper bound; a simplex walk visits only crossed cells.
- **How the table counts.** Exact is lattice points × 0.16 + 1 snoise per span
for the Jacobian. March is 1 snoise per sample. Both sides carry the two warp
phases.
- **Tolerance.** τ is 0.1 × *that octave's own* cell, so the affine error is
comparable across octaves.
- **Columns.** The two error columns are RMS errors of the active-length mean
of one warped octave. The last error column is what replacing the octave by its
expectation of 0 costs.
| Octave | Void exact/march cost | Void err exact / march / expectation | Wells exact/march cost | Wells err exact / march / expectation |
|---|---|---|---|---|
| x1 | 0.36 | 1.1e-2 / 1.6e-3 / 0.21 | 0.11 | 1.9e-2 / 9e-5 / 0.29 |
| x2 | 0.46 | 1.0e-2 / 4.7e-3 / 0.13 | 0.15 | 1.5e-2 / 2e-4 / 0.21 |
| x4 | 0.68 | 9.0e-3 / 4.0e-2 / 0.10 | 0.23 | 1.1e-2 / 5e-4 / 0.18 |
| x8 | **1.03** | 5.8e-3 / 8.0e-2 / 0.067 | 0.36 | 7.2e-3 / 1.1e-3 / 0.10 |
| x16 | 1.64 | 4.0e-3 / 9.0e-2 / 0.050 | 0.56 | 6.4e-3 / 9.4e-3 / 0.088 |
| x32 | 2.89 | 2.8e-3 / 0.105 / 0.036 | 0.87 | 4.7e-3 / 3.3e-2 / 0.057 |
| x64 | 5.26 | 2.0e-3 / 0.095 / 0.025 | **1.44** | 3.2e-3 / 5.3e-2 / 0.040 |
- **Where exact stops paying on cost:**
- At the measured kernel cost: x8 on the void and between x32 and x64 on the
wells.
- At the brief's model of one snoise per cell crossed: about x1 to x2 on the
void and x8 on the wells, from the per-span table in the probe log.
- **Exact against the march on accuracy:**
- At the shipped step the march is *more* accurate for octaves it oversamples:
void x1 to x2, wells x1 to x8. The exact path's error floor is the affine warp
error, about 1e-2 at τ = 0.1 per cell, and it scales with τ.
- Exact is more accurate at about 0.7x the cost where the march undersamples:
void x4 to x8, wells x16 to x32.
- Above about x16 on the void, the undersampled march is worse than simply
using the expectation (0.095 against 0.025 at x64). That is a band-limiting
result, which the footprint octave weights already exploit. It is not an
exact-integration result.
## 4. High-octave variance over a span
- **Span-mean statistics.** Over straight lines of noise length ℓ, the std of
the snoise span mean, against a point std of 0.358, is:
- 0.234 at ℓ = 1;
- 0.170 at ℓ = 2;
- 0.121 at ℓ = 4;
- 0.087 at ℓ = 8;
- 0.061 at ℓ = 16;
- 0.031 at ℓ = 64.
This is about ℓ^-0.5: the high octaves average out like a random field, not
like a periodic one.
- **Detail octave replaced by its expectation of 0,** on τ = 0.1 spans, measured
as |ΔOD| per span:
- Void: median 0.000 and p90 0.091. Relative to the span's OD (OD > 0.01) the
median is 2.5% and the p90 is 7.6%. The mean detail displacement is a median
1.0 world (p90 3.6) against the ramp width S = 50.
- Wells: median 0.11 and p90 5.7 optical depths. The relative median is 72%.
- **Why the wells are so bad.** Normalised to the clamp, the density moves
A·fade/B = 2 per unit of noise, and with extinction 0.5 that is 1 optical depth
per world unit per unit of noise. The mean detail displacement of 1.5 world
(p90 3.8) is 15% to 38% of the floor blend B = 10. The wells' optical depth is
set by where the clamp boundary lies, and that is exactly what the expectation
erases.
- So the expectation is adequate for the void's detail octave and is not
adequate for the wells.
## 5. The clamp and other nonlinearity
- **Void.** 50.0% of τ = 0.1 spans cross a regime boundary: 0 (d' ≤ Rh), the
wall ramp, or K (d' ≥ Rc). 45.6% of the active length lies in the ramp, where
the density is `K max(0, 1 − 1.5 (1 − (d'/Rc)²)^0.40)`. That is not a
polynomial in n, so exact ∫n does not give ∫ρ there. Outside the ramp the
density is constant and needs no noise.
- **Wells.** 42.3% of spans cross the clamp. Only 9.8% of the active length is
unclamped, so most of the length where the noise "matters" is length where the
noise only decides whether the density is zero.
- **The decisive check: transmittance error.** This is the sup over the ray of
|T_est − T_true|, with the truth taken on a 0.5- or 1-unit grid.
| Estimator | Void median / p90 | Wells median / p90 |
|---|---|---|
| Shipped march | 0.020 / 0.038 | 0.033 / 0.155 |
| March at 4x density | 0.006 / 0.009 | 0.030 / 0.049 |
| Density of the *exact* span-mean n (τ = 0.1 spans) | 0.072 / 0.125 | 0.719 / 0.997 |
The last row is an upper bound on what any exact-mean method can deliver
without also modelling the distribution inside the span. It is the
Heitz-style correction the survey describes. It loses to the march on both
fields.
## Triangle basis (operator's addition)
The triangle noise is `triNoise3d` in Aetheria `Assets/Shaders/Volumetric.cginc`
(origin/master f1dee184).
- `tri(x) = |frac(x) − 0.5|`.
- It runs two Nimitz-style iterations. Each iterates `p += tri3(bp·2)`, scales
`bp` by 1.8 and `p` by 1.2, and adds `tri(p.z + tri(p.x + tri(p.y)))/z`.
- It makes 18 tri calls per evaluation.
As shipped, `density()` uses it as follows:
- `pow(triNoise3d(·), exponent)`, with Noise.Exponent −0.25 multiplied by a
slope `flatness` read from a texture (NOISE_SLOPE is on).
- Two phases, warped by `globalFlow`, which is
`Tri3D = cross(normalize(tri3(P)), normalize(tri3(1.618P))) × 15`.
- The fine octave at ×8 is evaluated at a `pos` whose y the coarse octave has
already displaced.
- Amplitude −36.17.
Self's reading, tested:
- **"The warped ray is an exact polyline."** This holds for the warp *inside*
`triNoise3d`. It does not hold for Aetheria's flow: `normalize` and `cross`
make the flow piecewise rational. With the normalize dropped, the cross product
of two piecewise-linear vectors is piecewise quadratic. tri composed with a
piecewise polynomial stays piecewise polynomial of the same degree, with
breakpoints at quadratic roots, so that variant is still closed-form.
Counted along the real flow at the worst shift, the kinks run 15% to 30% above
the straight-ray counts, but the pieces between them are curved.
- **"The clamp roots are linear."** This holds only if the exponent is 1. With
−0.25·flatness, each piece is `(a + bt)^e`. One such piece has a closed-form
integral, but the clamp's root on a sum of two powered phases plus the
smoothstep fade has none.
- **"Whole periods give the exact mean."** This holds for one triangle wave at
whole periods, where the mean is exactly 0.25. It fails for the composed form,
which has no period along a general line; the scales 2, 1.8 and 1.2 and the
nested warp are incommensurate.
- Measured on lines, the span-mean std of `triNoise3d` is 0.029 at ℓ = 1,
0.010 at ℓ = 8 and 0.0042 at ℓ = 64, against a point std of 0.083. That is
about ℓ^-0.47, the same random-field decay as simplex.
- Its bias against the global mean of 0.1995 is within ±0.0015, so the
expectation is unbiased. It is not exact.
- **The cost decides it.** The exact piecewise-linear walk is verified, matching
a 1e-4 quadrature within 3.4e-8. But the nest has 77.7 linear pieces per noise
unit of straight line, against 3.44 lattice points for simplex.
- One piece costs 1.38 point evaluations with a derivative carried.
- At Aetheria's scale, on the wells rays, the pieces per world unit and the
exact cost relative to the march are:
| Octave | Pieces per world unit | Exact cost / march |
|---|---|---|
| x1 | 0.20 | 2.8x |
| x2 | 0.38 | 5.5x |
| x4 | 0.76 | 11x |
| x8 (shipped fine) | 1.5 | 22x |
| x64 | 12 | 176x |
- On the void rays it is 4.1x at x1 and 29x at x8.
- At the same time the march's RMS error on the span mean is already small:
2.4e-3 at x1 and 1.7e-2 at x8.
## Per-basis verdict
| Basis | Exact line integral | Under the shipped warp | Optical depth | Verdict |
|---|---|---|---|---|
| Simplex (CultMath `snoise`) | Closed form, 0.16 snoise per lattice point, 3.44 points per noise unit; validated | Affine spans of 100 to 300 world at τ = 0.1 cell; error floor about 1e-2 | Not delivered: the void's pow profile and the wells' clamp make ρ nonlinear in n; the exact-mean transmittance loses 3.6x and 22x | **Viable kernel, wrong target.** Pays only if the density were affine in n over the span, or as a control variate for a march (survey §4, Kettunen 2021) |
| Triangle (`triNoise3d`) | Closed form per linear piece; validated | Not piecewise linear under Aetheria's normalized-cross flow or the −0.25 power; made linear by exponent 1 and a non-normalized flow | Same nonlinearity, plus 78 pieces per noise unit | **Loses.** 3x to 22x the march even where its preconditions hold |
| Gabor kernels (survey §1.2, not probed) | Closed form for any affine-transformed kernel (complex erf, at most 16 terms) | The affine warp spans above transfer directly, with each span a whitening transform | Same density-nonlinearity limit | **Candidate smooth basis for large structure**, to be measured only if the density is first made linear in the noise over a span |
## What would change the verdict
- **A density that is affine in the noise inside a span.** For example, the
void's ramp replaced by a linear ramp in d', with clamp crossings bracketed by
the interval bound. Exact simplex integration would then deliver optical depth
directly in the 54% of void length that is outside the ramp's clamps. As
measured, that 54% is constant density and needs no noise at all.
- **Exact integration as a control variate,** not a replacement. The exact mean
is cheap and unbiased for ∫n, and the march corrects the nonlinear residual.
That is the Kettunen construction. It was not measured here.
- **Wider skipping.** The operator's first idea, skipping space outside the warp
range, is already what the interval envelope does, and the probe's active
intervals are its output.
- On the void the noise matters over only about 140 world per ray.
- On the wells, opacity ends the ray within a few world units of entering the
unclamped region.

View file

@ -0,0 +1,425 @@
# Analytic Noise Integration Along Warped Rays: Prior Art Survey
This is the facts file for an idea the operator raised on 2026-10-03:
> "I would be happy if we could just skip the space we know has no meaningful structure
> because it's outside of the noise warp range. But what you said about affine arithmetic
> makes me wonder if we can somehow, even within the distorted domain, figure out a series
> of optimal (in terms of extracting structure over multiple frames) spans for which we can
> analytically integrate the noise gradients over those ray segments."
Self's sketch, as given to this pass:
- Simplex noise along a straight ray is piecewise polynomial in t, so its line integral
is closed-form per crossed cell.
- A domain warp p + w(p) is nearly affine over a short span, with an affine-arithmetic
error bound, so within that span the warped ray is a straight line in noise space.
- Low octaves are integrated exactly. High octaves are replaced by their expectation,
with a variance bound.
- Clamped density max(0, n - c) is integrated by root-finding per piece.
- Spans are cached and refined across frames.
Eyes pass, 2026-10-03. This file gives facts and evidence pointers only. It makes no
recommendation.
Verification marks:
- **[fetched]** The claim was read in the cited source's full text during this pass.
PDFs were converted with `pdftotext -layout`.
- **[search]** The claim came from a search-result summary or abstract, not a full read.
- **[derivation]** The claim is elementary mathematics worked out in this pass from a
fetched definition. It is not a claim made by any cited source.
- **[not found]** The pass searched for this kind of work and found nothing.
## 0. Premises of the sketch, checked against definitions
- **[fetched] + [derivation]** Simplex noise kernel degree. Gustavson, *Simplex noise
demystified* (2005), gives the 3D reference code. Each of the four simplex corners
contributes `t^4 * dot(g, d)` with `t = 0.6 - |d|^2`, and the contribution is zero
when `t < 0`. On a straight ray `d` is affine in the ray parameter, so `|d|^2` is
quadratic and each corner term is a polynomial of degree 8 + 1 = 9. The pieces have
two kinds of breakpoint: the simplex faces the ray crosses (where the corner set
changes), and the entry and exit of each corner's support sphere (`|d|^2 = 0.6`).
Both kinds are roots of polynomials of degree 2 or lower in the ray parameter, so the
breakpoints are closed-form. With the 0.6 radius, the support extends past the
simplex, and the reference code truncates it at the simplex boundary. The resulting
small discontinuity across faces is a known property of that constant. It does not
change the piecewise-polynomial structure.
- **[derivation]** Classic Perlin noise (Perlin 2002 quintic fade `6t^5 - 15t^4 + 10t^3`,
read in Gustavson's text **[fetched]**) is, within one lattice cube along a straight
ray, a polynomial of degree 3 x 5 + 1 = 16. Its breakpoints are the cube faces.
- **[fetched]** Piece count grows with frequency. Balint (CESCG 2019, section 2)
states that many noise functions are piecewise integrable, but the number of pieces
is linear in the noise frequency. Their cost is therefore similar to numerical
integration. Balint designed cosine noise to escape this limit (section 1.1 below).
- **[derivation]** For the clamp `max(0, n - c)` on a degree-9 or degree-16 piece,
the roots of `n - c` are needed. Polynomials of degree 5 or more have no general
closed-form roots (Abel-Ruffini), so the clamp needs numerical root isolation per
piece, for example Sturm sequences, Descartes' rule or Bernstein-basis subdivision.
The integral between the isolated roots is closed-form.
## 1. Closed-form line integrals of noise, and analytic optical depth
### 1.1 Cosine noise: Balint, *Closed Form Transmittance in Heterogeneous Media Using Cosine Noise*, CESCG 2019 (non-peer-reviewed)
- **[fetched]** It computes noise defined as an offset plus a sum of cosines of
`x . S_i`. The paper uses 24 quasirandom vectors S_i, with Sobol directions on the
sphere and lengths in [0.8, 1.2], to look roughly like Perlin noise. Fractal octaves
are further cosine sets. Its line integral is a sum of sines, so the optical depth
over any segment is exact. The paper says this takes two noise samples, one at each
segment end. It also gives closed-form distance sampling, by inverting the integral
with binary search.
- **[fetched]** Cost: two noise-sized evaluations per segment, independent of octave
count. Table 1 reports transmittance evaluation at 179 ms closed-form against
1250 ms for delta tracking, about 7x faster. A real-time demo with 16 samples ran
at 80 fps at 675 x 1200 (RTX 2080 Ti).
- **[fetched]** Error: exact, with no error term, for the bare sum. The limits are
stated in section 6. Multiplying by a shape function, or composing with a shaping
function, generally destroys the elementary antiderivative. The workarounds
recover exactness only piecewise: constant multipliers between bounding isosurfaces,
or a cubic polynomial interpolant of the multiplier along the ray, since the product
of a polynomial and cosines has an elementary antiderivative. Negative densities from
a low offset are handled by clamping samples and integrals, which is not an exact
`max(0, .)` integral. Domain warp is not discussed.
### 1.2 Gabor kernels: Condor, Hermann, Yurtsever, Didyk, *Gabor Fields: Orientation-Selective Level-of-Detail for Volume Rendering*, ACM TOG 45(4) art. 62, 2026 (arXiv 2602.05081)
- **[fetched]** It computes closed-form definite line integrals of anisotropic 3D Gabor
kernels along an arbitrary ray segment (eq. 9). Each kernel is whitened into its
canonical space with a ZCA matrix, so any covariance, that is any affine-transformed
kernel, uses the same formula. The segment integral is the real part of a
complex-erf (Faddeeva) difference.
- **[fetched]** Cost: the complex erf is evaluated by a truncated Taylor series with
early exit, capped at 16 terms. Segments shorter than 1e-4 in whitened units fall back
to a single midpoint evaluation. Kernels are spatially clamped with an adaptive extent
that shrinks with frequency (eq. 15). The paper also renders procedural clouds as
Gaussians modulated by Gabor noise.
- **[fetched]** Error: each segment integral is exact apart from the erf truncation.
Orientation-selective masking of kernels whose line integral is near zero trades a
small error, or variance under stochastic selection, for speed. The paper reports
about 1.78x to 1.96x speedups. Per segment the density-integral estimator is
unbiased, but transmittance is biased by Jensen's inequality. Eq. 19 gives the
second-order bias as `0.5 * Var(tau) * exp(-E[tau])`.
### 1.3 Analytically integrable media, older
- **[search]** Iser, *Real-time Light Transport in Analytically Integrable
Quasi-heterogeneous Media*, CESCG 2018. Balint cites it **[fetched]** as using only
exponential and spherical density functions.
- **[fetched via Gabor Fields related work]** The closed-form Gaussian-primitive
transmittance line (Knoll et al. 2021; Condor et al. 2025, *Don't Splat your
Gaussians*) is the base that Gabor Fields extends.
### 1.4 Piecewise-polynomial segment integration in scientific volume rendering
- **[fetched]** Engel, Kraus, Ertl, *High-Quality Pre-Integrated Volume Rendering Using
Hardware-Accelerated Pixel Shading*, Graphics Hardware 2001. The scalar field along
the ray is assumed piecewise linear between samples. The volume rendering integral of
each linear segment, through an arbitrary nonlinear transfer function, is then one
lookup in a table indexed by front scalar, back scalar and segment length. Cost: one
texture lookup per segment, plus recomputing the table when the transfer function
changes. Error: exact for the piecewise-linear model. The model error is the
scalar field's deviation from linear between samples.
- **[search]** Williams, Max, Stein, *A High Accuracy Volume Renderer for Unstructured
Data*, IEEE TVCG 4(1), 1998. Exact integration through tetrahedra with linearly
varying scalar and piecewise-linear transfer functions.
### 1.5 Polynomial noise along a ray
- **[not found]** No published closed-form segment integral of Perlin or simplex noise
was found, as a method or a code library. Balint's remark in section 0 is the only
published statement found on the piecewise approach, and it dismisses it on cost
grounds.
## 2. Noise designed for analytic filtering or integration
- **[fetched]** Lagae, Lefebvre, Drettakis, Dutré, *Procedural Noise using Sparse Gabor
Convolution*, ACM TOG 28(3), 2009. It computes a sparse-convolution noise of Poisson
impulses with Gabor kernels. Each kernel is a Gaussian in frequency, so filtering with
a Gaussian pixel filter multiplies Gaussians, which gives an analytically filtered
kernel. Cost: the paper typically uses 100 impulses per kernel, or 25 to 50 at lower
densities. Table 2 (GTX 280) reports, at 20 impulses per cell, 290 fps for 2D noise,
111 fps for unfiltered surface noise and 50 fps for filtered surface noise. Error:
the filtering is exact for a Gaussian filter. The noise statistics converge with
impulse density.
- **[fetched]** Lagae, Drettakis, *Filtering Solid Gabor Noise*, ACM TOG 30(4), 2011. It
slices solid Gabor noise, filters each slice of each Gabor kernel, and gets a
generalisation of the projection-slice theorem. It also states that wavelet noise
integrates solid noise perpendicular to the surface. That is a line integral of noise
along the normal, done in the basis.
- **[search]** Cook, DeRose, *Wavelet Noise*, ACM TOG 24(3), 2005. A quadratic B-spline
noise band, which is almost band-limited, built from a precomputed coefficient tile.
Its 3D-to-surface projection integrates the noise along the normal. The original PDF
was not retrievable in this pass. Pixar and ACM both returned HTML.
- **[fetched, survey]** Lagae et al., *A Survey of Procedural Noise Functions*, CGF 2010.
- Lewis's sparse convolution noise has a power spectrum equal to the kernel's,
scaled.
- Frequency clamping (Norton, Rockwood, Skolmoski 1982) cancels bands above the filter
rate. It works only for narrowly band-pass noise, and Perlin noise is only weakly
band-pass.
- Nonlinear functions of noise, such as thresholds and colour maps, are not filtered
by filtering the noise. That is unsolved in general.
- Heidrich, Slusallek, Seidel 1998 (below) computes an area average of a procedural
shader with an error bound, using affine arithmetic.
- Perlin noise's amplitude distribution is visibly non-Gaussian, because it is zero at
every lattice point and has few gradients. The other noises surveyed are
approximately Gaussian.
- **[fetched, via the survey above]** "Filtering procedural textures" lineage:
- Rhoades et al. 1992 filters the colour table over `[N - e, N + e]`.
- Hart et al. 1999 sets `e` from the local noise gradient.
- Lagae 2009 sets the range from the analytically known loss of variance.
## 3. Affine arithmetic, reduced AA, Taylor models, and bounds on warps
### 3.1 Gamito and Maddock
- **[fetched]** Gamito, Maddock, *Ray Casting Implicit Fractal Surfaces with Reduced
Affine Arithmetic*, The Visual Computer 23:155-165, 2007. The full method is read in
Gamito's PhD thesis (Sheffield), sections 5.3 to 5.4.
- **Computes:** a root of `g(t) = 0` along a ray, by subdividing intervals of t. Each
interval is tested with an inclusion `G(T)` computed in reduced AA (Messine's AF1).
The form is `t0 + t1 e1 + t2 e2`: e1 is the shared ray-parameter symbol, and e2
absorbs every new nonlinearity by condensation after each operation.
- **Noise in RAA:** gradient-noise kernels lose no correlation under RAA, because the
three multiplications are local to one kernel and the cubic Hermite fade is done by
a single Chebyshev affine approximation. The same holds for sparse convolution
noise. Cellular noise loses accuracy, because `min` needs cancellation between
condensed terms, and the F2 and F3 kernels were not implementable.
- **Interval optimisation:** the parallelogram `g1/t1` slope plus or minus `g2`
shrinks `[ta, tb]` before subdividing (eq. 5.27).
- **Cost:** Table 5.2, gradient-noise hypertexture, average evaluations per ray and
time:
| Method | Evaluations per ray | Time |
|---|---|---|
| IA | 116.57 | 7m35s |
| Standard AA | 61.69 | 40m41s |
| RAA | 61.69 | 6m43s |
| RAA + interval optimisation | 46.39 | 5m04s |
RAA costs three numbers per quantity against two for IA. Standard AA is tight
but slow because its error-symbol lists grow.
- **Error bound:** conservative inclusion, as proven for AA by Comba and Stolfi 1993.
Rounding error is ignored.
- **[fetched]** Gamito thesis, chapter 9, *Surface Deformation with Flow Mapping*.
- Domain deformation `f(d(x))` with `d` defined by streamlines, that is by an ODE.
This is the case closest to a domain warp in this survey.
- Gamito states that a robust and tight AA estimate of the deformed ray `s(t)` was not
obtainable through the numerical ODE solution, so robustness was given up.
- The replacement test uses Lipschitz unbounding spheres at `s(ta)` and `s(tb)`. A
span is skipped if `|s(tb) - s(ta)| < min(ra, rb)` (eq. 9.23). A sharp turn of the
deformed ray between the endpoints can make this test wrong.
- He names truncated Taylor-series range estimation (Nedialkov et al. 2004) as the
possible fix, at higher cost and complexity.
### 3.2 Other affine and interval arithmetic sources
- **[fetched, bibliographic]** Comba, Stolfi, *Affine Arithmetic and its Applications to
Computer Graphics*, SIBGRAPI 1993. de Figueiredo, Stolfi, *Affine Arithmetic: Concepts
and Applications*, Numerical Algorithms 37, 2004. Gamito cites both. AA keeps
first-order correlations through shared noise symbols. Each nonaffine operation adds
one symbol, whose coefficient bounds the linearisation error. Chebyshev or min-range
affine approximations give that coefficient for unary functions.
- **[search]** Knoll, Hijazi, Kensler, Schott, Hansen, Hagen, *Fast Ray Tracing of
Arbitrary Implicit Surfaces with Interval and Affine Arithmetic*, CGF 28(1), 2009.
Inclusion-preserving reduced AA, SIMD CPU at interactive rates, and a stackless GPU
version.
- **[fetched, abstract]** Fryazinov, Pasko, Comninos, *Fast Reliable Ray-tracing of
Procedurally Defined Implicit Surfaces Using Revised Affine Arithmetic*, Bournemouth
TR-NCCA-2009-04. Revised AA, which also keeps one accumulated error symbol, is
reported fastest among the IA and AA variants compared, including on pseudo-random
procedural objects.
- **[fetched]** Sharp, Jacobson, *Spelunking the Deep: Guaranteed Queries on General
Neural Implicit Surfaces via Range Analysis*, ACM TOG 41(4), 2022. This is AA through
long compositions of nonlinear layers, which is structurally like noise composed with
a warp. Table 1 compares the variants. Time is relative to one scalar evaluation,
length is the 1D region that can be certified, and raycast time is relative to the
best method:
| Variant | Time | Length | Raycast time |
|---|---|---|---|
| Interval | 2.4x | 0.011 | 34.3x |
| Affine-full | 95.3x | 0.821 | 8.4x |
| Affine-fixed (no new symbols kept, extra error condensed into one) | 4.7x | 0.306 | 1.0x |
The paper uses affine-fixed for ray casting and affine-full for 3D queries.
- **[fetched, survey quote of abstract]** Heidrich, Slusallek, Seidel, *Sampling of
Procedural Shaders using Affine Arithmetic*, ACM TOG 17(3):158-176, 1998. It computes
the average of a procedural shader over a finite area, with an AA error bound.
- **[fetched, project page]** Keeter, *Massively Parallel Rendering of Complex
Closed-Form Implicit Surfaces*, ACM TOG 39(4), 2020. Interval arithmetic over a
shallow, high-branching tile hierarchy does two things: it skips empty tiles, and it
prunes the expression tape per tile. One benchmark drops the expression size by about
two orders of magnitude. It requires only C0 continuity, so warps and blends that break
Lipschitz continuity are allowed.
### 3.3 Lipschitz bounds through warps
- **[fetched]** Galin, Guérin, Paris, Peytavie, *Segment Tracing Using Local Lipschitz
Bounds*, CGF 39(2), 2020. It computes a local Lipschitz bound over a ray segment
instead of a global one. Section 4.4 covers warps `f o w^-1`, and bounds the gradient
by the product of the field gradient bound over the transformed segment and the
Jacobian norm bound over the segment. Cost: fewer field queries than sphere tracing,
with no acceleration structure. Error: conservative, if the local bounds are
conservative.
- **[fetched]** Seyb, Jacobson, Nowrouzezahrai, Jarosz, *Non-linear Sphere Tracing for
Rendering Deformed Signed Distance Fields*, ACM TOG 38(6), 2019.
- **Computes:** the ray in deformed space as a curve in undeformed space, by
integrating an ODE whose derivative is the inverse-deformation Jacobian times the ray
direction. An initial value comes from rasterising a deformed hull mesh.
- **Error control:** step requirements for the curve are separated from those for the
SDF. Adaptive Runge-Kutta (RKF45 and DP54 evaluated) is used far from the surface,
and forward Euler near it, against an error tolerance.
- **Scope:** surfaces only. There is no density integration.
- **[fetched]** Moinet, Neyret, *Fast Sphere Tracing of Procedural Volumetric Noise for
Very Large and Detailed Scenes*, CGF 2025 (Eurographics), HAL hal-05046040.
- **Computes:** sphere tracing of FBM density fields, using first- and second-order
(2-Lipschitz) forward inclusion functions. The FBM partial sums act as a nested
bounding volume hierarchy: the remaining octaves are bounded by their amplitude sum
`s(i+1, n)`, so evaluation stops early once a point is provably outside.
- **Cost:** up to 12x faster than classical sphere tracing on opaque or constant-density
scenes. Only up to 1.8x on non-constant density, and that gain comes from empty-space
skipping alone. One example frame: 16 ms against 110 ms naive at 1024 x 1024 on a
4080 Ti.
- **Stated limits:** FBM used as a domain warp is not supported, nor are nonlinear
transforms of FBM. The paper names analytic estimation of the contribution of
sub-pixel detail as an open problem.
### 3.4 Taylor models
- **[search]** Berz, Makino, *Verified Integration of ODEs and Flows Using Differential
Algebraic Methods on High-Order Taylor Models*, Reliable Computing 4, 1998. Makino,
Berz, *Taylor Models and Other Validated Functional Inclusion Methods*, IJPAM 2003.
- **Computes:** a Taylor model is a polynomial of order n plus an interval remainder
bound, which encloses any function given as code over a box. The sharpness of the
remainder scales with the box width to the power n + 1. The method also gives
verified ODE flows.
- **Verified integration:** Berz and Makino also published *New Methods for
High-Dimensional Verified Quadrature* (Reliable Computing 1999). There the integral
of the polynomial part is exact, and the remainder bound times the domain measure
bounds the error.
- First-order AA is the n = 1 case in spirit: an affine part plus a bounded remainder.
### 3.5 Affine bound on a warp over a span
- **[derivation]** Over a segment of half-length r, the best affine approximation
(Chebyshev) of a C2 coordinate function has sup error at most `r^2 / 4` times the
maximum of its second derivative along the segment. Linear interpolation of the
endpoints has `r^2 / 2` times that maximum. The noise error caused by a displacement
error of size `delta` is at most `L_noise * delta`. This is the same chain-rule
structure Galin uses in section 4.4.
## 4. Statistical or expected-value treatment of high-frequency detail
- **[fetched]** Heitz, Nowrouzezahrai, Poulin, Neyret, *Filtering Color Mapped Textures
and Surfaces*, I3D 2013. It computes the filtered value of a colour map applied to a
procedural function as `<C, N(mu, sigma^2)>`. The noise's footprint distribution is
replaced by a Gaussian with the footprint's mean and variance. This equals the colour
map convolved with `N(0, sigma^2)` and evaluated at the mean, which is precomputed into
a 2D table indexed by (mu, sigma). Cost: one lookup after mu and sigma are computed.
Error: exact when the driving function's footprint distribution is Gaussian. For a
specific noise instance it converges as the footprint grows, and the paper states this
limit.
- **[derivation]** For `X ~ N(mu, sigma^2)`, `E[max(0, X - c)] = (mu - c) Phi(z) +
sigma phi(z)` with `z = (mu - c) / sigma`. This is the rectified-Gaussian mean, and the
scalar instance of the Heitz 2013 construction for the map `max(0, . - c)`. The
survey's fetched finding that Perlin noise is non-Gaussian bears on how accurate it is.
- **[search]** Olano, Baker, *LEAN Mapping*, I3D 2010. Dupuy et al., *LEADR Mapping*, ACM
TOG 32(6), 2013. They store the first and second moments of slopes in mipmaps. Linear
filtering of the moments gives a Gaussian or Beckmann slope distribution per
footprint. Cost: two extra texture channels and linear filtering. Error: the Gaussian
assumption.
- **[fetched]** Gabor Fields (1.2): band and orientation masking of high-frequency
kernels, with the Jensen bias of transmittance `~ 0.5 Var(tau) exp(-E[tau])`.
- **[search]** Vicini, Jakob, Kaplanyan, *A Non-exponential Transmittance Model for
Volumetric Scene Representations*, ACM TOG 40(4), 2021. Bitterli et al., *A Radiative
Transfer Framework for Non-exponential Media*, ACM TOG 37(6), 2018. Jarabo, Aliaga,
Gutierrez 2018, arXiv 1805.02651. Unresolved, correlated sub-voxel density makes
transmittance non-exponential. Positive correlation gives slower decay than exponential.
- **[search]** Kettunen, d'Eon, Pantaleoni, Novák, *An Unbiased Ray-Marching
Transmittance Estimator*, ACM TOG 40(4), 2021. A power-series estimator built around a
control variate. The control variate is a ray-marched mean-density estimate, and rare
higher-order terms remove the bias. It is relevant as the known way to make a
deterministic or expected-value optical depth unbiased.
- **[fetched]** Schneider, *The Real-time Volumetric Cloudscapes of Horizon: Zero Dawn*,
SIGGRAPH 2015 Advances course. The high-detail noise is applied only as erosion where
the low-detail sample is non-zero. Rays take cheap large steps until the low-detail
isosurface is hit, step back once, and then take full samples. After several zero
samples they return to cheap stepping. This is an octave-level skip heuristic with no
bound. Moinet (3.3) is the bounded version for FBM.
## 5. Temporal reuse of per-pixel or per-tile raymarch structure
- **[fetched]** Schneider 2015 (above). The full raymarch cost about 20 ms. Each frame
renders 1 of 16 pixels per 4 x 4 block into a quarter-resolution buffer, and
reprojects the previous frame. Where reprojection fails, for example at screen edges,
a low-resolution buffer is substituted. The paper reports 10x or more faster, with a
target of about 2 ms on PS4.
- **[search]** Klein, Strengert, Stegmaier, Ertl, *Exploiting Frame-to-Frame Coherence
for Accelerating High-Quality Volume Raycasting on Graphics Hardware*, IEEE
Visualization 2005. First-hit depths of the previous frame are reprojected by point
splatting to estimate per-pixel ray start points. About 2x on typical datasets.
- **[search]** Yagel, Shi, *Accelerating Volume Animation by Space-Leaping*, IEEE
Visualization 1993. A C-buffer of first non-transparent voxel coordinates, updated
across frames. Gudmundsson, Randén 1990 introduced inter-frame empty-space skipping.
- **[search]** Scherzer et al., *Temporal Coherence Methods in Real-Time Rendering*,
CGF 2012 STAR (Eurographics 2011). Reprojection caches, with depth-tolerance validity
tests for disocclusion.
- **[search]** Hillaire, *Physically Based Sky, Atmosphere and Cloud Rendering in
Frostbite*, SIGGRAPH 2016 course. Temporal reprojection for volumetric clouds. Details
were not read in this pass.
- **[fetched]** Gamito thesis, chapter 7: progressive refinement rendering of
Lipschitz-bounded and AA-bounded surfaces. It refines across passes within one image,
not across animated frames.
- **[not found]** No source was found that caches per-ray span partitions, affine
forms or certified intervals across frames and refines them.
## 6. Warp linearisation combined with exact segment integration
- **[not found]** No source was found that combines piecewise affine linearisation of a
domain warp, with an error bound, and exact closed-form integration of noise over the
resulting segments.
- Nearest pieces, each covering part of the combination:
| Source | What it covers | What it leaves out |
|---|---|---|
| Gamito ch. 9 | Bounding a deformed ray | No integral. AA gave up on the ODE warp |
| Seyb 2019 | ODE curved ray with adaptive error control | Surfaces only |
| Galin 2020 | Jacobian-norm local bounds through warps | Surfaces, not integration |
| Keeter 2020 | Interval pruning that tolerates warps | Surfaces |
| Gabor Fields 2026 | Closed form for any affine-transformed Gabor kernel along a segment | No warp |
| Balint 2019 | Closed form for cosine noise | Composition and warps break it |
| Pre-integration (Engel 2001) | Exact per-segment integral | Under a piecewise-linear model of the scalar |
| Moinet 2025 | Bounded FBM octave skipping | Excludes FBM used as a domain warp |
- **[search]** Rezk-Salama et al., *Fast Volumetric Deformation on General Purpose
Hardware*, Graphics Hardware 2001. Fetched text: piecewise-trilinear deformation
patches with adaptive subdivision, rendered by object-aligned slicing. The
approximate inverse is `x + O(delta^2)`. A search summary describes the deformed rays
as polylines, but the fetched text uses slicing, not per-ray exact integration.
## Sources
- Balint 2019: https://www.cl.cam.ac.uk/~rkm38/pdfs/balint2019cosine_noise.pdf
- Condor et al. 2026, Gabor Fields: https://arxiv.org/pdf/2602.05081
- Gamito PhD thesis: https://staffwww.dcs.shef.ac.uk/people/S.Maddock/phd_theses/gamito/thesis_small.pdf
- Gamito, Maddock 2007: https://link.springer.com/article/10.1007/s00371-006-0090-7
- Lagae et al. 2009, Gabor noise: https://www-sop.inria.fr/reves/Basilic/2009/LLDD09/LLDD09PNSGC_paper.pdf
- Lagae, Drettakis 2011: https://inria.hal.science/inria-00606907/file/paper_0049_preprint.pdf
- Lagae et al. 2010 survey: https://www-sop.inria.fr/reves/Basilic/2010/LLCDDELPZ10/LLCDDELPZ10STARPNF.pdf
- Heitz et al. 2013: https://inria.hal.science/hal-00765799
- Sharp, Jacobson 2022: https://arxiv.org/pdf/2202.02444
- Seyb et al. 2019: https://cs.dartmouth.edu/~wjarosz/publications/seyb19nonlinear.html
- Galin et al. 2020: https://hal.science/hal-02507361
- Moinet, Neyret 2025: https://inria.hal.science/hal-05046040
- Keeter 2020: https://www.mattkeeter.com/research/mpr/
- Fryazinov et al. 2009: https://eprints.bournemouth.ac.uk/11708/1/TR-NCCA-2009-04.pdf
- Knoll et al. 2009: https://onlinelibrary.wiley.com/doi/10.1111/j.1467-8659.2008.01189.x
- Berz, Makino 1998: https://link.springer.com/article/10.1023/A:1024467732637
- Engel et al. 2001: https://www3.cs.stonybrook.edu/~mueller/teaching/cse616/engel.pdf
- Gustavson 2005: https://cgvr.cs.uni-bremen.de/teaching/cg_literatur/simplexnoise.pdf
- Schneider 2015: https://advances.realtimerendering.com/s2015/
- Kettunen et al. 2021: https://jannovak.info/publications/URM/URM.pdf
- Vicini et al. 2021: https://dvicini.github.io/non-exponential-representation/
- Rezk-Salama et al. 2001: https://www3.cs.stonybrook.edu/~mueller/teaching/cse616/p17-rezk-salama.pdf
- Klein et al. 2005: https://www.researchgate.net/publication/4188150

View file

@ -0,0 +1,158 @@
# Cross-process file locking and atomic replace: prior art
Facts only. Gathered 2026-10-02 for fork `variants:question:ts-python-write-lock`. No conclusions, no recommendation.
Provenance legend: **[src]** read from CultLib source this pass; **[primary]** vendor/spec/upstream doc or source fetched this pass (the fetch tool summarises pages through a small model, so quoted phrasing is paraphrase-grade unless shown in quotes); **[secondary]** search-result snippet or third-party issue, weak; **[UNVERIFIED]** not confirmed by any source read, or derived by inference only. Nothing here was tested by running code on Windows or Linux.
Dates caveat: the doc-site fetches reported release dates for `fs2 0.4.3` ("September 9, 2026") and `fs4 1.1.0` ("September 23, 2026"). The fs2 date is implausible against my memory of that crate (long stable, last release years earlier); treat crate dates as **[UNVERIFIED]**.
---
## 0. What CultLib does today (source, this checkout)
| Runtime | Lock | Temp name | Replace | Pointer |
|---|---|---|---|---|
| C# | Sidecar `<file>.lock`, `new FileStream(lockPath, OpenOrCreate, ReadWrite, FileShare.None, bufferSize 1)`. On `IOException`: if `wait` is false return null, else sleep 10 ms and retry for up to 30 s, after which the exception propagates. | `.{name}.{Guid:N}.tmp`, `FileMode.CreateNew`, `FileShare.None`, `WriteThrough`, `Flush(true)` | `File.Replace(temp, path, null)` if target exists, else `File.Move` | `F:\Projects\CultLib\src\GameCult.Caching\CultCache.cs` lines ~3475-3530 |
| Rust | Sidecar `<file>.lock` (`.lock` appended to full file name), opened `create/read/write/truncate(false)` (std default share mode), then `fs2::FileExt::lock_exclusive` / `lock_shared` / `try_lock_exclusive` (WouldBlock => `None`). Unlock explicit, then drop. | `{name}.{uuid v4}.tmp`; staged file `sync_all` then closed | Unix: `fs::rename`. Windows: `MoveFileExW(MOVEFILE_REPLACE_EXISTING \| MOVEFILE_WRITE_THROUGH)` retried 25 times, 20 ms apart, on ERROR_ACCESS_DENIED(5)/SHARING_VIOLATION(32)/LOCK_VIOLATION(33). Comment cites a Muninn crash on 2026-09-11. | `F:\Projects\CultLib\packages\cultcache-rs\src\lib.rs` ~925-945, 1040-1084, 2870-3030; `Cargo.toml`: `fs2 = "0.4"`, edition 2024 |
| TypeScript | None. In-process promise queue only (`next`/`result` chain). | `${file}.tmp-${pid}-${Date.now()}-${random}` (unique per writer) | `fs/promises.rename`, retried 10 times with 10 ms doubling on EPERM/EBUSY/EACCES; temp removed on error. No fsync of temp. | `F:\Projects\CultLib\packages\cultcache-ts\src\single-file-messagepack-backing-store.ts` ~143-205 |
| Python | None. `threading.RLock` per store object (in-process only). | Fixed `path.with_suffix(suffix + ".tmp")`, i.e. `store.cc.tmp`, shared by all writers | `Path.replace` (`os.replace`); no fsync | `F:\Projects\CultLib\packages\cultcache-py\src\cultcache_py\stores.py` lines 70-82, 121-126 |
Both C# and Rust name the lock `<store path>.lock`. The same-name sidecar is the only thing the two currently share. Both also hold the lock around read-modify-write of the snapshot **[src]**. A lock file is never deleted by either (no stale-file problem; the lock is the kernel object, not the file's existence) **[src, inferred from absence of delete]**.
Note re the fork text: C# does use a lock (stated in the C# code comment: "excludes other handles in this process and in others alike").
---
## 1. Language and OS primitives
### 1.1 .NET `FileStream` / `FileShare`
- **Windows**: `FileShare` maps to the `dwShareMode` of `CreateFile`. `FileShare.None` = share mode 0: "Any request to open the file (by this process or another process) will fail until the file is closed." **[primary]** https://learn.microsoft.com/en-us/dotnet/api/system.io.fileshare . Win32 side: dwShareMode 0 "cannot be opened again, either by the application that opened it or by another application, until its handle has been closed"; failure is ERROR_SHARING_VIOLATION. **[primary]** https://learn.microsoft.com/en-us/windows/win32/fileio/creating-and-opening-files . Share modes are checked at **open** time and are not byte-range locks; they do not call `LockFileEx` **[inferred]**.
- **Unix**: .NET emulates `FileShare` with advisory `flock(2)`. Source (`SafeFileHandle.Unix.cs`, runtime main): `lockOperation = (share == FileShare.None) ? LOCK_EX : LOCK_SH`, called as `FLock(handle, lockOperation | LOCK_NB)`; on `EWOULDBLOCK` an `IOException` is thrown; handle close issues `LOCK_UN`. **[primary]** https://raw.githubusercontent.com/dotnet/runtime/main/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
- So non-`None` opens take a **shared** flock; `None` takes an **exclusive** flock; non-blocking in all cases. A plain read with `FileShare.ReadWrite | Delete` still takes `LOCK_SH`, which would conflict with a writer holding `LOCK_EX` on the same file (only relevant if the same file is opened both ways). **[inferred from source above]**
- Disable switch: env `DOTNET_SYSTEM_IO_DISABLEFILELOCKING`; locking is off by default on Browser/WASI and iOS/tvOS (not Mac Catalyst). **[primary]**
- Because it is advisory flock, it excludes only other openers that also take flock (other .NET processes, or any program calling flock on that file). It does not stop a process that merely `open()`s and writes. **[inferred]**
- Since which .NET version: **[UNVERIFIED]**. I believe the flock-based FileShare emulation has existed since .NET Core 1.x/2.x, and .NET 6 rewrote FileStream into strategies while keeping it, but no source read this pass dates it.
- Windows `FileShare.Delete` is meaningful; the Unix code does not differentiate it. **[primary]**
- `File.Replace(src, dst, backup)` -> Win32 `ReplaceFile`; throws if volumes differ. **[primary]** https://learn.microsoft.com/en-us/dotnet/api/system.io.file.replace . Unix mapping to `rename` **[UNVERIFIED]**.
### 1.2 Rust
- `std::fs::File::lock`, `lock_shared`, `try_lock`, `try_lock_shared`, `unlock`: stable since **Rust 1.89.0**. **[primary]** https://doc.rust-lang.org/std/fs/struct.File.html
- Unix/WASI: `flock` (`LOCK_EX`, `LOCK_SH`, `+LOCK_NB`, `LOCK_UN`). Windows: `LockFileEx` (flags `LOCKFILE_EXCLUSIVE_LOCK`, `LOCKFILE_FAIL_IMMEDIATELY`) and `UnlockFile`.
- Docs: lock "may be advisory or mandatory", platform-dependent; on Windows the file must be opened for write (or read+append) for an exclusive lock; re-locking the same handle or a clone is unspecified, possibly deadlock; locks released when the file and all duplicated handles close; "this may change in the future".
- `fs2` 0.4.x (what CultCache Rust uses): Unix `flock(2)`; Windows `LockFileEx` (source `src/windows.rs`: `lock_shared` passes 0, `lock_exclusive` passes `LOCKFILE_EXCLUSIVE_LOCK`, try variants add `LOCKFILE_FAIL_IMMEDIATELY`; `unlock` uses `UnlockFile`). Docs: "advisory"; do not lock one `File` concurrently; duplicated files need great care. **[primary]** https://docs.rs/fs2/latest/fs2/trait.FileExt.html , https://raw.githubusercontent.com/danburkert/fs2-rs/master/src/windows.rs . An earlier summary of the fs2 docs page said "LockFile" for Windows; the source file says `LockFileEx`. Maintenance status of fs2: **[UNVERIFIED]**.
- `fs4`: fork of fs2 adding async, rustix instead of libc; docs say `flock`/`fcntl` on Unix, `LockFileEx` on Windows. Whether std's `File::lock` is mentioned or deprecates it: not stated on the page read. **[primary]** https://docs.rs/fs4/latest/fs4/ . Which Unix call it picks by default (flock vs fcntl): **[UNVERIFIED]**.
- Windows default share mode of Rust `OpenOptions`: `FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE`; `share_mode(0)` makes exclusive. **[primary]** https://doc.rust-lang.org/std/os/windows/fs/trait.OpenOptionsExt.html
- `std::fs::rename`: Unix `rename(2)`; Windows 10 1607+ `MoveFileExW(MOVEFILE_REPLACE_EXISTING)` or `SetFileInformationByHandle(FileRenameInfoEx)` (POSIX semantics); earlier, MoveFileExW only. "may change in future". **[primary]** https://doc.rust-lang.org/std/fs/fn.rename.html . (CultCache Rust bypasses std rename on Windows and calls MoveFileExW itself, per 0.)
### 1.3 Node
- No built-in advisory file lock in `node:fs`; **[UNVERIFIED]** by a direct docs read this pass, but nothing in the packages read contradicts it. Options seen:
- `proper-lockfile`: "mkdir strategy" - creates `<target>.lock` as a **directory**; works across local and network FS; stale detection by lock dir **mtime** (default `stale` 10 s, min 5 s; mtime refreshed every `update` = stale/2, min 1 s); `onCompromised` callback when refresh fails or is late; avoids `O_EXCL` because "O_EXCL is broken on NFS" (their README); contrasts with `lockfile` package which uses ctime for staleness, unsuitable for long-running holders. **[primary]** https://raw.githubusercontent.com/moxystudio/node-proper-lockfile/master/README.md
- `lockfile` (npm), `fs-ext` (native `flock`/`fcntl` bindings): not fetched. **[UNVERIFIED]**
- Libuv rename on Windows uses `MoveFileExW(MOVEFILE_REPLACE_EXISTING)`; reports say it fails with EPERM while any handle holds the destination open, and refuses a READONLY destination; some runtimes (Bun PR) fall back to POSIX-semantic replace. **[secondary]** search snippets: https://github.com/oven-sh/bun/pull/43997 , https://github.com/joyent/libuv/issues/283 . Not read from libuv source: **[UNVERIFIED]**.
- `npm/write-file-atomic`: temp name = `filename + "." + murmurhex(__filename, process.pid, ++invocations)` (plus worker thread id); same-process writes to one file queued and serialised; fsync on by default; no cross-process lock mentioned. **[primary]** https://raw.githubusercontent.com/npm/write-file-atomic/main/README.md
### 1.4 Python
- `fcntl.flock()` wraps `flock(2)`; docs: "On some systems, this function is emulated using fcntl()". `fcntl.lockf()` is "essentially a wrapper around the fcntl() locking calls". Unix only. "On at least some systems, LOCK_EX can only be used if the file descriptor refers to a file opened for writing." The page does not state whether flock and lockf interact. **[primary]** https://docs.python.org/3/library/fcntl.html
- `msvcrt.locking(fd, mode, nbytes)`: locks `nbytes` from the current file position (may extend past EOF); `LK_LOCK`/`LK_RLCK` retry once per second, 10 attempts, then `OSError`; `LK_NBLCK` fails immediately; regions cannot overlap; adjacent regions not merged. Underlying Win32 call not stated in docs. **[primary]** https://docs.python.org/3/library/msvcrt.html . (The CRT `_locking` is documented elsewhere as `LockFile`-based; **[UNVERIFIED]** here.)
- `filelock` (tox-dev): `FileLock` alias = `UnixFileLock` (`fcntl.flock`, described as kernel-enforced and released on crash) on Unix; `WindowsFileLock` described as `LockFileEx` byte-range locking on Windows (older versions used `msvcrt.locking`; version not checked, **[UNVERIFIED]**); `SoftFileLock` is a "cooperative file-existence marker", with same-host PID inspection and reclaim from dead processes; docs recommend verifying shared-filesystem semantics before deploying soft locks; no statement on cross-type exclusion. **[primary]** https://py-filelock.readthedocs.io/en/latest/index.html
- `portalocker`: README fetch 404; its primitives are **[UNVERIFIED]** (not read).
- `os.replace` on Windows semantics (MoveFileEx REPLACE_EXISTING): **[UNVERIFIED]**, not fetched.
### 1.5 Interoperability matrix (flock / fcntl / LockFileEx / share modes)
| Question | Finding | Source |
|---|---|---|
| Linux, local FS: does `flock` exclude `fcntl` (POSIX byte-range) locks? | No. man flock(2): on Linux "there is no interaction between the types of lock placed by flock() and fcntl(2)" (local filesystems). | **[primary]** https://man7.org/linux/man-pages/man2/flock.2.html |
| Linux over NFS (since 2.6.12) | flock is emulated as whole-file fcntl byte-range locks, so flock and fcntl **do** interact there. | same |
| Linux over CIFS (since 5.5) | flock emulated with SMB byte-range locks; flock and fcntl interact; and the lock becomes **mandatory**: I/O from other descriptors on a locked file fails with EACCES. | same |
| Duplicate fds / fork | A lock belongs to the open file description; shared by dup/fork; persists until `LOCK_UN` or **all** duplicates close. Independent `open()`s in the same process conflict with each other under flock. | same |
| Does a Rust `flock` (std or fs2) exclude a .NET-on-Unix `FileShare.None` open? | Both are `flock(LOCK_EX)` on the same inode; by the sources above they should conflict (.NET's non-blocking attempt gets EWOULDBLOCK -> IOException -> CultCache C# retries). **Not tested.** | **[inferred]**; **[UNVERIFIED]** empirically |
| Same, but .NET `FileShare.ReadWrite` opener (shared flock) vs Rust `lock_exclusive` | Conflicts (LOCK_SH vs LOCK_EX), so a reader opening the lock file with any FileShare would block/fail against a Rust holder. CultCache does not open the lock file for reading. | **[inferred]** |
| Windows: does a share-mode-0 open (.NET `FileShare.None`) exclude a Rust `LockFileEx` locker? | Order A: .NET holds the file with share mode 0. Rust's `OpenOptions::open` (share mode default RWD) then fails at `CreateFile` with ERROR_SHARING_VIOLATION, before it can lock. In `open_lock_file` that is a `with_context` error, **not a wait/retry** (looked at the source). Order B: Rust holds a handle (RWD) and `LockFileEx`; .NET `CreateFile` with share mode 0 conflicts with the existing open handle's access -> ERROR_SHARING_VIOLATION -> `IOException` -> C# retries up to 30 s. So mutual exclusion holds in both orders, but in order A Rust errors instead of waiting. | **[inferred]** from MS CreateFile share-mode table + Rust OpenOptionsExt default; **[UNVERIFIED]** empirically |
| Windows: is `LockFileEx` mandatory? | Exclusive region lock "denies all other processes both read and write access to the specified region"; shared lock denies writes; locks are not honoured through mapped views. If a process dies or closes the handle the OS releases the locks, but "the time it takes ... depends upon available system resources" (MS recommends explicit unlock). Handle needs GENERIC_READ or GENERIC_WRITE. | **[primary]** https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex |
| Windows: second handle in same process | "If the locking process opens the file a second time, it cannot access the specified region through this second handle until it unlocks the region." | same |
| Rust-on-Windows `LockFileEx` vs Python `msvcrt.locking` (same region?) | `LockFileEx` regions conflict if they overlap; fs2 locks which byte range (offset 0, len u32::MAX..?) : **[UNVERIFIED]**; msvcrt.locking locks `nbytes` from the current position (so depends on `lseek`); whether CRT `_locking` uses LockFile/LockFileEx and which range would need to match: **[UNVERIFIED]**. |
| Python Unix `flock` vs Rust/.NET flock | Same kernel primitive on Linux/macOS (Python's flock "may be emulated using fcntl on some systems", so platform-dependent). Python `lockf` is fcntl: independent from flock on local Linux FS. | **[primary]** python docs + man page |
| macOS flock vs fcntl | **[UNVERIFIED]** (not fetched). |
| POSIX fcntl lock semantics | Per-process: `close()` of **any** fd for the file by the process drops all of that process's fcntl locks (SQLite doc 2.2 below). flock locks are per open-file-description and do not have that hazard. | **[primary]** SQLite howtocorrupt; flock(2) |
| Python `fcntl.flock` on Windows | Module is Unix only (docs). | **[primary]** |
---
## 2. Lockfile protocols in established software
### git
- Lockfile API: lock for `foo` is `foo.lock`, created with `O_CREAT|O_EXCL`; writer writes new content into it; `commit_lock_file()` renames it over `foo`; `rollback_lock_file()` removes it; writers exclude each other; readers never block and see either old or new content (relies on atomic rename); cleanup of outstanding lockfiles is registered with `atexit` and signal handlers. **[primary]** https://git-scm.com/docs/api-lockfile
- `lockfile.c`: retry with quadratic backoff for at least `timeout_ms`; PID file registered as a tempfile for signal/atexit cleanup; failure message says another git process may be running "or the lock file may be stale" and tells the user to remove it manually. **[primary]** https://raw.githubusercontent.com/git/git/master/lockfile.c
- Consequences recorded in the docs: a hard kill (SIGKILL, power loss) leaves a `.lock` file and later writers fail until it is removed; git does not time-based-reclaim index.lock **[inferred from message text, not stated; UNVERIFIED]**. Note the git design makes the lock file **the temp file** (lock name = staging name); CultCache's current design has a separate sidecar lock plus uniquely named temp.
### SQLite (see also section 4)
- Unix default VFS: POSIX advisory locks via `fcntl()`; Windows VFS: `LockFile`/`LockFileEx`/`UnlockFile`. Five states UNLOCKED/SHARED/RESERVED/PENDING/EXCLUSIVE. **[primary]** https://www.sqlite.org/lockingv3.html (byte-range layout not stated on that page).
- Built-in unix VFS variants: `unix` (POSIX advisory), `unix-dotfile` (dot-file locking), `unix-excl` (holds exclusive lock, WAL index in heap), `unix-none` (no locking), `unix-namedsem` (VxWorks). "Except for 'unix' and 'unix-excl', the various Unix VFSes all use incompatible locking implementations"; processes using different VFSes on one DB "may not see each others locks ... resulting in database corruption". **[primary]** https://www.sqlite.org/vfs.html . (`unix-flock` / `unix-afp` / `unix-nfs` appear in SQLite's source as separate io-method variants; not listed on that page: **[UNVERIFIED]**.)
### LMDB
- Uses a separate `lock.mdb` file for the reader table and writer mutex. Header warnings: stale reader slots from aborted programs make the DB grow quickly (`mdb_reader_check`); stale writer lock clears automatically on Windows, BSD with SysV semaphores, and Linux with robust POSIX mutexes, otherwise all DB users must be closed; "Do not have open an LMDB database twice in the same process at the same time. Not even from a plain open() call - close()ing it breaks flock() advisory locking."; "Do not use LMDB databases on remote filesystems, even between processes on the same host. This breaks flock() on some OSes"; `MDB_NOLOCK` disables locking entirely. **[primary]** https://raw.githubusercontent.com/LMDB/lmdb/mdb.master/libraries/liblmdb/lmdb.h
- Note: that warning is about `flock` and `close()`, which contradicts the usual statement that only fcntl locks have the close() hazard; the source of the discrepancy (older Linux/BSD semantics, or the emulated flock on some OS) is **[UNVERIFIED]**.
### Cargo / npm / pip / Gradle
- Cargo: package-cache and build-dir lock using an OS file lock, prints "Blocking waiting for file lock"; source fetch (`src/cargo/util/flock.rs`) 404; details (flock/fs4/std, handling of filesystems without locking) **[UNVERIFIED]**.
- Gradle: `DefaultFileLockManager` holds cross-process locks on cache/daemon-registry files; reports of builds hanging on locks left by crashed or killed daemons. **[secondary]** web search snippets only (https://discuss.gradle.org/t/gradle-daemon-keeps-locking-intermediate-files-preventing-builds/38004 , https://www.javathinking.com/blog/gradle-build-is-hanging-without-failure-defaultfilelockmanager-acquiring-and-releasing-lock-on-daemon-addresses-registry/ ); Gradle's own docs on lock protocol not read. **[UNVERIFIED]**
- npm: `write-file-atomic` has no cross-process lock (above); npm's cache (cacache) locking: **[UNVERIFIED]**, not fetched.
- pip: **[UNVERIFIED]**, not fetched.
### Windows rename retry (Postgres)
- `pgrename()` on Windows calls `MoveFileEx(from, to, MOVEFILE_REPLACE_EXISTING)` and retries on ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION, ERROR_LOCK_VIOLATION: up to 100 retries, 0.1 s apart (~10 s). Comment: other applications may have the file open without sharing flags; LOCK_VIOLATION seen from some anti-virus software; bounded because the caller may hold locks affecting other backends. **[primary]** https://raw.githubusercontent.com/postgres/postgres/master/src/port/dirmod.c . CultCache Rust uses the same error set with 25 x 20 ms (0.5 s); TS retries EPERM/EBUSY/EACCES 10 times with doubling from 10 ms (about 10 s worst case) **[src]**.
---
## 3. Temp-file naming and rename atomicity
- Unique temp names per writer: C# GUID, Rust uuid v4, TS pid+time+random **[src]**; npm write-file-atomic pid + counter hash **[primary]**; Python CultCache fixed `.tmp` **[src]**. Rust additionally has a startup cleanup that deletes only temps matching `<store>.<uuid>.tmp` (tests at `lib.rs` ~3420, 4667-4689 distinguish `not-a-uuid.tmp`, bare `.tmp`, neighbour stores) **[src]**. Note the naming differs between runtimes: C# `.{name}.{guid:N}.tmp` (leading dot, no dashes) vs Rust `{name}.{uuid-with-dashes}.tmp` vs TS `{name}.tmp-...`; whether the Rust stale-temp sweeper recognises C#/TS temps: it matches the Rust format only (per the test names) **[src, partial reading]**.
- Fixed temp name consequence (inferred, not tested): two processes writing `store.cc.tmp` concurrently share one file: interleaved `write_bytes` (truncate-and-write) can yield a mixed or truncated file that one process then renames into place, and the other process's `replace` can fail with source-not-found. **[inferred]**
- POSIX `rename()`: if `new` exists it "shall be removed and old renamed to new"; the rationale requires the action to be atomic; a directory entry named `new` stays visible to other threads throughout and refers to either old or new file; on failure other than I/O errors `new` is unaffected; spec does not define crash durability. **[primary]** https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html
- Directory fsync: the usual pattern is write temp, fsync temp, rename, then fsync the parent directory so the rename survives power loss; many projects have filed bugs for omitting the directory fsync. **[secondary]** search snippets (e.g. https://github.com/npm/write-file-atomic/issues/64 titled "Rename atomicity is not enough"; https://0xkiire.com/crash-consistency-fsync-rename/). Linux ext4 `auto_da_alloc` replace-via-rename heuristic: **[UNVERIFIED]**, not fetched. Whether CultCache Rust/C# fsync the parent directory on Unix: **[UNVERIFIED]** (not seen in the excerpts read; Rust does `sync_all` on the temp, C# `Flush(true)` + `WriteThrough`; TS and Python do neither).
- Windows `MoveFileExW`:
- `MOVEFILE_REPLACE_EXISTING` replaces the destination if ACLs allow; errors if destination is an existing directory; `MOVEFILE_WRITE_THROUGH` waits for the move to be flushed (documented as guaranteeing flush of a copy+delete move). Same-volume rename is a metadata operation. The doc page does not use the word "atomic" and does not describe behavior when the destination is open by another handle. **[primary]** https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw
- Open-handle failure: reported from libuv/Bun and Postgres as ERROR_ACCESS_DENIED / SHARING_VIOLATION / LOCK_VIOLATION when another process has the destination open without FILE_SHARE_DELETE, or an anti-virus scan holds it. **[primary]** Postgres comment; **[secondary]** libuv/Bun.
- Rust's std rename (1.0+ Windows 10 1607) may use `FileRenameInfoEx` with POSIX semantics, which can replace an open destination; CultCache Rust calls `MoveFileExW` directly instead **[src; primary for std]**.
- Windows `ReplaceFileW` (used by .NET `File.Replace`): replaced file opened with GENERIC_READ|DELETE|SYNCHRONIZE and share mode `READ|WRITE|DELETE`; replacement file opened with **no sharing mode specified**; preserves creation time, short name, object id, DACLs, encryption, compression, named streams; resulting file has the replacement's file ID; all files must be on the same volume; documented failure states leave the system in partially-moved states: ERROR_UNABLE_TO_REMOVE_REPLACED (1175; names unchanged), ERROR_UNABLE_TO_MOVE_REPLACEMENT (1176; with no backup, the replaced file "no longer exists" and the replacement exists under its original name), ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 (1177). `REPLACEFILE_WRITE_THROUGH` "not supported". The page makes no statement that ReplaceFile is atomic; it describes it as combining steps. **[primary]** https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew
- So on Windows, C# (`File.Replace`) and Rust/TS/Python (MoveFileEx or equivalent) use different primitives with different failure windows. **[inferred from the two pages]**
- .NET `File.Move` with overwrite on Windows/Unix: **[UNVERIFIED]** (CultCache C# uses `File.Move(temp, path)` only when the target does not exist; there is a window in which another writer creates the target between the `File.Exists` check and the move; the lock covers it when taken **[src, inferred]**).
---
## 4. Known failures and warnings
SQLite "How To Corrupt An SQLite Database File" (https://www.sqlite.org/howtocorrupt.html) **[primary]**:
- 2.1 Filesystems with broken or missing lock implementations (network filesystems, NFS) can corrupt when multiple processes access.
- 2.2 POSIX advisory locks are cancelled by `close()`: `close()` on any descriptor for the file by any thread in the process releases the process's locks on that file; example: thread B opens, reads and closes the DB file for a backup and thread A's locks vanish silently. SQLite works around it with a mutex-protected global list of open files, which fails if two copies of SQLite are linked into one app (2.3; a commercial product shipped this bug with infrequent corruption on Linux/Mac). WAL multi-process defenses added in 3.51.0 (2025-11-04).
- 2.4 Two processes using different locking protocols (e.g. POSIX advisory vs dot-file) cannot see each other's locks; all connections must use the same protocol.
- 2.5 Unlinking or renaming the DB file while in use: processes end up with different physical files sharing one journal; SQLite warns (since 3.7.17) when the file is unlinked while open.
- 2.6 Multiple links (hard/symbolic links): same hazard; resolved by canonicalising symlinks since 3.10.0.
- 2.7 Carrying a connection across `fork()`: all locking mechanisms fail with unpredictable results.
- 1.3/1.4 Hot journal deleted, moved or mispaired with database.
- The SQLite VFS page: incompatible unix VFS locking => corruption (section 2 above).
Other recorded failure modes:
- Windows `LockFileEx` locks are released by the OS on process death but with a lag (MS doc). **[primary]**
- CIFS flock becomes mandatory (man flock(2)). **[primary]**
- Lock file left behind: git (cleanup via atexit/signal; a hard kill leaves `.lock`, message tells operator to remove) **[primary]**; Gradle hangs after killed daemons **[secondary]**; `proper-lockfile` and `filelock.SoftFileLock` provide mtime / PID-based stale reclaim as a mitigation **[primary]**.
- Lock protocols that are compatible only when everyone uses the same one: SQLite (above); `filelock` docs do not state cross-type exclusion between `UnixFileLock`/`SoftFileLock`. **[primary]**
- Lost-update by missing cross-process lock around read-modify-write: `write-file-atomic` documents only same-process serialisation; no source read this pass reports a concrete lost-update bug in it. A search-based report of corrupted/missing "settings.json" on Windows from a failed atomic write exists as an unvetted issue (https://github.com/BarganConstantin/ccdeck/issues/772, **[secondary]**, read-only destination). Reports specific to cross-language writers of one file, other than SQLite's different-VFS warning: not found this pass. **[UNVERIFIED]**
- Lock file deleted by one participant while others hold or wait on it (different inode afterward): general hazard of delete-on-unlock lock files, not sourced this pass beyond SQLite 2.5 (unlink of a live file); **[UNVERIFIED]** as a named issue for flock sidecars.
---
## 5. Open items not resolved by reading (candidates for an empirical check; nothing done)
1. Do the C# `FileShare.None` lock and Rust `fs2::lock_exclusive` actually exclude each other on Windows (both open orders) and on Linux? Only inferred above.
2. Does fs2 on Windows lock the same byte range as std 1.89 `File::lock` and as Python `msvcrt.locking`/`filelock`? Unread.
3. macOS behavior of flock vs fcntl and of .NET FileShare there. Unread.
4. .NET release that introduced Unix FileShare-via-flock. Unread.
5. Whether any runtime fsyncs the parent directory after rename on Unix. Unread.
6. Cargo, npm, pip, Gradle lock protocol primary sources. Unread.
7. `portalocker`, `fs-ext`, `lockfile`, `proper-lockfile` behavior under Windows share modes. Unread.

View file

@ -0,0 +1,305 @@
# Culling Efficiency Against an Oracle: Prior Art Survey
This is the facts file for Eureka question `cultmath-tapes:question:wells-ceiling-oracle`,
which interprets ruling `cultmath-tapes:ruling:operator-wells-contract-vs-ceiling`. That
ruling holds scenario (c) to "at least 90% of the probe-free ceiling (the cost of the
dense cells alone)". The question asks what "ceiling" means:
- (a) the pre-pass's own mask, under which a pre-pass that skips nothing scores 0.98;
- (b) an oracle mask, the cells where at least one ray samples nonzero density.
Under (b), Imagination measured 0.38 at warp 0 and 0.19 at warp D.
This survey covers how published work measures culling or skipping efficiency against
ground truth, and what efficiency figures conservative bounds reach. Eyes pass,
2026-10-03. It gives facts and evidence pointers only, with no recommendation.
Verification marks:
- **[fetched]** The claim was read in the cited source's full text during this pass.
PDFs were converted with `pdftotext -layout`.
- **[search]** The claim came from a search-result summary, not a full read.
- **[not found]** The source was read, and the named kind of measurement is absent from it.
Reference kinds used below:
- **oracle**: an exact or ground-truth answer computed independently of the method,
such as the exact visible set or the set of truly non-empty cells.
- **self**: the method's own result, or a baseline that does no culling.
- **peer**: another approximate method.
## 1. Visibility culling: conservative versus exact visible set
### 1.1 Survey vocabulary
- **[fetched]** Cohen-Or, Chrysanthou, Silva, Durand, *A Survey of Visibility for
Walkthrough Applications* (IEEE TVCG 2003). Section 2 lists "Tightness of
approximation" as a review criterion. It notes that few papers report the ratio
between the size of the potentially visible set and the size of the visible set,
and asks later work to report it. Most surveyed techniques are conservative, which
means they overestimate the visible set.
https://www.cs.tau.ac.il/~dcor/online_papers/papers/visibility-survey-ieee.pdf
- **[fetched]** Bittner, *Hierarchical Techniques for Visibility Computations* (PhD
thesis, CTU Prague, 2002), chapter 2, equations 2.1 to 2.4. It defines a quality
measure against the exact PVS `S_E`:
- relative overestimation `Q_o = |S_A \ S_E| / |S_E|`;
- relative underestimation `Q_u = |S_E \ S_A| / |S_E|`;
- the expected quality, which is the expectation of these over inputs.
The same text appears in Bittner and Wonka, *Visibility in Computer Graphics*
(section 4.3).
https://dcgi.fel.cvut.cz/home/bittner/publications/diss_noimages.pdf
http://www.casa.ucl.ac.uk/mike-michigan-april1/mike's%20stuff/attach/bittner_wonka.pdf
- **[fetched]** Nirenstein, Blake, Gain, *Exact From-Region Visibility Culling* (EGWR
2002), introduction and Table 1. The paper classifies algorithms as exact,
conservative, aggressive or approximate. Conservative methods "incur a false
visibility error". The paper states:
- exact visibility "provides a benchmark for comparing other approaches";
- the percentage of polygons culled is "a very crude and imperfect metric", because
it ignores the type, distribution and magnitude of visibility error.
Reported results (Table 2) are culling fractions of the whole scene: 99.45% for the
town and 99.12% for the forest. No conservative method is measured against the
exact set in this paper **[not found]**. A search summary attributed a 95.5% figure
for extended projection to this paper, but that figure is not in the full text.
https://people.cs.uct.ac.za/~jgain/wp-content/papercite-data/pdf/nirenstein2002.pdf
### 1.2 Measured conservative-versus-exact ratios
- **[fetched]** Bittner, Wonka, Wimmer, *Fast Exact From-Region Visibility in Urban
Scenes* (EGSR 2005), section 6. The exact 2.5D algorithm is used as the oracle, and
the paper simulates conservative methods against it:
- **Section 6.2, one penumbra wedge per occluder.** The conservative PVS was more
than 47% larger than the exact PVS for 2 m view cells and more than 76% larger
for 4 m view cells. As exact/conservative ratios these are ≤ 0.68 and ≤ 0.57.
- **Section 6.3, object-space discretization** (Wonka et al.'s method, a 1400×1400
grid, Vienna). The PVS overestimate averaged 17%, a ratio of about 0.85.
- **Section 6.3, Table 4, line-space discretization** (Leyvand et al.). PVS sizes by
grid resolution, with the exact PVS last:
| Scene | 512 | 1024 | 2048 | 16384 | exact |
|---------|--------|--------|--------|-------|-------|
| Vienna | 633 | 535 | 489 | 455 | 235 |
| Atlanta | 50,744 | 25,751 | 10,082 | 2,656 | 1,152 |
The ratios exact/conservative below are computed from that table, not quoted from
the paper:
- Vienna: 0.37, 0.44, 0.48, 0.52;
- Atlanta: 0.023, 0.045, 0.11, 0.43.
The paper says the overestimation comes from conservative shrinking and from gaps
"between formerly connected occluders".
https://peterwonka.net/Publications/pdfs/2005.EGSR.Bittner.FastExactFromRegionVisibility.pdf
### 1.3 Run-time occlusion culling against an ideal renderer
- **[fetched]** Mattausch, Bittner, Wimmer, *CHC++: Coherent Hierarchical Culling
Revisited* (Eurographics 2008), section 7 (Figure 7) and the conclusion. The
reference algorithm "OPT" renders only the visible nodes of the hierarchy and
issues no queries. It was implemented by recording visibility with an exact
stop-and-wait pass, so it is an oracle at node granularity that is independent of
the method under test. CHC++ is reported to be "for most cases within a few percent
of the 'ideal' method".
Caveat: occlusion queries are exact per-node tests, not conservative range bounds.
The gap to OPT measures query overhead and latency, not bound looseness.
The paper also separates out the "optimal" algorithm defined by Guthe et al.
(NOHC-OPT), and shows CHC++ "clearly below" that optimum. That earlier optimum was a
cost-model optimum, not an oracle.
https://dcgi.fel.cvut.cz/home/bittner/publications/chc++.pdf
- **[fetched]** Zhang et al., *Visibility Culling using Hierarchical Occlusion Maps*
(SIGGRAPH 1997). It reports "% culling" of the total model, and frame time
normalized to rendering without HOM. Both are **self** references. No comparison
with the exact visible set **[not found]**.
https://www.cs.princeton.edu/courses/archive/spr01/cs598b/papers/zhang97.pdf
- **[search]** Wonka et al., *Guided Visibility Sampling* (SIGGRAPH 2006). This is
an aggressive, sampling-based PVS method. Its Table 2 reports error as false pixels
on a 1000×1000 screen, together with PVS size. Section 4.5 compares it to Bittner's
exact algorithm on one scene, because exact solvers do not run on the larger test
scenes. The text was extracted in this pass, but not read in full.
https://peterwonka.net/Publications/pdfs/2006.SG.Wonka.GuidedVisibilitySampling.final.pdf
## 2. Empty-space skipping in volume rendering
- **[fetched]** Li, Mueller, Kaufman, *Empty Space Skipping and Occlusion Clipping for
Texture-based Volume Rendering* (IEEE Vis 2003):
- **Section 5, Table 1.** Empty-space skipping is measured as a frame-rate speedup
over no skipping, 2.8× on average. This is a **self** reference.
- **Section 5, Table 2.** It reports the percentage of non-empty voxels skipped
through occlusion, 48% to 64% with a 56.2% average. The denominator is all
non-empty voxels, not the truly occluded voxels. The authors write that "there
are still many occluded voxels that have not been skipped" and call the number
"smaller than we have expected". The truly occluded set is not computed
**[not found]**.
- **Section 5.** Overhead is reported separately. The opacity map and clipping cost
about 10% of the empty-space-skipping render time, measured with a mode that pays
the overhead but skips nothing ("ESS+M").
https://www3.cs.stonybrook.edu/~mueller/papers/OcclusionClipping.pdf
- **[fetched]** Zellmann, *Comparing Hierarchical Data Structures for Sparse Volume
Rendering with Empty Space Skipping* (arXiv 1912.09596, 2019). It reports frame
rates and build times, and compares only against naive ray marching and simple
macrocell grids, as the paper itself states. No step count or sample count against
an ideal traversal **[not found]**.
https://arxiv.org/abs/1912.09596
- **[search]** Hadwiger et al., *SparseLeap* (IEEE TVCG 24(1), 2018). It builds
per-pixel lists of ray segments by rasterizing occupancy geometry. The full text
was not retrieved (the repository returned HTML), so it is unknown whether it
reports skip efficiency against the true empty space.
https://vccvisualization.org/research/sparseleap/
- **[search]** Deakin and Knackstedt, *Efficient ray casting of volumetric images using
distance maps for empty space skipping* (Computational Visual Media 6(1), 2020). The
full text was not retrieved.
## 3. Interval and affine arithmetic for implicits, noise and SDF marching
- **[fetched]** Keeter, *Massively Parallel Rendering of Complex Closed-Form Implicit
Surfaces* (SIGGRAPH 2020):
- **Section 3, Interval arithmetic.** Ambiguous regions "either contain the shape's
boundary or are a false positive (due to interval arithmetic's conservative
behavior)". The false-positive share is not measured **[not found]**.
- **Section 2.1, Figure 5.** Reported efficiency is against **self** and peer
baselines: a brute-force per-pixel interpreter (about 58 ms/MP) and a compiled
kernel (about 3 ms/MP).
- **Section 3.2, Figure 6.** Tape length is reduced 17× in 64² tiles and 216× in 8²
subtiles, from 6056 clauses to means of 356 and 28.
- **Section 5, Figures 9 and 10.** Heatmaps of work per pixel show extra work at
model edges, where interval evaluation "can't skip entire regions". The text says
many smooth blends lead "to less useful interval evaluation results".
- **Section 6.** It names reduced affine arithmetic as a way "to more tightly bound
intervals".
https://www.mattkeeter.com/research/mpr/keeter_mpr20.pdf
- **[fetched]** Keeter, *Gradients are the new intervals* (blog, 2025-05-14). It
compares interval evaluation with gradient-based pseudo-intervals by tape length
(120.2 against 121.5 clauses) and by render time. These are peer references. There
are no tile-classification counts and no ground truth **[not found]**.
https://www.mattkeeter.com/blog/2025-05-14-gradients/
- **[fetched]** Heidrich, Slusallek, Seidel, *Sampling Procedural Shaders Using Affine
Arithmetic* (ACM TOG 17(3), 1998):
- **What "error" measures.** Error is the width of the computed range per pixel. It
is not compared with the true range from dense sampling **[not found]**. The
tightness against the true variation is argued qualitatively, from shader cross
sections.
- **Section 3.2.** Noise over an affine form that spans several lattice cells falls
back to interval arithmetic, and a span of more than two cells returns the full
noise range [0, 1]. For turbulence (blue marble), 81% of pixels lie within 5%
error and 59% within 1%. The eroded shader, with high-frequency noise, gets 77%
and 53%.
- **Section 4.** Against AA, interval arithmetic gave errors "up to 50 percent where
affine arithmetic produces errors below 1/256" (wood shader, Figures 6 and 7).
https://vccimaging.org/Publications/Heidrich1998SPS/Heidrich1998SPS.pdf
- **[fetched]** Gamito and Maddock, *Ray casting implicit fractal surfaces with reduced
affine arithmetic* (The Visual Computer 23(3), 2007). Section 5, Tables 1 to 3, use
a sphere hypertextured with three octaves of noise. The accuracy proxy is the
average number of interval evaluations per ray, a **peer** comparison with no oracle
count. The rows below are reconstructed from a mangled text layout, and the
reconstruction agrees with the paper's prose.
| Noise | IA evals/ray | IA time | Standard AA evals/ray | Reduced AA evals/ray | Reduced AA + interval optimisation, evals/ray | Its time |
|------------------------|--------------|-----------|-----------------------|----------------------|-----------------------------------------------|-----------|
| Perlin gradient | 78.40 | 8m27.5s | 34.60 | 34.60 | 20.81 | 3m43.2s |
| Lewis sparse convolution | 48.44 | 26m31.0s | 23.25 | 23.25 | 13.46 | 6m29.3s |
| Worley cellular | 45.70 | 25m54.7s | 32.56 | 43.78 | 21.29 | 14m25.9s |
The text cites "the excessive conservativeness of IA estimates", and says IA
compensates by being fast.
https://eprints.whiterose.ac.uk/id/eprint/3762/1/gamito_newraycastvisual.pdf
- **[fetched]** Knoll, Hijazi, Kensler, Schott, Hansen, Hagen, *Fast Ray Tracing of
Arbitrary Implicit Surfaces with Interval and Affine Arithmetic* (CGF 28(1), 2009).
Section 6.1.1 and Tables 1 and 2 report frames per second for IA against reduced
AA:
- RAA is "generally 1.5 to 2× faster", and "3 to 4 times faster" for functions
"with high bound overestimation";
- IA is faster for superquadrics.
These are **peer** references, with no oracle count of wasted subdivisions
**[not found]**.
https://sci.utah.edu/~knolla/cgrtia.pdf
- **[fetched]** Sharp and Jacobson, *Spelunking the Deep: Guaranteed Queries on General
Neural Implicit Surfaces via Range Analysis* (SIGGRAPH 2022), section 3.4 and Table 1.
Tightness is measured as "the typical size of a region which can be bounded away
from the zero":
| Method | 1D length | 3D volume | Ray-cast time (relative to the fastest) |
|---------------|-----------|-----------------|-----------------------------------------|
| interval | 0.011 | < 0.001 × 10⁻³ | 34.3× |
| affine (full) | 0.821 | 3.499 × 10⁻³ | 8.4× |
| affine (fixed)| 0.306 | 0.267 × 10⁻³ | 1.0× |
The prose says interval arithmetic "yields extremely pessimistic bounds". These are
**peer** references, with no oracle region size **[not found]**.
https://arxiv.org/abs/2202.02444
- **[fetched]** Mitchell, *Robust Ray Intersection with Interval Arithmetic* (Graphics
Interface 1990). It reports root-finding timings for its method and for peer
root finders. No pruning-against-oracle measure **[not found]**.
http://graphicsinterface.org/wp-content/uploads/gi1990-8.pdf
- Duff, *Interval Arithmetic and Recursive Subdivision for Implicit Functions and
Constructive Solid Geometry* (SIGGRAPH 1992), and Snyder, *Interval Analysis for
Computer Graphics* (SIGGRAPH 1992), were not retrieved in this pass.
https://doi.org/10.1145/142920.134027
## 4. Conservative rasterization and tile classification
- **[fetched]** Hasselgren, Akenine-Möller, Ohlsson, *Conservative Rasterization*
(GPU Gems 2, chapter 42):
- **Section 42.1.** It defines overestimated rasterization as every pixel whose cell
intersects the polygon, and underestimated rasterization as every pixel whose
cell lies wholly inside it.
- **Section 42.5.** Overdraw is discussed qualitatively: it depends on triangle
acuity and resolution. Figure 42-7 compares the two proposed algorithms only. No
count of excess fragments against the exact overlap set **[not found]**.
https://developer.nvidia.com/gpugems/gpugems2/part-v-image-oriented-computing/chapter-42-conservative-rasterization
- **[search]** Antochi, Juurlink, Vassiliadis, Liuha, *Scene management models and
overlap tests for tile-based rendering* (DSD 2004), and the companion *Efficient
tile-aware bounding-box overlap test*. In a search summary, the bounding-box
triangle-to-tile test "can generate up to 30% false positives" on common workloads,
measured against exact overlap. The full text could not be retrieved (the TU Delft
host refused the connection, and CiteSeerX returned HTML).
https://citeseerx.ist.psu.edu/document?doi=5c78f05f185b9513bf930588dd2a4ae334090620
## 5. Summary of reference kinds and reported figures
| Source | Reference | Figure reported |
|--------|-----------|-----------------|
| Bittner et al. 2005, §6.2 | oracle (exact PVS) | conservative PVS +47% / +76% (ratio ≤ 0.68 / ≤ 0.57) |
| Bittner et al. 2005, §6.3 | oracle | object discretization +17% (≈ 0.85); line-space 0.023–0.52 by scene and resolution |
| Bittner thesis, ch. 2 | oracle | defines `Q_o`, `Q_u` relative to `|S_E|` |
| Cohen-Or et al. 2003 | — | notes the PVS/visible-set ratio is rarely reported |
| Nirenstein et al. 2002 | oracle (its own exact) | percent culled; calls that metric crude |
| CHC++ 2008 | oracle (OPT, node level) | within a few percent of OPT (exact queries, not bounds) |
| HOM 1997 | self | % of model culled, normalized frame time |
| Li, Mueller, Kaufman 2003 | self / all non-empty | 2.8× speedup; 56% of non-empty voxels skipped; overhead ≈ 10% reported separately |
| Zellmann 2019 | self | fps against no skipping |
| Keeter 2020 | self (brute force, compiled) | 17× / 216× tape reduction; false-positive share not measured |
| Heidrich et al. 1998 | none (range width) | IA error up to 50% where AA < 1/256 |
| Gamito, Maddock 2007 | peer | IA 2.1–3.8× more evaluations per ray than reduced AA + optimisation on noise |
| Knoll et al. 2009 | peer | RAA 1.5–4× faster than IA |
| Sharp, Jacobson 2022 | peer | IA provable region about 75× smaller in 1D than full AA |
| GPU Gems 2, ch. 42 | peer | qualitative overdraw |
| Antochi et al. 2004 [search] | oracle (exact overlap) | bounding box up to 30% false positives |
On the interval side (section 3), none of the sources read in this pass reports a
pruning or culling fraction against an oracle mask, such as the truly empty cells or
the truly non-ambiguous tiles. They report only against self or peer baselines. The
oracle-referenced ratios found are all on the visibility side (sections 1.2 and 1.3),
and the one from rasterization (section 4) is search-only. No published figure was
found for interval-arithmetic culling of noise or warped-noise fields as a fraction of
an oracle.
## 6. Local context (from the eureka mind, not re-measured)
- **Question** `cultmath-tapes:question:wells-ceiling-oracle` (admitted 2026-10-03,
Imagination). At 3ac7250a it gives:
- warp 0: oracle efficiency 0.38, mask ceiling 0.96, cull fraction 0.95;
- warp D = 60: oracle efficiency 0.19, mask ceiling 0.99, cull fraction 0.84;
- a pre-pass that skips nothing: 0.14 against the oracle.
- **Finding** `cultmath-tapes:finding:cut-interval-ops.s2.wells-ceiling-contract-vacuous`
(Soul, confirmed by mutation). The mask-referenced (c) check reports 0.98 with the
wells skip disabled. Locations are in
`packages/cultmath/tests/CultMath.Tests/NoiseBoundTests.cs`: lines 1257–1260, 1437
and 1461.

View file

@ -0,0 +1,75 @@
# Union and record decode tolerance: prior art and CultCache's stance
Fork: thing2:question:contract-decode-tolerance. Follow-up: thing2:follow_up:cultcache-union-parity.
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 |
| FlatBuffers | page silent [fetched] | append-only evolution, explicit discriminants |
| 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.

View file

@ -0,0 +1,83 @@
# Cycles CPU FMA contraction: facts for the kernel-pin fork
Serves finding `cultmath-tapes:finding:cut-cycles-converters.s1.kernel-pin-unraised-fork` (kind Finding, InForce, Medium, admitted 2026-10-13, soul-cycles-converters-s1).
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
35. [src] Blender's Cycles render tests: item 10 (image thresholds, default 0.016/1%).
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).

View file

@ -0,0 +1,83 @@
# Decode stack safety: prior art (facts only)
Serves follow-up `cultlib-gaps:follow_up:decode-stack-safety` (campaign cultlib-gaps), source finding `thing2:finding:cut-contract-rs.s1.decode-stack-overflow-within-bound`.
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.
- .NET System.Text.Json: `JsonSerializerOptions.MaxDepth` default 0 meaning 64; exceeding throws `JsonException`. https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.maxdepth
- 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.

View file

@ -0,0 +1,103 @@
# 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.

View file

@ -0,0 +1,49 @@
# 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
which member is an element id.

View file

@ -0,0 +1,462 @@
# Foreign Records: Prior Art Survey
Facts file for the single-file "foreign records" fork (lay-back versus refusing
whole-view writes while holding records the writer does not own). Eyes pass,
2026-10-02. Facts and evidence pointers only; no recommendation.
Verification marks:
- **[fetched]** the claim was read on the cited page during this pass.
- **[search]** the claim came from a search-result summary of the cited page,
not a full read.
- **[memory]** recalled from the cited source, not re-read in this pass.
Treat it as unverified.
## 0. Our design, for comparison
Source: `src/GameCult.Caching/Contracts/cultcache-persistence-format.md` (main, 614fd445).
- Store is `[header, catalog, records]`; the catalog "must embed a catalog entry
for every schema referenced by the records it contains" (Embedded Schema Catalog).
- The stated purpose of the catalog is to let another implementation "decide
whether it can: read directly, soft-migrate with warnings, or refuse the data
honestly".
- Soft migration lists "persisted schema has extra slots the local reader can
safely ignore or preserve" and the warning "field preserved but not surfaced
natively". That is slot-level preservation inside a record the reader owns. The
contract does not mention records under schema ids the reader does not own;
the lay-back ruling lives elsewhere (grep of `src/**/*.md` for "foreign" or
"lay-back" found nothing).
- Concurrency: "A plain flush and an unconditional commit are last-writer-wins
... a single file is replaced by the writer's whole view." "Conditional commit
is the only safe multi-process write." C# and Rust implement conditional
commit; TypeScript and Python do not yet (Transaction Visibility).
Granularity note used throughout: most systems below handle unknown *fields
inside a record whose type the writer knows*. Our fork is about unknown *whole
records* (schema ids) inside a container the writer rewrites. Each entry says
which granularity it covers.
## 1. Serialization formats
### 1.1 Protocol Buffers: unknown-field preservation
- **What it does.** Unknown fields are "well-formed protocol buffer serialized
data representing fields that the parser does not recognize". Proto3 messages
"preserve unknown fields and include them during parsing and in the serialized
output, which matches proto2 behavior." [fetched] https://protobuf.dev/programming-guides/proto3/ (Unknown Fields)
- **Where preservation fails, per the docs.** Serializing to JSON; iterating
fields to populate a new message. Mitigation given: use binary, use
`CopyFrom()`/`MergeFrom()`. TextFormat prints unknown fields by number but
cannot parse them back. [fetched] same page.
- **History.** Proto3 shipped dropping unknown fields. v3.5.0 release notes:
"Unknown fields are now preserved in proto3 for most of the language
implementations for proto3 by default." C++ and Python offer
`DiscardUnknownFields()`, Java a `DiscardUnknownFieldsParser`, for callers who
relied on dropping. The release notes give no rationale. [fetched]
https://github.com/protocolbuffers/protobuf/releases/tag/v3.5.0
- **Why it was restored.** No official rationale document was found. The
user-facing request is issue #272 (2015-04-07): "I know that unknown fields
have been removed from proto3, but I am trying to get an explanation about why
this change was made". The thread body was not readable in this pass. [fetched,
partial] https://github.com/protocolbuffers/protobuf/issues/272. Secondary
accounts attribute the reversal to pass-through and read-modify-write
intermediaries in uncoordinated upgrades. [search]
https://github.com/scalapb/ScalaPB/issues/406, https://kmcd.dev/posts/protobuf-unknown-fields/.
**The primary reasoning is unverified.**
- **Cost and regret.** Go: preservation through `XXX_unrecognized []byte`
broke users who compared proto3 messages with `==` or used them as map keys.
[search] https://groups.google.com/d/msg/golang-nuts/F5xFHTfwRnY/sPv5nTVXBQAJ.
JSON round trips still drop unknown fields; there is an open request to keep
them. [search] https://github.com/golang/protobuf/issues/1390. A secondary
source flags preserved unknown fields as a way to smuggle unvalidated payloads
past validation at trust boundaries. [fetched] https://kmcd.dev/posts/protobuf-unknown-fields/
- **Granularity.** Field level, inside a message the reader's code knows.
Protobuf does not preserve unknown *message types*; the closest is
`google.protobuf.Any` (type URL plus bytes), which carries an opaque typed
payload as a declared field. [memory]
- **Differs from us.** There is no in-band schema. Unknown bytes are
uninterpretable without the newer `.proto`, and only tag and wire type are
self-describing. There is no container rewrite or multi-process store
concern; protobuf is a message codec.
### 1.2 Protobuf field-number reuse
- **Rule.** "Field numbers should never be reused. Never take a field number out
of the reserved list for reuse with a new field definition." Listed
consequences: "Developer time lost to debugging, A parse/merge error (best case
scenario), Leaked PII/SPII, Data corruption." Syntax: `reserved 2, 15, 9 to 11;`
and `reserved "foo", "bar";`. [fetched] https://protobuf.dev/programming-guides/proto3/
- Best practices: "Never re-use a tag number. It messes up deserialization."
Reserve deleted numbers. "Almost never change the type of a field." Renaming
breaks JSON and text format because they key on names. [fetched]
https://protobuf.dev/best-practices/dos-donts/
- Kleppmann (2012): "you must never reuse the tag number for another field in
future, because you may still have data stored that uses that tag for the field
you deleted". [fetched] https://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html
- **Named public incidents:** none found in this pass. The docs' consequence
list is the strongest primary evidence; blog posts repeat it. **Unverified.**
- **Differs from us.** Our `schemaId` is a content hash of the canonical slot
schema, so changing slot meaning changes the id. The reuse hazard moves from
"number collides" to "a record under an old id is carried, or reinterpreted,
after an undeclared rename".
### 1.3 Avro and schema registries
- **Resolution.** The reader needs the writer's schema. Fields in the writer
schema but not the reader's are ignored. Reader fields missing from the writer
take the reader default, or raise an error when there is no default. Aliases
map renames. [fetched] https://avro.apache.org/docs/1.11.1/specification/ (Schema Resolution)
- **Object Container Files** carry the schema once in the header (`avro.schema`,
required). One file has one writer schema. [fetched] same page.
- **Kleppmann (2012):** Avro needs "the exact same version of the schema as the
writer of the data", so you either "annotate each record with its schema
version" or use "a schema registry". [fetched] blog above.
- **Confluent wire format:** byte 0 is the magic byte (0), bytes 1 to 4 are the
big-endian schema id; the deserializer fetches the writer schema by id.
[fetched] https://docs.confluent.io/platform/current/schema-registry/fundamentals/serdes-develop/index.html
- **Compatibility modes:** BACKWARD (the default), FORWARD, FULL, their
`_TRANSITIVE` forms, and NONE. BACKWARD is the default "so that you can rewind
consumers to the beginning of the topic". Upgrade order follows the mode:
consumers first for BACKWARD, producers first for FORWARD. [fetched]
https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html
- **Failure mode.** Resolution projects writer data onto the reader schema, so
writer-only fields are gone from the decoded object. A read-modify-write by an
older reader loses them unless the application keeps the writer record.
[inference from the spec; no Avro doc states it as a warning]
- **Differs from us.** Kafka records are immutable log entries, so consumers
never rewrite the producer's records. A container file is written once by one
writer. Neither case has whole-file rewrite by several processes.
### 1.4 Thrift
- Generated readers switch on field id and call
`TProtocolUtil.skip(iprot, schemeField.type)` in the default case: unknown
fields are skipped. [search; generated-code pattern] Kleppmann (2012) says the
same of protobuf and Thrift: the parser "can figure out how many bytes it needs
to skip". [fetched]
- **Conflict.** "Thrift: The Missing Guide" says unknown fields "are not
discarded, and if the message is later serialized, the unknown fields are
serialized along with it". That text reads as copied from protobuf docs.
[fetched] https://diwakergupta.github.io/thrift-missing-guide/. Apache Thrift's
generated Java skips without retaining. **Retention is unverified and likely
false for stock Apache Thrift.**
- Rules: never change numeric tags; new fields optional; prefix removed fields
`OBSOLETE_`. [fetched] Missing Guide.
### 1.5 Cap'n Proto
- Field ordinals are append-only: "as long as each new member's number is larger
than all previous members"; "You cannot change a field, method, or enumerant's
number." [fetched] https://capnproto.org/language.html
- Unknown fields survive a copy: "If you copy the struct from one message to
another (e.g. by calling a set() method on a parent object), the extra fields
will be preserved." Caveat: "copying a struct value from another message into
an element of a list will truncate the value". [fetched] https://capnproto.org/cxx.html
- The wire format carries just enough structure (struct or list, sizes) to copy
an object recursively without its schema. [search]
https://capnproto.org/encoding.html. Preservation is a property of the encoding,
not of a per-type rule.
- **Differs from us.** The copy is opaque; nothing is reinterpreted. There is no
catalog and no store.
### 1.6 FlatBuffers
- Add new fields at the end; never remove one, mark it `deprecated` instead (no
accessors are generated); explicit `id` attributes relax ordering. Old readers
ignore unknown fields. [search] https://flatbuffers.dev/evolution/
- The object API (unpack, mutate, pack) builds a fresh buffer from native
fields. Whether unknown fields survive that round trip: **not verified in
this pass** [memory: they do not].
- Known defect: C++ object API with deprecated fields generated non-compiling
code. [search] https://github.com/google/flatbuffers/issues/5186
### 1.7 Microsoft Bond
- `bonded<T>` defers deserialization and holds a reference to the serialized
form. "Using bonded<Response> allows the intermediary to aggregate responses,
preserving their full content, even though schema of Response is not known
when the aggregator is built." Transcoding through a bonded value preserves
fields not in `T`. [fetched] https://microsoft.github.io/bond/manual/bond_cpp.html
- Untagged protocols (Simple Binary) "serialize only data and thus require that
consumers know the payload schema via some out-of-band mechanism"; Bond also
has runtime schemas (`SchemaDef`) for that. [fetched; SchemaDef from memory]
- Changing ordinals is a breaking change. [fetched]
- **Note.** Bond's preservation is an explicit opt-in wrapper type at the
declared pass-through point, not default retention of everything.
### 1.8 MessagePack-based schema systems
- MessagePack itself has no schema. MessagePack-CSharp (`[Key(n)]` int keys, the
basis of our slot layout) has no documented unknown-key or extension-data
retention for typed objects. A search found no such feature and no issue
requesting it. **Unverified absence.**
https://github.com/MessagePack-CSharp/MessagePack-CSharp
- Comparable opt-in field-level retention in other ecosystems:
Newtonsoft.Json and System.Text.Json `[JsonExtensionData]` capture unmapped
properties into a dictionary and write them back; serde `#[serde(flatten)]` into
a map does the same, and is incompatible with `deny_unknown_fields`. [search]
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/migrate-from-newtonsoft,
https://serde.rs/attr-flatten.html. All are field level and opt in per type.
- **FIDL (Fuchsia), RFC-0033:** messages with unknown fields "MUST be validated
and parsed successfully"; the default is `flexible` (unknown values allowed and
exposed), and the `strict` keyword makes unknown fields invalid. Unknown
handles must be closed by default. The RFC frames strictness as a security tool
for protocols where misinterpreting new data is dangerous. [fetched]
https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs/0033_handling_unknown_fields_strictness
## 2. Embedded stores and document databases
### 2.1 SQLite
- **Unknown tables.** The writer modifies pages through the B-tree and rollback
or WAL journal. Tables a program never touches are never rewritten, so there is
no rule about them. [memory; consistent with the file format doc]
- **Unknown schema syntax.** A library older than 3.31.0 opening a schema with
generated columns "will perceive the generated column syntax as an error and
will report that the database schema is corrupt". Older versions can read and
write newer files "as long as the database schema does not contain features
... not understood by the earlier version". [search] https://sqlite.org/gencol.html.
The whole database is refused, not just the table.
- **File format versions.** Write version > 2: "the database file must be treated
as read-only". Read version > 2: "that database cannot be read or written".
[fetched] https://www.sqlite.org/fileformat2.html (offsets 18 and 19). This is
a format-level read-only fallback for content the library cannot safely write.
- **Schema change across connections.** The schema cookie (offset 40) is bumped
on every schema change. A prepared statement checks it and reprepares or fails
with `SQLITE_SCHEMA`. [fetched] same page.
- `user_version` (offset 60) and `application_id` (offset 68) are application
owned; SQLite does not use them. [fetched]
- **Whole-file rewrite.** SQLite's own comparison: custom and wrapped
pile-of-files formats "usually require a rewrite of the entire document in
order to change a single byte", and application concurrency logic for them is
"a notorious bug-magnet". [fetched] https://www.sqlite.org/appfileformat.html
- **Differs from us.** The schema is SQL text in `sqlite_schema`, not a typed
slot catalog. Writes are partial and in place, and there is one implementation
rather than five.
### 2.2 LMDB
- Opaque byte keys and values, no schema; named sub-databases. Copy-on-write
B+tree with MVCC. One writer at a time, serialized by a mutex in the lock file;
readers do not block the writer and the writer does not block readers;
several processes may open one environment. [search; lmdb.tech/doc was not
reachable over TLS in this pass] https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database,
https://lmdb.readthedocs.io/
- `MDB_INCOMPATIBLE`: an operation does not match a database's flags (for
example dupsort), or "the database type changed". [search]
- **Differs from us.** There are no types, so "foreign" is not a concept;
untouched keys are never rewritten.
### 2.3 Core Data (Apple)
- **Model mismatch is refused.** "The model used to open the store is
incompatible with the one used to create the store" (Cocoa error 134100)
unless a migration runs: lightweight (inferred mapping, using
`NSMigratePersistentStoresAutomaticallyOption` and
`NSInferMappingModelAutomaticallyOption`) or custom/staged. [search]
https://developer.apple.com/forums/thread/100639
- Store metadata holds per-entity version hashes (`NSStoreModelVersionHashes`).
`isConfiguration(withName:compatibleWithStoreMetadata:)` compares them with
the model. [search]
https://developer.apple.com/documentation/coredata/nsmanagedobjectmodel/isconfiguration(withname:compatiblewithstoremetadata:)
- **Atomic stores (XML, binary) are the closest analogue to our single file.**
They keep the whole object graph in memory and save the whole file: "the old
file is not deleted until the new one has been successfully written."
[fetched] https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/PersistentStoreFeatures.html.
Combined with the refusal above, an atomic store is never rewritten by a model
that does not cover every entity in it. The page says nothing on multi-process
access to atomic stores. [fetched: absence]
- **Differs from us.** Single runtime, entity-hash catalog in metadata, a
migration-or-refuse gate at open. No foreign entity is ever carried.
### 2.4 Realm
- Opening with a changed schema requires a higher `schemaVersion`, plus a
`migrationBlock` where needed; otherwise "Migration is required". Added and
removed properties are applied to the on-disk schema automatically on
migration. [search] https://github.com/realm/realm-js/issues/4752,
https://realm.netlify.app/docs/swift/latest/
- A changelog entry allows opening old frozen versions "with a schema that
contains additional classes, but not additional properties". [search]
https://github.com/realm/realm-core/releases?page=5
- Whether a Realm opened with a class *subset* keeps the other tables untouched:
**not verified** [memory: the tables are kept, since Realm writes in place by
table and does not rewrite the whole file].
- Realm file-format upgrades are one-way. [memory]
### 2.5 CouchDB and PouchDB
- `PUT /{db}/{docid}` writes a new revision of the whole document; there is no
field merge, and omitted fields are gone in the new revision. A missing or
stale `_rev` returns `409 Conflict`: "specified revision is not latest for
target document". The revision may be given in the body, as `rev`, or in
`If-Match`. [fetched] https://docs.couchdb.org/en/stable/api/document/common.html
- Under replication, conflicting revisions are all kept; one deterministic
winner is chosen and the others are marked `_conflicts`; CouchDB does not merge.
[search] https://docs.couchdb.org/en/stable/replication/conflicts.html
- There is no schema; unknown fields survive only if the client writes them
back. Document-mapper layers drop them: Mongoose's `strict` option "ensures
that values passed to our model constructor that were not specified in our
schema do not get saved to the db", and is "enabled by default". [fetched]
https://mongoosejs.com/docs/guide.html (MongoDB, cited as the same pattern).
- **RxDB** (on PouchDB or other storage): any schema `version` > 0 requires
`migrationStrategies` for each step; a strategy returning `null` deletes the
document; writes during a running migration fail with `COL25`; in multi-tab
setups "exactly one tab is running a migration of a collection". [fetched]
https://rxdb.info/migration-schema.html. Opening storage written by a newer
RxDB major is refused ("Cannot open database state with newer RxDB version").
[search] https://github.com/pubkey/rxdb/issues/5836
- **Differs from us.** The per-document `_rev` gives optimistic concurrency at
record granularity, and documents are never rewritten as a side effect of
writing another.
### 2.6 IndexedDB
- No field schema: values are structured clones, stored by value. Schema is
object stores and indexes, versioned by an integer. [fetched] https://www.w3.org/TR/IndexedDB/
- Opening with a higher version fires `upgradeneeded` inside a versionchange
transaction. Opening with a *lower* version than the stored one fails with
`VersionError`: "An attempt was made to open a database using a lower version
than the existing version." Older code is refused outright. [fetched] same spec.
- Other open connections receive `versionchange`; if they stay open the upgrader
gets `blocked`. [fetched] same spec and
https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/blocked_event
- **Differs from us.** The browser engine is the single writer implementation
and the schema has one integer owner per database.
### 2.7 Whole-file rewrite while preserving unknown content
Systems found that rewrite a container while keeping parts they do not
understand:
- **Automerge binary format.** Programs that edit documents must "maintain all
columns, even those that they don't understand the meaning of" and write null
into unknown columns for new rows. Implementations "must preserve operations
containing actions they don't support". Unknown value type codes: "MUST read and
store the type code and `length` bytes". Changes are SHA-256 hashed over their
binary form, so byte preservation is required for hash integrity. [fetched]
https://automerge.org/automerge-binary-format-spec/. The unit carried is an
immutable, content-addressed change; nothing preserved is ever re-decided by
the carrier.
- **Core Data atomic stores** rewrite the whole file but refuse to open under an
incompatible model (2.3), so they never carry unknown entities.
- No system was found that rewrites a whole mutable file while carrying
mutable records of unknown types that other writers may change concurrently.
**Absence of evidence, not a verified negative.**
## 3. Literature
- **Kleppmann, *Designing Data-Intensive Applications* (1st ed., O'Reilly 2017),
ch. 4 "Encoding and Evolution", section "Dataflow Through Databases".** [memory,
not re-read] The section says an older version of code may read, update and
write back a record written by newer code, and newer fields can be lost unless
the old code keeps unknown fields. Figure 4-7 shows this. The section contains
the line "data outlives code" [search; confirmed in secondary notes
https://gist.github.com/bcherny/b870a60d1650973df7e400c8603ac76d]. It also says
rewriting data into a new schema is possible but expensive, so most databases
avoid it. **The exact wording is unverified.**
- **Scherzinger, Klettke, Störl, "Managing Schema Evolution in NoSQL Data
Stores", DBPL 2013.** Contrasts eager migration with *lazy* migration (migrate
on read or touch) for schema-less stores whose data grows heterogeneous.
[search] https://arxiv.org/abs/1308.0514
- **Ringlstetter, Scherzinger, Bissyandé, "Data model evolution using
object-NoSQL mappers: folklore or state-of-the-art?", BIGDSE@ICSE 2016,
pp. 33-36.** Mapper evolution annotations (rename, remove, transform on load)
are used on GitHub, but mostly for tasks other than evolving the data model.
[search] https://dblp.org/rec/conf/icse/RinglstetterSB16.html
- **Scherzinger and Sidortschuck, "An Empirical Study on the Design and Evolution
of NoSQL Database Schemas", ER 2020.** [search, not read]
https://arxiv.org/pdf/2003.00054
- **Bancilhon and Spyratos, "Update semantics of relational views", ACM TODS
6(4), 1981, pp. 557-575.** A view update is translated by holding a chosen
*complement* constant; a complete set of view updates has a translator iff it
is translatable under constant complement. [search]
https://dl.acm.org/doi/10.1145/319628.319634. Mapping (this survey's framing,
not the paper's): a writer's owned records are a view of the store, the foreign
records are the complement, and lay-back is constant-complement translation.
- **Foster, Greenwald, Moore, Pierce, Schmitt, "Combinators for bidirectional tree
transformations", POPL 2005 / TOPLAS 2007.** A lens is `get: S -> V` and
`put: V x S -> S`. Laws: GetPut, `put(get(s), s) = s`; PutGet,
`get(put(v, s)) = v`; PutPut is the stronger optional law. `put` takes the
source `s` as an argument, so the complement is restored from whatever `s` is
supplied. [search for the paper; the law statements are from memory, and one
search summary had GetPut and PutGet swapped]
https://www.cis.upenn.edu/~bcpierce/papers/lenses-toplas-final.pdf
- **Litt, van Hardenberg, Henry, "Project Cambria" (Ink & Switch, Oct 2020).**
Bidirectional lenses between schema versions, arranged as a graph. In
Cambria-Automerge every write is tagged with its writer's schema and translated
at read time, so "old changes can be evolved using lenses that didn't even
exist at the time of the original change". Lenses travel inside the document.
Stated limits: lossy conversions (scalar to array), one-way changes
(split/merge), no external lookups, untested performance. "There are limits to
interoperability." [fetched] https://www.inkandswitch.com/cambria/
- **Local-first and CRDT, Automerge:** see 2.7. Unknown ops, columns and value
types from newer peers are preserved byte for byte and are immutable once
written. [fetched]
## 4. Concurrency facts for whole-file rewrite
- **Last-writer-wins on whole replace** loses a concurrent writer's changes
unless the write is conditional. Our contract already says this (section 0).
- **HTTP optimistic concurrency.** RFC 9110 §13.1.1 If-Match exists to prevent
the "lost update" problem; a failed precondition returns 412 Precondition
Failed (§15.5.13). [memory; the page fetch truncated before those sections]
https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.1
- **Per-record CAS:** CouchDB `_rev`, 409 (2.5).
- **Git ref update:** `git update-ref <ref> <new> <old>` stores the new value
"after verifying that the current value of the <ref> matches <old-oid>".
Transactions lock every ref (`.lock` files) at `prepare`; "If all <ref>s can be
locked with matching <old-oid>s simultaneously, all modifications are
performed. Otherwise, no modifications are performed." [fetched]
https://git-scm.com/docs/git-update-ref. The pattern: take an exclusive lock
file, compare the old value under the lock, write, rename into place.
- **SQLite** serializes writers by file lock (rollback) or WAL write lock;
schema-cookie checks catch schema drift between prepare and run (2.1).
- **LMDB:** single writer mutex across processes, MVCC readers (2.2).
- **IndexedDB:** schema change is an exclusive versionchange transaction that
waits on other connections (2.6).
- **RxDB:** one migrating tab, writes refused during migration (2.5).
- **Crash consistency of temp-plus-rename.** Pillai et al., "All File Systems Are
Not Created Equal: On the Complexity of Crafting Crash-Consistent
Applications", OSDI 2014. The ALICE tool found 60 crash vulnerabilities across
11 widely used applications; persistence properties, including how rename
orders against data writes, vary across six Linux file systems. [search]
https://research.cs.wisc.edu/adsl/Publications/alice-osdi14.pdf. Whether
`fsync` of the temp file and its directory is needed for durability of the
rename: [memory: yes on Linux ext4 and others]. Windows `MoveFileEx` with
`MOVEFILE_REPLACE_EXISTING` and `ReplaceFile` atomicity guarantees: **not
verified in this pass.**
## 5. Cross-cutting facts, as observed (not conclusions)
- Every system found that preserves unknown data does so at *field* level
inside a record whose type the code owns (protobuf, Cap'n Proto, Bond opt-in,
JSON extension data, serde flatten), or as *immutable content-addressed*
units (Automerge changes).
- Systems that hold several record types in one store either rewrite only what
they touch (SQLite, LMDB, Realm [unverified], CouchDB, IndexedDB), or rewrite
the whole file and refuse a model that does not cover the file (Core Data
atomic stores).
- Systems where older code meets a newer schema at open time do one of:
refuse to open (IndexedDB `VersionError`, Core Data 134100, RxDB newer-major,
SQLite unknown schema syntax), go read-only (SQLite write version > 2), or
require a migration before any write (Realm, RxDB, Core Data).
- Protobuf moved from dropping to preserving (3.5). Its documented residual
losses are in conversion paths (JSON, field-by-field copy), and Go's
preservation broke message comparability.
- FIDL made preserve-and-expose the default and added `strict` as an opt-in
refusal, justified on security grounds.
## Not verified in this pass
- Primary rationale for restoring proto3 unknown fields in 3.5.
- Any named incident caused by protobuf field-number reuse.
- Stock Apache Thrift retention of unknown fields (the Missing Guide says yes,
the generated-code pattern says skip).
- FlatBuffers object-API round trip of unknown fields.
- MessagePack-CSharp unknown-key retention (absence).
- Realm behaviour opening with a class subset.
- Exact DDIA wording and figure number.
- LMDB primary docs (TLS failure); RFC 9110 section text.
- Windows rename and replace atomicity semantics.

View file

@ -0,0 +1,115 @@
# Ground motion law: prior art (facts only)
Serves fork: cultmath-tapes, site-ground motion law (invariant `one-motion-owner`, target `cultmath-tapes:target:r2`), blocks `cut-site-ground`.
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
Source: https://www.advances.realtimerendering.com/s2015/The%20Real-time%20Volumetric%20Cloudscapes%20of%20Horizon%20-%20Zero%20Dawn%20-%20ARTR.pdf (99 slides, read)
- 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.
### 1.2 Nubis, Evolved, Schneider, SIGGRAPH 2022
Source: https://advances.realtimerendering.com/s2022/SIGGRAPH2022-Advances-NubisEvolved-NoVideos.pdf (read)
- 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.
### 1.3 Nubis3 (Nubis Cubed), Schneider, SIGGRAPH 2023
Source: https://advances.realtimerendering.com/s2023/Nubis%20Cubed%20(Advances%202023).pdf (read)
- Voxel clouds, slide "Sampling Density": `inSamplePosition -= float3(cCloudWindOffset.x, cCloudWindOffset.y, 0.0) * voxel_cloud_animation_speed`. Wind offset only; no curl, no flow field.
- 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).
### 1.5 Toft, Bowles, Zimmermann (Studio Gobo), "Optimisations for Real-Time Volumetric Cloudscapes", arXiv 2016
Source: https://arxiv.org/abs/1609.05344 (PDF read)
- 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.
## 3. Flow maps, curl noise, advected textures
### 3.1 Flow maps, Vlachos, Valve, SIGGRAPH 2010 "Water Flow in Portal 2"
Source: https://www.advances.realtimerendering.com/s2010/Vlachos-Waterflow(SIGGRAPH%202010%20Advanced%20RealTime%20Rendering%20Course).pdf (read)
- 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
Source: https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph2007-curlnoise.pdf (read, 3 pages)
- 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].
### 3.3 Advected textures, Neyret, SCA 2003
Source: https://evasion.inrialpes.fr/Publications/2003/Ney03/neyret161.pdf (read)
- 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.

View file

@ -0,0 +1,291 @@
# An integration-native volumetric noise domain: probe
Campaign `cultmath-tapes`, Imagination probe, 2026-10-03, under ruling
`cultmath-tapes:ruling:integration-native-noise-domain`. This is research under the
parked tape target. It has no spec, no shipped-field change and no committed code.
It follows `analytic-integration-feasibility.md`, which found that exact noise
integrals are cheap but that the shipped densities are nonlinear in the noise. The
question here is the reverse one. Suppose a domain is designed so that span
integration holds by construction. Does it buy more visual detail per unit cost
than the shipped fields?
On the operator's instruction (relayed by Self), the reference is physical, not
Aetheria's tuned compositing:
- transmittance is Beer–Lambert, `T = exp(-τ)`;
- the images use single scattering with Henyey–Greenstein `g = 0.6` and albedo 1;
- both fields are integrated the same way.
The shipped look is a comparison, not a target.
## Verdict
**On the void, yes, by a wide margin. On the wells, modestly.** At or below the
shipped march's cost, the integration-native field carries much more resolvable
detail, and it is exact. The catches:
- The look changes character. The default cascade reads as fibres or brush strokes,
not as soft cloud.
- The cost is traversal, not integration.
- The field cannot be ray-marched at all. Every consumer, shadow rays included,
must use the exact path.
Headline numbers, 512 x 256, single-threaded C# on Yggdrasil:
| Void, transmittance image | Cost per pixel | vs shipped march | msf (grad²/var) | Power above 1/8 cycle/px | Pixels with \|∇T\| > 0.05 |
|---|---|---|---|---|---|
| Shipped field, converged reference (595 samples) | n/a | n/a | 0.0111 | 0.20% | 0.00% |
| Shipped march (11.4 samples) | 28.9 µs (44 snoise3) | 1.00x | 0.0178 | 0.71% | 0.00% |
| Integration-native, 2 levels, exact | 13.9 µs | **0.48x** | **0.0755** | **2.95%** | 41.9% |
| Integration-native, 3 levels, exact | 35.7 µs | 1.24x | 0.125 | 9.15% | 54.7% |
| Integration-native, 6 levels (LOD stops at 4 to 5), exact | 74.2 µs | 2.57x | 0.159 | 12.5% | 60.9% |
- **Mean density is matched.**
- 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,
evaluated per kernel, at no cost to exactness.
## Measurements
**Setup.**
- Probe source: scratch clone `probe-clone@5ec9f529`,
`packages/cultmath/probe/DomainProbe/Program.cs`, never merged.
- Run on Yggdrasil through the verify stopgap: dotnet 10 container, 4 CPUs, tiered
JIT off.
- Full log: Self's scratchpad, `dom-full1.log`.
- The scenes are those of the first probe, with fixed cameras:
- Void: the shipped orbiting camera at time 20, at the 1920-wide f = 935 field of
view, rendered at 512 x 256.
- Wells: warp D = 60, camera at xz (1200, 0) looking at the central well, slopes
x ±1 and y from −0.85 to 0.15.
- The window is the shipped field's active intervals. Every method integrates the
same window.
**Cost.**
- Single-threaded C#, best of three passes over the same 2048 random pixels.
- The host is shared, and absolute timings moved by up to 2x between the smoke run
and the full run; Flow went from 125 to 660 ns. Ratios within one run are the
reliable numbers.
- Flow is 47% of a shipped sample in this run, so a flow-cached march would cost
about 15 µs on the void and 31 µs on the wells.
| Config (void) | Cost vs march | Roots visited / nodes tested / kernels integrated per pixel | Kernels per level |
|---|---|---|---|
| L1 | 0.12x | 43 / 8 / 8 | 8 |
| L2 | 0.48x | 59 / 25 / 20 | 8, 12 |
| L3 | 1.24x | 74 / 42 / 28 | 8, 12, 8 |
| L4 | 1.81x | 82 / 60 / 35 | 8, 12, 8, 7 |
| L6 (default) | 2.57x | 93 / 86 / 36 | 8, 12, 8, 7, 1, 0 |
| L6, no child placement | 1.79x | 93 / 91 / 39 | same metrics as L6 |
| L6, no LOD | 2.95x | 93 / 103 / 49 | 8, 12, 8, 7, 7, 7 |
| Config (wells) | Cost vs march | Kernels integrated per pixel |
|---|---|---|
| L1 | 0.04x | 5 |
| L2 | 0.23x | 11 |
| L3 | 0.49x | 16 |
| L4 | 0.77x | 20 |
| L6 | 1.20x | 25 |
| L6, no child placement | 0.62x | 27 |
**Where the time goes.**
- The integrals are about 36 kernels × 10 ns, or 0.4 µs of a 74 µs pixel.
- The rest is traversal:
- roots visited but missed: 93 visited for 8 hits, because AABBs of elongated
rotated ellipsoids are loose;
- child generation;
- placement lookups. The wells' placement costs a 5-bowl `Sdf`, and dropping
child placement halves the wells cost with identical metrics.
**Aliasing.** One sample per pixel against four on the default void field: |ΔT| has
an RMS of 0.015 (p90 0.022). At 4 spp the msf is 0.146 against 0.159. So about 92%
of the measured detail is resolved structure, not sparkle. Exact along the ray is
not the same thing as antialiased across the pixel; the LOD rule is the prefilter.
**Variants, void L6 (msf / power above 1/8).**
- k = 1 default: 0.159 / 12.5%.
- Hard k = 0: 0.145 / 11.0%.
- Gaussian (erf): 0.174 / 14.2%, at 0.89x the polynomial cost here. The erf kernel is
3x the polynomial kernel in isolation, but the kernel is not where the time goes.
## Images
All images are in Self's session scratchpad,
`C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-CultLib\b7e842a1-2045-474e-a51b-240d832b770b\scratchpad\integration-domain-images\`:
- **Transmittance, 512 x 256.** Shown as `sqrt(1 − T)`, white for opaque.
- `void-T-shipped-ref.png`, `void-T-shipped-march.png`.
- `void-T-IN-L1.png`, `void-T-IN-L3.png`, `void-T-IN-L6.png`,
`void-T-IN-L6-hard.png`, `void-T-IN-L6-gauss.png`.
- The wells equivalents, which are silhouettes and uninformative.
- **Single scattering, 256 x 128.**
- Method: HG g = 0.6 and albedo 1. The point light is at the void's sun with 1/r²
falloff; the wells use a directional sun with a sky background.
- Each view sample takes a shadow ray: a 2-world march for the shipped field, an
exact traversal for the integration-native field.
- Tonemap: `1 − exp(−1.5 L / p99(shipped))`, then gamma 2.2, the same exposure for
both fields.
- `void-SS-shipped.png`, `void-SS-IN-L6.png`, `wells-SS-shipped.png`,
`wells-SS-IN-L6.png`.
What they show:
- The shipped void is a soft, low-contrast haze.
- The integration-native void is dense flow-aligned streaks with deep gaps, like
hair or brush strokes.
- The shipped wells are smooth rolling dunes. The integration-native wells are
layered pancakes with streaked detail.
## The catches
1. **The character is a design parameter, and this probe picked one.**
- High anisotropy, axis-chained children and an intermittent `u²` split give
fibres.
- A cloudier look would need rounder children, wider offsets and less
intermittency. That trades away some measured detail and was not swept.
- The detail metrics measure spatial frequency, not taste. The operator has to
look at the images.
2. **The cost is a BVH, not arithmetic.**
- Integration is nearly free. Traversal, child generation and placement dominate.
- Tighter root bounds (OBB tests before AABB bins), cheaper placement and fewer
children are obvious levers, but none was measured.
- On a GPU, variable-length kernel lists per ray diverge. 3DGRT (Moënne-Loccoz et
al. 2024 [memory]) puts the same problem on hardware ray tracing.
- CPU C# ratios do not transfer to the GPU, where snoise is relatively cheaper.
3. **It is all or nothing.**
- This field cannot be marched. At the shipped step, |ΔT| is a median 0.14.
- Shadow rays, light probes and any sampler consumer need the exact traversal too.
The single-scattering pass here cost an exact shadow traversal per view sample:
257 s for both void SS images together, against 54 s for the whole 512 x 256
shipped transmittance set.
- Lighting has no closed form. The in-scatter integral is still quadrature. What
the domain makes exact is every optical depth inside it. A per-kernel lighting
cache would amortise shadows, but that is a different shading model (splat-like),
and so a product fork.
4. **Exact along the ray, not across the pixel.** The LOD truncation is the
prefilter. 8% of the measured detail at 1 spp is aliasing.
5. **The wells gain little** because the shipped wells fog is near-opaque. Its look
is the surface silhouette, and that is set at root scale. The domain pays most
where the medium is semi-transparent, as in the void.
6. **Timing noise.** Absolute costs vary up to 2x between runs on the shared host.
The cost ratios above are within one run.
## What would change the verdict
- A sweep toward a cloud-like character: round children, Nc = 8, wider offsets,
Gaussian kernels. It should be judged on the single-scattering images against the
same cost.
- Tight root culling: an exact ellipsoid-versus-bin test, or roots stored by
oriented bound. If roots visited fall from 93 to near the 8 hits, the void L3 cost
would fall well under the march.
- A GPU port of one level-limited configuration, to replace the C# ratios with
shader timings.

View file

@ -0,0 +1,151 @@
# 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).
# Eyes facts: inverse procedural modelling (mesh -> parametric program)
Date 2026-10-10. Tags: FETCHED (page/file read this session), SEARCH (search-result digest only), KNOWLEDGE (unverified recall).
Search digests come from a small model; SEARCH numbers are not paper-verified.
## 0. What exists in GameCult
- FETCHED F:\Projects\CultLib\docs\cultmath-tape-target.md: steps 2-5 (tape, pruning, HLSL interpreter, meshing) PARKED (2026-10-02,
ruling tape-target-unparks-when-asura-stable). Step 1 intervals + affine forms are on main (docs/cultmath-interval-ground-cut.md).
The "grammar of nested brushes" is operator intent only (quote 2026-10-02: space station from "a grammar of nested brushes");
doc says the station project is "a separate project with its own target". Chosen shape (c): port Fidget ideas into CultLib (Fidget =
MPL-2.0 Rust, v0.5.1). Meshing plan: surface nets + Asura tile pass (Newton refinement onto f=0, crease snapping from field gradients);
"Thin authored bevels can be shaded by their analytic normals". Tape gradients "reuse CultMath's analytic-gradient primitives".
- FETCHED grep CultLib\docs for brush/tape: "brush" hits in cultmath-interval-ground-cut.md are Aetheria noise/gravity-well field brushes
(PowerBrush (1-x^2)^16 etc.), not CSG brushes. No CSG brush grammar or tape code exists in CultLib; only the parked doc.
- FETCHED (voidbot) separate repo GameCult/GameCult.Geometry.Csg ("CultGeometry CSG", Rust; local F:\Projects\GameCult.Geometry.Csg).
Mesh-based convex plane-splitting CSG (not SDF), RealtimeCSG-shaped API: ordered Additive/Subtractive/Intersecting brush streams, boxes,
oriented boxes, CylinderZ/DomeCapZ/FloretArm, dirty-frontier, OBJ output, plus domain-tree/selected-cut LOD schemas (src/cult_geometry).
Doc text: "agents should revise intent by moving, adding, or renaming brushes, not by poking triangles"; "brush list is the durable
design surface. The mesh is a cache."
## 0b. The earlier "vibe geometry" attempt
- FETCHED voidbot gamecult-site GameCult/Projects/VibeGeometry.md: repo GameCult/VibeGeometry, started 2026-05-04, last public push
2026-05-07; "agent-authored Blender scene generation: geometry nodes, Python builders, semantic graph contracts, render evidence, and
the ongoing war against decorative node spaghetti". Org ledger: "Started as architecture and persistence scaffolding, then dived almost
immediately into CSG grammar, parity fixtures, native bridge probing, mesh generation, and performance work."
- FETCHED local git F:\Projects\VibeGeometry-cultgeometry-rename (93 commits). Commit 09bff8c (2026-05-29, "Focus VibeGeometry on vg-csg")
deleted the scene-generation tree; recoverable at 09bff8c^: docs/research/procedural-doctrine.md (~1000-line "spatial reasoning
playbook"), dream-machine-grammar.md, geonodes-translation-corpus.md, ircss-french-houses-* (translations of Blender node graphs into
Geometry Script Python), builds/{aetheria-bloom-habitat, lucent-tether-habitat, dream-machine-level, industrial-maintenance-loop}.md,
notes/architecture-rationale.md, a "reusable procedural helper library" (aa4d49d). The DSL was Blender Python via Geometry Script
(fork F:\Projects\geometry-script; upstream PR carson-katri/geometry-script#69 linked by Metacrat 2026-05-04) emitting geometry-node
graphs, plus the Rust brush DSL (LevelDsl: solid_box, cut_box, solid_oriented_box, dome_cap_z, floret_arm).
- FETCHED notes/architecture-rationale.md @09bff8c^, section "Core Failure": "The central failure mode is local plausibility without global
coherence. An agent can write plausible Blender Python, create a large node graph, and still lose the visual machine ... the script
runs, the graph exists, the render has objects in it, and somehow the target has been replaced by decorative procedural fog".
"Seductive proxies": valid Python, non-empty renders, high node counts, pretty materials, parameter sliders.
procedural-doctrine.md: "Ornament in the wrong frame is not detail. It is a confident lie". Commit titles record frame errors:
"Correct Bloom terrace slum orientation", "Correct Lucent dome spin-gravity frame", "Move Lucent ray florets outside dome".
- FETCHED VibeGeometry README: "failed experiments are useful without being allowed to hold a steering wheel"; old mission "deleted
from the live tree"; live mission narrowed to runtime CSG (domain tree -> LOD CSG -> selected cut -> triangles).
- NOT FOUND: no indexed operator or agent statement that models "weren't good enough at spatial reasoning to use the DSL". voidbot
search_history (Discord) returned only unrelated hits. The repo docs above are the only failure statements in project words
(coherence and frame errors; the phrase "spatial reasoning" appears only in the playbook title and its body).
## 1. Inverse CSG / shape-program synthesis
Most train or test on DeepCAD / ShapeNet / ABC / Infinigen; none retrieved was evaluated on noisy AI-generated hard-surface meshes.
- InverseCSG (Du et al., SIGGRAPH Asia 2018, MIT). SEARCH: mesh/point samples -> CSG program; geometric processing (RANSAC-style primitive
detection) turns the mixed discrete/continuous problem into a purely discrete one; careful point samples guide a discrete search;
divide-and-conquer segmentation; program synthesis (solver) per segment. Claims >100 primitive parts, compact programs, beats prior
methods on compactness/runtime. D2CSG authors (SEARCH) criticise it: RANSAC needs tuning, noisy initial primitives, long search.
Project page inversecsg.csail.mit.edu lists code; licence not checked.
- CSGNet (Sharma 2018): KNOWLEDGE only; supervised, voxel/2D -> CSG sequence.
- UCSG-Net (Kania, NeurIPS 2020). SEARCH: unsupervised; predicts box/sphere params; differentiable indicator on SDF; several CSG layers with
soft operator choice; structure and params learned jointly by gradient descent; autoencoding scope (2D/3D), no mesh-to-program at test
for unseen exotic shapes beyond the trained net.
- CAPRI-Net (CVPR 2022). SEARCH: axis-aligned quadric primitives, fixed 3-layer assembly; ABC point-cloud Chamfer 0.09 vs UCSG 1.43; ~5.3 vs
12.92 convex shapes. D2CSG (2023), CSG-Stump (2021): SEARCH, unsupervised compact trees. SECAD-Net (CVPR 2023): SEARCH, sketch-extrude,
editable; CD 2.94/4.20 (ABC/Fusion) vs UCSG 3.14/4.45.
- Point2CAD (Liu et al.): SEARCH only that a repo exists (jofcodeusal/point2cad mirror, Docker toshas/point2cad); method not retrieved.
Point2Cyl (CVPR 2022): SEARCH, supervised net to extrusion cylinders. DeepCAD (Wu 2021): KNOWLEDGE, ~178k sketch-extrude sequences.
- ShapeAssembly (Jones 2020, TOG): SEARCH; programs of cuboid proxies attached to each other with free parameters; extraction from PartNet.
ShapeMOD (2021): SEARCH; discovers macros across a corpus minimising calls and free parameters. Repos/licences not found by search
(KNOWLEDGE: github rkjones4/ShapeAssembly exists; unverified).
- CAD-Recode (Rukhovich et al., ICCV 2025, arXiv 2412.14042). FETCHED abstract: point cloud -> CadQuery Python (sketch-extrude); point-cloud
projector + small pretrained LLM decoder; trained on 1M procedurally generated CAD sequences; eval DeepCAD, Fusion360, CC3D (real scans);
output usable by off-the-shelf LLMs for editing and QA. SEARCH (unverified): Qwen 0.5B/1.5B decoders; DeepCAD mean CD 3.43 -> 0.30, IoU
77.6 -> 92.0 vs CAD-SIGNet; test time: 10 sampled candidates executed, min-Chamfer kept. Code github.com/filaPro/cad-recode (SEARCH);
HF dataset filapro/cad-recode is CC-BY-NC-4.0 (SEARCH); repo licence unchecked. Structure by autoregressive decoding, params as literal
tokens, re-ranking by Chamfer. Abstract says nothing on fillets/chamfers.
- MeshCoder (Dai et al., NeurIPS 2025, arXiv 2508.14879). FETCHED: 16,384-point cloud -> triplane tokenizer -> Llama-3.2-1B (LoRA) ->
Blender Python. API: cube/cylinder/UV-sphere/cone/torus, Translation (sweep 2D section along 3D trajectory), Bridge Loop, Boolean
(union/intersect/diff), Array, Fill Grid, Spoon, Fork. Data: ~10M part-code pairs, ~1M object-code pairs, 41 categories (Infinigen
Indoor). Table 2 averages CD x1e-2 / IoU%: Shape2Prog 6.00/45.0, PLAD 1.87/67.6, MeshCoder 0.06/86.75. Worst: Fork 58.9 IoU, CeilingLight
65.8, Array parts 78.9. Limits: human-made objects only; geometry only; training objects kept only if every part CD < 5e-3. LLM does not
edit; edits are manual param changes; GPT-4o used only for QA on the code. Output is part-segmented. Licence not stated in paper.
- CADCodeVerify (Alrashedy, ICLR 2025, arXiv 2410.05340). SEARCH: text prompt -> CadQuery (GPT-4); VLM writes/answers validation questions
about renders and refines; CADPrompt benchmark (200 objects); GPT-4 point-cloud distance -7.30%, compile rate +5.5% (v2; v1 said 5.0%
success). Target is a prompt, not a mesh.
- Newer render-loop agents (SEARCH): SEIG "Thinking in Blender" (arXiv 2606.02580): single reference image -> staged Blender code (geometry,
materials, composition, lighting), VLM generator + verifier per stage; FETCHED abstract gives no numbers, no code link. LL3M (2508.08228):
text -> Blender with critic reading renders. ShapeCraft (2510.17603): renders bbox/part/global views as feedback. SceneCraft (2403.01248):
render-critique-revise. Not found by search: "3D-Premise", "LLMto3D", Img2CAD/Text2CAD specifics.
## 2. Classical primitive fitting as a front end
- Efficient RANSAC (Schnabel/Wahl/Klein, CGF 2007): SEARCH/FETCHED page text: planes, spheres, cylinders, cones, tori in unorganised clouds;
multi-million-point sets "within less than a minute". CGAL Shape Detection implements it plus Region Growing (Lafarge&Mallet). CGAL is
dual GPL/LGPL + commercial; the Shape Detection package's own licence NOT confirmed (digest guessed GPL).
- GlobFit (Li/Wu/Chrysanthou/Sharf/Cohen-Or/Mitra, SIGGRAPH 2011): SEARCH; starts from RANSAC primitives, discovers global relations
(orientation, placement, equality; symmetry) and enforces them by constrained optimisation; assumes man-made, repeated, globally aligned
parts. Code: archived code.google.com/archive/p/globfit, "research use only" wording.
- Mitra 2006 symmetry detection, SPFN (2019): KNOWLEDGE/SEARCH only. ParSeNet (2020): SEARCH; B-spline patch decomposition, beats RANSAC
and SPFN baselines. No repos/licences retrieved.
- What fits supply as data: exact primitive parameters with inlier sets and residuals, segment labels, relation graph (GlobFit).
InverseCSG uses the primitives to make the problem discrete. Recorded weakness: noisy primitives, parameter tuning (SEARCH).
## 3. Differentiable fitting of SDF/CSG programs
- UCSG-Net (above): soft operator selection; structure learned jointly.
- Fuzzy Boolean differentiable CSG (dgp.toronto.edu/~hsuehtil/pdf/fuzzyBoolean.pdf): SEARCH; gradient descent over boolean and primitive
parameters; notes prior work pre-fixes tree structure and operators and optimises only primitive params; replaces min/max with a unified
fuzzy operator so the operation type is optimised. Costs listed: analytic SDF per primitive type, per-iteration SDF evaluation, image
losses need differentiable sphere tracing.
- DiffCSG (arXiv 2409.01421): SEARCH; differentiable rasterisation of mesh CSG, image-space gradients to primitive params; image-guided
editing, not mesh fitting.
- ResFit, Residual Primitive Fitting with SuperFrusta (arXiv 2512.09201): SEARCH; one analytic SDF primitive with 8 params (dilation, taper,
bulge, onion hollowing, profile roundness, axial scaling); alternates global shape analysis and local primitive optimisation; code "to be
open-sourced upon acceptance".
- Evolutionary mesh -> CSG abstraction (SEARCH; authors not retrieved): fast, coarse. Sigmoid-over-SDF soft occupancy with alternating
add-primitive / joint-refine loop (SEARCH, primitive-scene paper).
- Not found: a published gradient fit of an SDF program with bevel/round parameters to a messy hard-surface mesh.
- Handling of discrete structure across sources: soft selection (UCSG, CSG-Stump, fuzzy Boolean); pre-fixed structure (InverseCSG after
RANSAC; CAPRI-Net fixed layers); greedy add/prune (ResFit); neural token decoding (CAD-Recode, MeshCoder).
## 4. LLM spatial reasoning in 3D code generation
- BlenderGym (Gu et al., CVPR 2025, arXiv 2504.01786). FETCHED: 245 start-goal Blender scene pairs, five tasks (placement, lighting,
procedural material, blend shape, procedural geometry); the VLM sees start and goal and edits Blender code (target-matching setting).
Table 1 best-VLM vs human: geometry Chamfer 1.120 (GPT-4-Turbo) vs 0.334; geometry photometric loss 6.747 vs 1.269; placement CD 8.324 vs
1.532; blend shape CD 0.904 vs 0.399. Paper: VLMs "particularly poorly in procedural material and geometry editing"; all open-source
models except MiniCPM failed to produce executable code on >75% of geometry instances. Verifier agreement with humans: human 0.79,
Claude 3.5 Sonnet 0.66, random 0.5. Scaling a small verifier (InternVL2-8B, k 1 -> 64 over 32 candidates) beat unscaled GPT-4o and
Claude 3.5 on blend shape. Low compute: less verification share (0.33); high compute: more (0.62-0.73). Models are 2024-25 era. Code
licence not in retrieved text.
- BenchCAD (arXiv 2605.10865): SEARCH; 17,900 CadQuery programs, 106 part families; frontier models recover coarse outer geometry but fail
faithful parametric programs; sweeps/lofts/twist-extrudes replaced by sketch-extrude; single-literal errors (5.6 vs 5.85) at IoU >= 0.9.
BenchCAD-Edit: text instruction sets the upper bound; the image is "at best a clarifier and at worst a distractor" for thinking models.
- Text2CAD-Bench (2605.18430): SEARCH; execution failures even at easy level; sweep/loft/shell widely fail; executing != correct.
- Text-to-B-Rep paper (2603.11831): SEARCH; spatial misalignment of primitives, omitted fillets, geometry contradicting described layout;
authors blame weak precise 3D spatial reasoning.
- "Foundation Models for Automatic CAD Generation" (2607.05573): SEARCH, single source; numeric analytic feedback peaked at round 1; VLM
critique of renders kept improving in rounds 2-3 for DeepSeek/Qwen; VLM critics struggle on rotationally symmetric geometry.
- Multi-view conditioning beat single-view and sketch on Chamfer/F-score in a B-Rep paper (SEARCH, not CadQuery).
- Target vs description: BlenderGym gives a target and still shows a large human gap; CAD-Recode and MeshCoder avoid prompting by training
point-cloud encoders; no controlled prompted-LLM study of target-vs-description was found.
- MindCube spatial study (SEARCH): errors 20% final reasoning step, 13% 3D reconstruction, 6% generated programs.
## 5. Hard-surface specifics
- SDF rounding (Quilez distfunctions, SEARCH): sdRoundBox = box SDF minus r, an iso-offset so the shape grows by r. Polynomial smin with k
as blend band; Quilez marks smin as "bound" not exact. Chamfer/fillet operators not retrieved; KNOWLEDGE: hg_sdf (Mercury) has
fOpUnionChamfer / fOpUnionRound / fOpUnionStairs (unverified here).
- MeshCoder (FETCHED): no fillet/chamfer primitive; booleans, sweeps, bridge-loop, arrays only.
- BenchCAD / text-to-B-Rep (SEARCH): omitted fillets/refinement features named as failure; sweeps/lofts degrade to extrudes.
- CAD-Recode/DeepCAD data are sketch-extrude (SEARCH/KNOWLEDGE); fillet/chamfer not covered in what was retrieved.
- InverseCSG output: boolean of primitives, no blends (KNOWLEDGE). CAPRI-Net quadrics: smooth surface in one primitive (SEARCH). ResFit
primitive has a profile-roundness parameter (SEARCH).
## Gaps
- Operator/agent words on why VibeGeometry failed: not indexed (see 0b).
- Not read at body level: CAD-Recode fillet handling, InverseCSG experiments, GlobFit relation list, SEIG numbers, ResFit results.
- Licences unverified: InverseCSG, ShapeAssembly, CAD-Recode repo, MeshCoder, GlobFit, CGAL Shape Detection, Point2CAD.

View file

@ -0,0 +1,84 @@
# 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))`).
### 4a. Winner identity, fraction of grid pixels whose winning cell differs (t=0 / t=8)
| variant | vs `site` | vs `base` |
|---|---|---|
| base | .147 / .156 | 0 / 0 |
| qpt | .147 / .156 | 0 / 0 (identical argmin to base) |
| cellc | .127 / .114 | .185 / .147 |
| site_t0 | 0 / .226 | .147 / .147 |
| warp | .675 / .646 | .668 / .639 |
| site | 0 / 0 | .147 / .156 |
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 |
|---|---|---|---|---|---|
| site | .1083 | .01125 | .00113 | .000113 | 2.9e-6 |
| qpt | .00461 | .000461 | 4.6e-5 | 4.6e-6 | 4.1e-6 |
| cellc | .1060 | .0204 | .00206 | .000207 | 5.2e-6 |
[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.

View file

@ -0,0 +1,559 @@
{
"grid": {
"t=0.0": {
"base": {
"winner_differs_from_site": 0.14684,
"winner_differs_from_base": 0.0,
"out_absdiff_vs_site_mean": 0.1692938271424024,
"out_absdiff_vs_site_max": 0.9999999999999667,
"n_cells": 53
},
"site": {
"winner_differs_from_site": 0.0,
"winner_differs_from_base": 0.14684,
"out_absdiff_vs_site_mean": 0.0,
"out_absdiff_vs_site_max": 0.0,
"n_cells": 53
},
"qpt": {
"winner_differs_from_site": 0.14684,
"winner_differs_from_base": 0.0,
"out_absdiff_vs_site_mean": 0.1679024745847925,
"out_absdiff_vs_site_max": 1.0,
"n_cells": 53
},
"cellc": {
"winner_differs_from_site": 0.126984,
"winner_differs_from_base": 0.184648,
"out_absdiff_vs_site_mean": 0.16449204766510442,
"out_absdiff_vs_site_max": 0.99999757371696,
"n_cells": 54
},
"warp": {
"winner_differs_from_site": 0.6748,
"winner_differs_from_base": 0.667952,
"out_absdiff_vs_site_mean": 0.31037303493350155,
"out_absdiff_vs_site_max": 1.0,
"n_cells": 143
},
"site_t0": {
"winner_differs_from_site": 0.0,
"winner_differs_from_base": 0.14684,
"out_absdiff_vs_site_mean": 0.0,
"out_absdiff_vs_site_max": 0.0,
"n_cells": 53
},
"site_window2_vs_window5_winner_differs": 0.0
},
"t=8.0": {
"base": {
"winner_differs_from_site": 0.155996,
"winner_differs_from_base": 0.0,
"out_absdiff_vs_site_mean": 0.18560227222422834,
"out_absdiff_vs_site_max": 0.9999999025356109,
"n_cells": 53
},
"site": {
"winner_differs_from_site": 0.0,
"winner_differs_from_base": 0.155996,
"out_absdiff_vs_site_mean": 0.0,
"out_absdiff_vs_site_max": 0.0,
"n_cells": 53
},
"qpt": {
"winner_differs_from_site": 0.155996,
"winner_differs_from_base": 0.0,
"out_absdiff_vs_site_mean": 0.17977625463968147,
"out_absdiff_vs_site_max": 0.999999999999996,
"n_cells": 53
},
"cellc": {
"winner_differs_from_site": 0.114268,
"winner_differs_from_base": 0.146552,
"out_absdiff_vs_site_mean": 0.1556440909167879,
"out_absdiff_vs_site_max": 0.9999919751523085,
"n_cells": 54
},
"warp": {
"winner_differs_from_site": 0.646396,
"winner_differs_from_base": 0.639384,
"out_absdiff_vs_site_mean": 0.2802002446118991,
"out_absdiff_vs_site_max": 1.0,
"n_cells": 128
},
"site_t0": {
"winner_differs_from_site": 0.226044,
"winner_differs_from_base": 0.14684,
"out_absdiff_vs_site_mean": 0.23360220832359216,
"out_absdiff_vs_site_max": 1.0,
"n_cells": 53
},
"site_window2_vs_window5_winner_differs": 0.0
}
},
"line_pop": {
"horizontal z=2.6 (near well 0)": {
"base t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.000679179751586656,
"maxjump_out_per_step": 0.011527150729375713,
"maxjump_f1_with_window6": 0.000679179751586656,
"f1_err_vs_win6": 0.0
},
"base t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.000679179751586656,
"maxjump_out_per_step": 0.011527150729375713,
"maxjump_f1_with_window6": 0.000679179751586656,
"f1_err_vs_win6": 0.0
},
"site t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0010050426622043585,
"maxjump_out_per_step": 0.016002826669016756,
"maxjump_f1_with_window6": 0.0010050426622043585,
"f1_err_vs_win6": 0.0
},
"site t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.0010050426622043585,
"maxjump_out_per_step": 0.016002826669016756,
"maxjump_f1_with_window6": 0.0010050426622043585,
"f1_err_vs_win6": 0.0
},
"site t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.001918579519211816,
"maxjump_out_per_step": 0.03595224397909913,
"maxjump_f1_with_window6": 0.001918579519211816,
"f1_err_vs_win6": 0.0
},
"site t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.001918579519211816,
"maxjump_out_per_step": 0.03595224397909913,
"maxjump_f1_with_window6": 0.001918579519211816,
"f1_err_vs_win6": 0.0
},
"qpt t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0019043178713147313,
"maxjump_out_per_step": 0.016796471326461004,
"maxjump_f1_with_window6": 0.0019043178713147313,
"f1_err_vs_win6": 0.0
},
"qpt t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.0019043178713147313,
"maxjump_out_per_step": 0.016796471326461004,
"maxjump_f1_with_window6": 0.0019043178713147313,
"f1_err_vs_win6": 0.0
},
"qpt t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.0056772408954191,
"maxjump_out_per_step": 0.04338667429132481,
"maxjump_f1_with_window6": 0.0056772408954191,
"f1_err_vs_win6": 0.0
},
"qpt t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.0056772408954191,
"maxjump_out_per_step": 0.04338667429132481,
"maxjump_f1_with_window6": 0.0056772408954191,
"f1_err_vs_win6": 0.0
},
"cellc t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.000899555983544964,
"maxjump_out_per_step": 0.028931107704223913,
"maxjump_f1_with_window6": 0.000899555983544964,
"f1_err_vs_win6": 0.0
},
"cellc t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.000899555983544964,
"maxjump_out_per_step": 0.028998803253634242,
"maxjump_f1_with_window6": 0.000899555983544964,
"f1_err_vs_win6": 0.0
},
"cellc t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.001589550701590614,
"maxjump_out_per_step": 0.02212547196092063,
"maxjump_f1_with_window6": 0.001589550701590614,
"f1_err_vs_win6": 0.0
},
"cellc t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.001589550701590614,
"maxjump_out_per_step": 0.02212547196092063,
"maxjump_f1_with_window6": 0.001589550701590614,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.005413713480415305,
"maxjump_out_per_step": 0.11861272322439464,
"maxjump_f1_with_window6": 0.005413713480415305,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.005413713480415305,
"maxjump_out_per_step": 0.6979479036971218,
"maxjump_f1_with_window6": 0.005413713480415305,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.019199908363967788,
"maxjump_out_per_step": 0.28876383821796175,
"maxjump_f1_with_window6": 0.019199908363967788,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.019199908363967788,
"maxjump_out_per_step": 0.28876383821796175,
"maxjump_f1_with_window6": 0.019199908363967788,
"f1_err_vs_win6": 0.0
},
"base t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.000679179751586656,
"maxjump_out_per_step": 0.011527150729375713,
"maxjump_f1_with_window6": 0.000679179751586656,
"f1_err_vs_win6": 0.0
},
"base t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.000679179751586656,
"maxjump_out_per_step": 0.011527150729375713,
"maxjump_f1_with_window6": 0.000679179751586656,
"f1_err_vs_win6": 0.0
},
"site t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0011605444949153165,
"maxjump_out_per_step": 0.03721603977227228,
"maxjump_f1_with_window6": 0.0011605444949153165,
"f1_err_vs_win6": 0.0
},
"site t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.0011605444949153165,
"maxjump_out_per_step": 0.037218919278060314,
"maxjump_f1_with_window6": 0.0011605444949153165,
"f1_err_vs_win6": 0.0
},
"site t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.01726416929635244,
"maxjump_out_per_step": 0.37104796823449204,
"maxjump_f1_with_window6": 0.0024839789447830274,
"f1_err_vs_win6": 0.016881040623005106
},
"site t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.0024839789447830274,
"maxjump_out_per_step": 0.045046050707719165,
"maxjump_f1_with_window6": 0.0024839789447830274,
"f1_err_vs_win6": 0.0
},
"qpt t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0019988841823728176,
"maxjump_out_per_step": 0.01873902874992317,
"maxjump_f1_with_window6": 0.0019988841823728176,
"f1_err_vs_win6": 0.0
},
"qpt t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.0019988841823728176,
"maxjump_out_per_step": 0.01873902874992317,
"maxjump_f1_with_window6": 0.0019988841823728176,
"f1_err_vs_win6": 0.0
},
"qpt t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.005628071367035137,
"maxjump_out_per_step": 0.04897917252830741,
"maxjump_f1_with_window6": 0.005628071367035137,
"f1_err_vs_win6": 0.0
},
"qpt t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.005628071367035137,
"maxjump_out_per_step": 0.04897917252830741,
"maxjump_f1_with_window6": 0.005628071367035137,
"f1_err_vs_win6": 0.0
},
"cellc t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0013893624582810293,
"maxjump_out_per_step": 0.0186819346008138,
"maxjump_f1_with_window6": 0.0013893624582810293,
"f1_err_vs_win6": 0.0
},
"cellc t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.0013893624582810293,
"maxjump_out_per_step": 0.02079886198517822,
"maxjump_f1_with_window6": 0.0013893624582810293,
"f1_err_vs_win6": 0.0
},
"cellc t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.0033405760083177194,
"maxjump_out_per_step": 0.07493559630083158,
"maxjump_f1_with_window6": 0.0033405760083177194,
"f1_err_vs_win6": 0.0
},
"cellc t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.0033405760083177194,
"maxjump_out_per_step": 0.07493559630083158,
"maxjump_f1_with_window6": 0.0033405760083177194,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.005131686955684001,
"maxjump_out_per_step": 0.10245451309831222,
"maxjump_f1_with_window6": 0.005131686955684001,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.005131686955684001,
"maxjump_out_per_step": 0.5048292489051344,
"maxjump_f1_with_window6": 0.005131686955684001,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.01955931573768127,
"maxjump_out_per_step": 0.2975581851502386,
"maxjump_f1_with_window6": 0.01955931573768127,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.01955931573768127,
"maxjump_out_per_step": 0.2975581851502386,
"maxjump_f1_with_window6": 0.01955931573768127,
"f1_err_vs_win6": 0.0
}
},
"diagonal": {
"base t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.001275910800458191,
"maxjump_out_per_step": 0.03528728288006466,
"maxjump_f1_with_window6": 0.001275910800458191,
"f1_err_vs_win6": 0.0
},
"base t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.006834249882349042,
"maxjump_out_per_step": 0.45027124784270633,
"maxjump_f1_with_window6": 0.001275910800458191,
"f1_err_vs_win6": 0.006469535309243879
},
"site t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0018160559768694018,
"maxjump_out_per_step": 0.036994428279204405,
"maxjump_f1_with_window6": 0.0018160559768694018,
"f1_err_vs_win6": 0.0
},
"site t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.17389687729083692,
"maxjump_out_per_step": 0.9512571305077048,
"maxjump_f1_with_window6": 0.0018160559768694018,
"f1_err_vs_win6": 0.17352236500589702
},
"site t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.0032951968389483,
"maxjump_out_per_step": 0.08101849084444412,
"maxjump_f1_with_window6": 0.0032951968389483,
"f1_err_vs_win6": 0.0
},
"site t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.0032951968389483,
"maxjump_out_per_step": 0.08049836455119364,
"maxjump_f1_with_window6": 0.0032951968389483,
"f1_err_vs_win6": 0.0
},
"qpt t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0026472302788482782,
"maxjump_out_per_step": 0.03868824720262809,
"maxjump_f1_with_window6": 0.0026472302788482782,
"f1_err_vs_win6": 0.0
},
"qpt t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.010707146916472454,
"maxjump_out_per_step": 0.21289370234612265,
"maxjump_f1_with_window6": 0.0026472302788482782,
"f1_err_vs_win6": 0.009199392603544854
},
"qpt t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.007883634786070015,
"maxjump_out_per_step": 0.07213687421025916,
"maxjump_f1_with_window6": 0.007883634786070015,
"f1_err_vs_win6": 0.0
},
"qpt t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.007883634786070015,
"maxjump_out_per_step": 0.07213687421025916,
"maxjump_f1_with_window6": 0.007883634786070015,
"f1_err_vs_win6": 0.0
},
"cellc t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0021042254469000876,
"maxjump_out_per_step": 0.03605934023336077,
"maxjump_f1_with_window6": 0.0021042254469000876,
"f1_err_vs_win6": 0.0
},
"cellc t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.15454958335751312,
"maxjump_out_per_step": 0.47361732317771077,
"maxjump_f1_with_window6": 0.0021042254469000876,
"f1_err_vs_win6": 0.15418392181872642
},
"cellc t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.004089971897996092,
"maxjump_out_per_step": 0.04544931913493744,
"maxjump_f1_with_window6": 0.004089971897996092,
"f1_err_vs_win6": 0.0
},
"cellc t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.004089971897996092,
"maxjump_out_per_step": 0.04544931913493744,
"maxjump_f1_with_window6": 0.004089971897996092,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.007786414559647126,
"maxjump_out_per_step": 0.10615772398185219,
"maxjump_f1_with_window6": 0.007786414559647126,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.007786414559647126,
"maxjump_out_per_step": 0.10615772398185219,
"maxjump_f1_with_window6": 0.007786414559647126,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.0220788526153487,
"maxjump_out_per_step": 0.2726388272791984,
"maxjump_f1_with_window6": 0.0220788526153487,
"f1_err_vs_win6": 0.0
},
"warp t=0.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.0220788526153487,
"maxjump_out_per_step": 0.2726388272791984,
"maxjump_f1_with_window6": 0.0220788526153487,
"f1_err_vs_win6": 0.0
},
"base t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.001275910800458191,
"maxjump_out_per_step": 0.03528728288006466,
"maxjump_f1_with_window6": 0.001275910800458191,
"f1_err_vs_win6": 0.0
},
"base t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.006834249882349042,
"maxjump_out_per_step": 0.45027124784270633,
"maxjump_f1_with_window6": 0.001275910800458191,
"f1_err_vs_win6": 0.006469535309243879
},
"site t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.00155291340314212,
"maxjump_out_per_step": 0.04226739191047357,
"maxjump_f1_with_window6": 0.00155291340314212,
"f1_err_vs_win6": 0.0
},
"site t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.11344119297302535,
"maxjump_out_per_step": 0.3917028634066005,
"maxjump_f1_with_window6": 0.00155291340314212,
"f1_err_vs_win6": 0.11285505036112697
},
"site t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.002453426564246408,
"maxjump_out_per_step": 0.03688685308461992,
"maxjump_f1_with_window6": 0.002453426564246408,
"f1_err_vs_win6": 0.0
},
"site t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.002453426564246408,
"maxjump_out_per_step": 0.03688685308461992,
"maxjump_f1_with_window6": 0.002453426564246408,
"f1_err_vs_win6": 0.0
},
"qpt t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0018171966866530331,
"maxjump_out_per_step": 0.03560130205583023,
"maxjump_f1_with_window6": 0.0018171966866530331,
"f1_err_vs_win6": 0.0
},
"qpt t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.011573594040749535,
"maxjump_out_per_step": 0.44945714067888365,
"maxjump_f1_with_window6": 0.0018171966866530331,
"f1_err_vs_win6": 0.011263134181283219
},
"qpt t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.004178672365624969,
"maxjump_out_per_step": 0.06700822884555246,
"maxjump_f1_with_window6": 0.004178672365624969,
"f1_err_vs_win6": 0.0
},
"qpt t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.004178672365624969,
"maxjump_out_per_step": 0.06700822884555246,
"maxjump_f1_with_window6": 0.004178672365624969,
"f1_err_vs_win6": 0.0
},
"cellc t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.0013657606892926166,
"maxjump_out_per_step": 0.03848639655536479,
"maxjump_f1_with_window6": 0.0013657606892926166,
"f1_err_vs_win6": 0.0
},
"cellc t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.19448286979973972,
"maxjump_out_per_step": 0.9778972151563999,
"maxjump_f1_with_window6": 0.0013657606892926166,
"f1_err_vs_win6": 0.1940193666886757
},
"cellc t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.0016119831862634326,
"maxjump_out_per_step": 0.05348368104496215,
"maxjump_f1_with_window6": 0.0016119831862634326,
"f1_err_vs_win6": 0.0
},
"cellc t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.0016119831862634326,
"maxjump_out_per_step": 0.05348368104496215,
"maxjump_f1_with_window6": 0.0016119831862634326,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=1.6 WIN=2": {
"maxjump_f1_per_step": 0.00389655575000758,
"maxjump_out_per_step": 0.06278112824237458,
"maxjump_f1_with_window6": 0.00389655575000758,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=1.6 WIN=1": {
"maxjump_f1_per_step": 0.00389655575000758,
"maxjump_out_per_step": 0.4429137542853424,
"maxjump_f1_with_window6": 0.00389655575000758,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=6.0 WIN=2": {
"maxjump_f1_per_step": 0.012386023943556368,
"maxjump_out_per_step": 0.1972931166165931,
"maxjump_f1_with_window6": 0.012386023943556368,
"f1_err_vs_win6": 0.0
},
"warp t=8.0 B=6.0 WIN=3": {
"maxjump_f1_per_step": 0.012386023943556368,
"maxjump_out_per_step": 0.1972931166165931,
"maxjump_f1_with_window6": 0.012386023943556368,
"f1_err_vs_win6": 0.0
}
}
},
"time_pop": {
"base": {
"maxjump_f1_per_dt": 0.0,
"maxjump_out_per_dt": 0.0,
"winner_changes_total": 0
},
"site": {
"maxjump_f1_per_dt": 0.006243969970252006,
"maxjump_out_per_dt": 0.10827554277384255,
"winner_changes_total": 17
},
"qpt": {
"maxjump_f1_per_dt": 0.004880867091880958,
"maxjump_out_per_dt": 0.004611044852681684,
"winner_changes_total": 0
},
"cellc": {
"maxjump_f1_per_dt": 0.006179970448791816,
"maxjump_out_per_dt": 0.10604425529368433,
"winner_changes_total": 15
},
"site_t0": {
"maxjump_f1_per_dt": 0.0,
"maxjump_out_per_dt": 0.0,
"winner_changes_total": 0
}
}
}

View file

@ -0,0 +1,140 @@
{
"timejump site dt=0.01": {
"q": [
2.3,
2.8
],
"t_worst": 9.91,
"max_out_step": 0.10827554277384255,
"max_f1_step": 0.0028572323710521763
},
"timejump site dt=0.001": {
"q": [
2.3,
2.8
],
"t_worst": 9.91,
"max_out_step": 0.011248851985623975,
"max_f1_step": 0.00028847749905558384
},
"timejump site dt=0.0001": {
"q": [
2.3,
2.8
],
"t_worst": 9.91,
"max_out_step": 0.00113067110268672,
"max_f1_step": 2.8874698390524145e-05
},
"timejump site dt=1e-05": {
"q": [
2.3,
2.8
],
"t_worst": 9.91,
"max_out_step": 0.00011316362556801352,
"max_f1_step": 2.8877387294734547e-06
},
"timejump qpt dt=0.01": {
"q": [
5.9,
1.9
],
"t_worst": 16.21,
"max_out_step": 0.00461104485268235,
"max_f1_step": 0.004042127425204045
},
"timejump qpt dt=0.001": {
"q": [
5.9,
1.9
],
"t_worst": 16.21,
"max_out_step": 0.0004611240505579539,
"max_f1_step": 0.000406360115853599
},
"timejump qpt dt=0.0001": {
"q": [
5.9,
1.9
],
"t_worst": 16.21,
"max_out_step": 4.611242486959877e-05,
"max_f1_step": 4.065726355373389e-05
},
"timejump qpt dt=1e-05": {
"q": [
5.9,
1.9
],
"t_worst": 16.21,
"max_out_step": 4.611242504171109e-06,
"max_f1_step": 4.065938649877587e-06
},
"timejump cellc dt=0.01": {
"q": [
5.9,
1.9
],
"t_worst": 0.39,
"max_out_step": 0.10604425529368433,
"max_f1_step": 0.005110972264514513
},
"timejump cellc dt=0.001": {
"q": [
5.9,
1.9
],
"t_worst": 0.39,
"max_out_step": 0.020376801353283347,
"max_f1_step": 0.0005172341605852449
},
"timejump cellc dt=0.0001": {
"q": [
5.9,
1.9
],
"t_worst": 0.39,
"max_out_step": 0.002064834070920063,
"max_f1_step": 5.178482072565327e-05
},
"timejump cellc dt=1e-05": {
"q": [
5.9,
1.9
],
"t_worst": 0.39,
"max_out_step": 0.00020688477103125713,
"max_f1_step": 5.179096147611695e-06
},
"shape base": {
"mean_cell_pixels_near_wells": 10951.06215310558,
"mean_cell_pixels_far": 10515.08654105133,
"border_frac_near": 0.3004222385907398,
"border_frac_far": 0.28125746844758714
},
"shape site": {
"mean_cell_pixels_near_wells": 9671.733834071645,
"mean_cell_pixels_far": 11438.643308839986,
"border_frac_near": 0.2704401863244494,
"border_frac_far": 0.25280623954417336
},
"shape qpt": {
"mean_cell_pixels_near_wells": 10951.06215310558,
"mean_cell_pixels_far": 10515.08654105133,
"border_frac_near": 0.1644136900747995,
"border_frac_far": 0.2500635410778843
},
"shape cellc": {
"mean_cell_pixels_near_wells": 11562.292900789493,
"mean_cell_pixels_far": 12273.640744435888,
"border_frac_near": 0.27424759572989177,
"border_frac_far": 0.2618651108270203
},
"shape warp": {
"mean_cell_pixels_near_wells": 3446.9220259152826,
"mean_cell_pixels_far": 8566.10048594699,
"border_frac_near": 0.29084665580811486,
"border_frac_far": 0.3016702768114897
}
}

View file

@ -0,0 +1,123 @@
"""Scratch probe: where does a weighted lattice Voronoi cell get its weight?
2D, lattice cell = 1 unit, wells with a proximity field, numpy only.
Variants of the per-site weight wt_i (multiplies the distance to site i, as the fauna probe's lattice_web):
base : wt = hashed .5..1, no field (reference, the unweighted-by-field node)
site : wt = base*(1+B*prox(site position, t)) (fauna probe, 4354ef2c)
qpt : wt = base*(1+B*prox(query position, t)) (common factor for every candidate site)
cellc : wt = base*(1+B*prox(lattice cell centre, t)) (node calls the field at the integer cell id)
site_t0: as site, but field read at t=0 (stale time) while wells have moved
warp : base weights, query warped q' = q*(1+B*prox(q)) (domain scaling, a different construct)
"""
import numpy as np, sys, json
rng_seed = 7
def hash01(ix, iz, k):
# integer hash (lowbias32-style), stand-in for pcg3d; deterministic, vectorised
x = (ix.astype(np.uint64)*np.uint64(73856093) ^ iz.astype(np.uint64)*np.uint64(19349663) ^ np.uint64(k*83492791+rng_seed)) & np.uint64(0xFFFFFFFF)
x ^= x >> np.uint64(16); x = (x*np.uint64(0x7feb352d)) & np.uint64(0xFFFFFFFF)
x ^= x >> np.uint64(15); x = (x*np.uint64(0x846ca68b)) & np.uint64(0xFFFFFFFF)
x ^= x >> np.uint64(16)
return (x >> np.uint64(8)).astype(np.float64)/16777216.0
W0 = np.array([[2.0,2.5],[5.5,4.0],[3.5,6.0],[6.5,1.5]]); WS = np.array([0.8,1.2,0.6,1.0])
OM = np.array([0.30,-0.22,0.17,-0.41]) # well orbit rate, rad per unit t (wells orbit a centre)
CEN = np.array([4.0,4.0])
def wells(t):
r = W0-CEN; c, s = np.cos(OM*t), np.sin(OM*t)
return CEN + np.stack([r[:,0]*c-r[:,1]*s, r[:,0]*s+r[:,1]*c], 1)
def prox(x, z, t):
w = wells(t); out = 0
for k in range(len(w)):
out = np.maximum(out, np.exp(-(np.hypot(x-w[k,0], z-w[k,1])/WS[k])**2))
return out
KW=.35
def node(qx, qz, variant, t=0.0, B=1.6, WIN=2, tfield=None):
"""returns f1, f2, winning cell id (ix*10007+iz), soft border value"""
qx0, qz0 = qx, qz
if variant == 'warp':
s = 1+B*prox(qx, qz, t); qx, qz = qx*s, qz*s
nx, nz = np.floor(qx), np.floor(qz)
m = np.full(qx.shape, 8.0); f1 = m.copy(); f2 = m.copy(); wid = np.zeros(qx.shape)
pq = prox(qx0, qz0, t)
tf = t if tfield is None else tfield
for j in range(-WIN, WIN+1):
for i in range(-WIN, WIN+1):
cx, cz = nx+i, nz+j
ox, oz = hash01(cx, cz, 11), hash01(cx, cz, 12)
base = hash01(cx, cz, 13)*.5+.5
sx, sz = cx+ox, cz+oz
if variant in ('base','warp'): wt = base
elif variant == 'site': wt = base*(1+B*prox(sx, sz, tf))
elif variant == 'site_t0': wt = base*(1+B*prox(sx, sz, 0.0))
elif variant == 'cellc': wt = base*(1+B*prox(cx+.5, cz+.5, tf))
elif variant == 'qpt': wt = base*(1+B*pq)
d = np.hypot(sx-qx, sz-qz)*wt
upd1 = d < f1; upd2 = (~upd1) & (d < f2)
wid = np.where(upd1, cx*10007+cz, wid)
f2 = np.where(upd1, f1, np.where(upd2, d, f2)); f1 = np.where(upd1, d, f1)
h = (lambda u: u*u*(3-2*u))(np.clip(.5+.5*(m-d)/KW, 0, 1))
m = m+(d-m)*h-h*(1-h)*KW/(1+3*KW)
gap0 = .25*KW/(1+3*KW)
soft = np.clip((f1-m)/gap0, 0, 1)
out = np.maximum(soft**2, np.exp(-((f2-f1)/.10)**2))
return f1, f2, wid, out
def grid(n=500, lo=1.0, hi=7.0):
xs = np.linspace(lo, hi, n); X, Z = np.meshgrid(xs, xs); return X, Z
def main():
R = {}
X, Z = grid()
for t in (0.0, 8.0):
ref = {v: node(X, Z, v, t) for v in ('base','site','qpt','cellc','warp')}
ref['site_t0'] = node(X, Z, 'site_t0', t)
# large-window truth for 'site' (exact argmin unless window too small)
truth = node(X, Z, 'site', t, WIN=5)[2]
key = f"t={t}"; R[key] = {}
for v, (f1, f2, wid, out) in ref.items():
R[key][v] = dict(
winner_differs_from_site=float((wid != ref['site'][2]).mean()),
winner_differs_from_base=float((wid != ref['base'][2]).mean()),
out_absdiff_vs_site_mean=float(np.abs(out-ref['site'][3]).mean()),
out_absdiff_vs_site_max=float(np.abs(out-ref['site'][3]).max()),
n_cells=int(len(np.unique(wid))))
R[key]['site_window2_vs_window5_winner_differs'] = float((ref['site'][2] != truth).mean())
# cell count density near wells for site vs base
# pop measure: dense line, step 1e-3 units; also jumps in time at fixed q
ts = np.linspace(1.0, 7.0, 6001) # 1e-3
lines = {'horizontal z=2.6 (near well 0)': (ts, np.full_like(ts, 2.6)),
'diagonal': (ts, 1.0+ts*0.9-0.5)}
P = {}
for ln, (lx, lz) in lines.items():
P[ln] = {}
for t in (0.0, 8.0):
for v in ('base','site','qpt','cellc','warp'):
for B, WIN in ((1.6,2),(1.6,1),(6.0,2),(6.0,3)):
if v in ('base',) and B != 1.6: continue
f1, f2, wid, out = node(lx, lz, v, t, B, WIN)
ref = node(lx, lz, v, t, B, 6)
P[ln][f"{v} t={t} B={B} WIN={WIN}"] = dict(
maxjump_f1_per_step=float(np.abs(np.diff(f1)).max()),
maxjump_out_per_step=float(np.abs(np.diff(out)).max()),
maxjump_f1_with_window6=float(np.abs(np.diff(ref[0])).max()),
f1_err_vs_win6=float(np.abs(f1-ref[0]).max()))
# time pop: fixed query points, t stepped at 1e-2; wells move
tt = np.linspace(0, 20, 2001)
qs = [(2.3,2.8),(4.7,3.9),(3.5,5.5),(5.9,1.9)]
T = {}
for v in ('base','site','qpt','cellc','site_t0'):
mj = 0; mo = 0; changes = 0
for (qx, qz) in qs:
f1s=[];outs=[];wids=[]
for t in tt:
a,b,w,o = node(np.array([qx]), np.array([qz]), v, t)
f1s.append(a[0]); outs.append(o[0]); wids.append(w[0])
mj=max(mj,np.abs(np.diff(f1s)).max()); mo=max(mo,np.abs(np.diff(outs)).max())
changes += int((np.diff(wids)!=0).sum())
T[v] = dict(maxjump_f1_per_dt=float(mj), maxjump_out_per_dt=float(mo), winner_changes_total=changes)
json.dump(dict(grid=R, line_pop=P, time_pop=T), open('probe_results.json','w'), indent=1)
print(json.dumps(R, indent=1)); print(json.dumps(T, indent=1))
for ln in P:
print(ln)
for k,v in P[ln].items(): print(' ', k, {a: round(b,5) for a,b in v.items()})
main()

View file

@ -0,0 +1,37 @@
"""Follow-up: (1) continuity refinement: does the max time-jump of 'site' scale with dt? (2) cell size near vs far from wells,
border-pixel fraction per variant."""
import numpy as np, json, importlib.util, sys, io, contextlib
spec = importlib.util.spec_from_file_location('p', 'siteweight_probe.py')
src = open('siteweight_probe.py').read().replace('\nmain()\n', '\n') # import without running main
ns = {}; exec(compile(src, 'p', 'exec'), ns)
node, prox, grid, wells = ns['node'], ns['prox'], ns['grid'], ns['wells']
res = {}
# (1) worst time step at dt=.01 for each variant, then refine dt inside +-.02 of it (continuity: step should shrink ~ with dt)
qs = [(2.3,2.8),(4.7,3.9),(3.5,5.5),(5.9,1.9)]
def series(v, qx, qz, tt):
f1 = np.empty(len(tt)); out = np.empty(len(tt))
for k, t in enumerate(tt):
a,b,w,o = node(np.array([qx]), np.array([qz]), v, t); f1[k]=a[0]; out[k]=o[0]
return f1, out
for v in ('site','qpt','cellc'):
tt = np.arange(0, 20.0001, 0.01); best = (0, None, None)
for qx, qz in qs:
f1, out = series(v, qx, qz, tt); i = int(np.argmax(np.abs(np.diff(out))))
if abs(out[i+1]-out[i]) > best[0]: best = (abs(out[i+1]-out[i]), (qx,qz), tt[i])
_, q, t0 = best
for dt in (0.01, 0.001, 0.0001, 0.00001):
t2 = np.arange(t0-0.02, t0+0.03, dt); f1, out = series(v, q[0], q[1], t2)
res[f"timejump {v} dt={dt}"] = dict(q=q, t_worst=float(t0), max_out_step=float(np.abs(np.diff(out)).max()), max_f1_step=float(np.abs(np.diff(f1)).max()))
print(v, dt, res[f"timejump {v} dt={dt}"], flush=True)
# (2) cell area near vs far wells, border fraction
X, Z = grid(600)
w = wells(0.0)
near = np.min([np.hypot(X-w[k,0], Z-w[k,1])/ns['WS'][k] for k in range(4)], axis=0) < 1.0
for v in ('base','site','qpt','cellc','warp'):
f1,f2,wid,out = node(X, Z, v, 0.0)
ids, cnt = np.unique(wid, return_counts=True); area = dict(zip(ids, cnt))
pa = np.array([area[i] for i in wid.ravel()]).reshape(wid.shape)
res[f"shape {v}"] = dict(mean_cell_pixels_near_wells=float(pa[near].mean()), mean_cell_pixels_far=float(pa[~near].mean()),
border_frac_near=float((out[near]>.5).mean()), border_frac_far=float((out[~near]>.5).mean()))
print(v, res[f"shape {v}"])
json.dump(res, open('probe_results2.json','w'), indent=1)

View file

@ -0,0 +1,11 @@
import numpy as np, probe as P
g = np.linspace(-3200, 3200, 260); X, Z = np.meshgrid(g, g); q = np.stack([X, Z], -1)
rng = np.random.default_rng(3); ang = rng.uniform(0, 6.283, X.shape); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)
for boost, S in [(1.6, 1), (1.6, 2), (4, 1), (4, 2), (9, 1)]:
ref = P.node(q, 100.0, 9, boost, S, nrm)
row = []
for W in (2, 3, 4, 5):
r = P.node(q, 100.0, W, boost, S, nrm)
miss = np.mean((np.abs(r[0]-ref[0])>1e-12)|(np.abs(r[1]-ref[1])>1e-12))
row.append(f"W{W}: miss {miss:.5f} dOut {np.abs(r[3]-ref[3]).max():.3f}")
print(f"boost {boost} S {S} ratio {2*(1+boost):.1f}x{S}:", "; ".join(row))

View file

@ -0,0 +1,10 @@
import numpy as np, probe as P
g = (np.arange(-12, 12) + .5) * 260; X, Z = np.meshgrid(g, g); c = np.stack([X, Z], -1).reshape(-1, 2)
for ex in (False, True):
prev = None; worst = 0; big = 0
for t in np.arange(0, 300, 1/60):
Pw = np.exp(-(P.border(c, P.bodies(t), ex) / P.FALL) ** 2)
if prev is not None:
s = np.abs(Pw - prev); worst = max(worst, s.max()); big += (s > .01).sum()
prev = Pw
print("exact" if ex else "first-order", f"cell-centre profile: max step per 1/60 s {worst:.4f}, steps > .01: {big} (576 centres x 18000 ticks)")

View file

@ -0,0 +1,9 @@
import numpy as np, probe as P
rng = np.random.default_rng(9); n = 15000
kx = rng.integers(-12, 12, n); z = rng.uniform(-3100, 3100, n); eps = 1e-6
qa = np.stack([kx * 260.0 - eps, z], -1)[None]; qb = np.stack([kx * 260.0 + eps, z], -1)[None]
for W, fold, boost, S in [(6,"f1kw",1.6,2),(6,"her",1.6,2),(6,"f1kw",4,2)]:
ang = rng.uniform(0, 6.283, n); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)[None]
a = P.node(qa, 100.0, W, boost, S, nrm, True, fold); b = P.node(qb, 100.0, W, boost, S, nrm, True, fold)
j = np.abs(a[3] - b[3]); js = np.abs(a[2]-b[2]); jf = np.abs(a[0]-b[0])
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}")

View file

@ -0,0 +1,13 @@
import numpy as np, probe as P
g = np.linspace(-3200, 3200, 200); X, Z = np.meshgrid(g, g); q = np.stack([X, Z], -1)
rng = np.random.default_rng(3); ang = rng.uniform(0, 6.283, X.shape); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)
wmin = .5
for boost, S in [(1.6, 1), (1.6, 2), (4, 2), (9, 1)]:
f1, f2, soft, out = P.node(q, 100.0, 6, boost, S, nrm)
C = np.maximum(f2, f1 + P.KW)
k = np.floor(C / wmin).astype(int) + 1 # first ring whose lower bound (k-1)*wmin >= C is excluded... rings visited 0..k
k = np.ceil(C / wmin).astype(int) # rings 0..k visited (ring k+1 has lb k*wmin >= C)
cells = (2 * k + 1) ** 2
wmax = 1.0 * (1 + boost); Usound = 1.58 * wmax * S
Wsound = int(np.ceil((Usound + P.KW) / wmin))
print(f"B{boost} S{S}: rings needed p50 {np.median(k):.0f} p99 {np.percentile(k,99):.0f} max {k.max()} -> cells p50 {np.median(cells):.0f} max {cells.max()}; worst-case sound W {Wsound} ({(2*Wsound+1)**2} cells)")

View file

@ -0,0 +1,14 @@
import numpy as np, probe as P, sys
fold = sys.argv[1]; rng = np.random.default_rng(2); jm = js = 0; nsw = 0
for k in range(3):
o = rng.uniform(-2500, 2500, 2); a = rng.uniform(0, 3.1416); d = np.array([np.cos(a), np.sin(a)])
s = np.arange(0, 15 * 260, .5); P.node((o + s[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold); a1 = P.node.a1[0].copy()
idx = np.nonzero(np.diff(a1))[0][:40]
lo = s[idx]; hi = s[idx + 1]; ref = a1[idx]
for _ in range(40):
mid = .5 * (lo + hi); P.node((o + mid[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold)
same = P.node.a1[0] == ref; lo = np.where(same, mid, lo); hi = np.where(same, hi, mid)
ra = P.node((o + lo[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold); ma = P.node.m[0].copy()
rb = P.node((o + hi[:, None] * d)[None], 100.0, 3, 1.6, 1.0, None, True, fold); mb = P.node.m[0]
jm = max(jm, np.abs(ma - mb).max()); js = max(js, np.abs(ra[2] - rb[2]).max()); nsw += len(idx)
print(fold, f"F1 swaps {nsw}: max jump m {jm:.2e}, soft {js:.2e}")

View file

@ -0,0 +1,6 @@
import numpy as np, probe as P
g = np.linspace(-3200, 3200, 200); X, Z = np.meshgrid(g, g); q = np.stack([X, Z], -1)
rng = np.random.default_rng(3); ang = rng.uniform(0, 6.283, X.shape); nrm = np.stack([np.cos(ang), np.sin(ang)], -1)
for S in (1, 2):
a = P.node(q, 100.0, 6, 1.6, S, nrm, True, "her"); b = P.node(q, 100.0, 6, 1.6, S, nrm, True, "f1kw")
d = np.abs(a[3] - b[3]); print(f"S{S} look her vs f1kw: mean {d.mean():.4f} p99 {np.percentile(d,99):.3f} max {d.max():.3f}; mean out {a[3].mean():.3f}")

View file

@ -0,0 +1,60 @@
"""Weighted lattice cellular probe (Imagination, lattice-weighted-cellular). numpy, float64.
Bodies orbit (period = distance s). Cell weight = base(hash)*(1+BOOST*exp(-(border(centre)/FALL)^2)),
border over the bodies' power scores (site.w = sigma). Her fold: row-major, m from 8; variant m0f1 starts m at F1."""
import numpy as np, sys
LAT, FALL, KW, EW, OMEGA = 260.0, 120.0, .35, .10, .05
GAP0 = .25 * KW / (1 + 3 * KW)
rng = np.random.default_rng(7)
NB = 6
DIST = np.array([0.0, 700, 1200, 1800, 2400, 3000]); PH = rng.uniform(0, 6.283, NB)
SIG = np.array([1500, 300, 450, 350, 900, 600]) / np.sqrt(128.0) * 4 # sigma ratio 5:1, edges well inside
def bodies(t):
a = PH + np.where(DIST > 0, 2 * np.pi * t / np.maximum(DIST, 1), 0)
return np.stack([np.cos(a) * DIST, np.sin(a) * DIST], -1)
def h32(x):
x = x.astype(np.uint32)
x ^= x >> 16; x *= np.uint32(0x7feb352d); x ^= x >> 15; x *= np.uint32(0x846ca68b); x ^= x >> 16
return x
def hash01(cx, cz, s):
k = ((cx.astype(np.int64) * 73856093) ^ (cz.astype(np.int64) * 19349663) ^ (s * 83492791)) & 0xffffffff
return (h32(h32(k)) >> 8) / 16777216.0
def border(q, B, exact):
"""q: (...,2). Power border distance over bodies B."""
sc = ((q[..., None, :] - B) ** 2).sum(-1) - SIG ** 2
o = np.argsort(sc, -1); i1 = o[..., 0]; i2 = o[..., 1]
s1 = B[i1]
if not exact:
s2 = B[i2]; return (np.take_along_axis(sc, i2[..., None], -1)[..., 0] - np.take_along_axis(sc, i1[..., None], -1)[..., 0]) / (2 * np.linalg.norm(s2 - s1, axis=-1))
best = np.full(q.shape[:-1], 1e30)
w1 = SIG[i1]
for k in range(NB):
d = B[k] - s1; L = np.linalg.norm(d, axis=-1)
pl = np.maximum((((s1 - q) * d).sum(-1) + .5 * ((d * d).sum(-1) + w1 ** 2 - SIG[k] ** 2)) / np.where(L > 0, L, 1), 0)
best = np.where((L > 0) & (pl < best), pl, best)
return best
def node(q, t, W, boost, S=1.0, nrm=None, exact=True, fold="her"):
B = bodies(t); p = q / LAT; n = np.floor(p); f = p - n
if nrm is None: nrm = np.zeros_like(q); nrm[..., 0] = 1
ds = []
for j in range(-W, W + 1):
for i in range(-W, W + 1):
cx, cz = n[..., 0] + i, n[..., 1] + j
base = .5 + .5 * hash01(cx, cz, 1)
P = np.exp(-(border(np.stack([(cx + .5) * LAT, (cz + .5) * LAT], -1), B, exact) / FALL) ** 2)
wt = base * (1 + boost * P)
ox = .5 + .5 * np.sin(OMEGA * t + 6.2831 * hash01(cx, cz, 2)); oz = .5 + .5 * np.sin(OMEGA * t + 6.2831 * hash01(cx, cz, 3))
vx, vz = i - f[..., 0] + ox, j - f[..., 1] + oz
a = vx * nrm[..., 0] + vz * nrm[..., 1]; c = -vx * nrm[..., 1] + vz * nrm[..., 0]
ds.append(np.hypot(S * a, c) * wt)
D = np.stack(ds, -1); srt = np.sort(D, -1); f1, f2 = srt[..., 0], srt[..., 1]; a1 = np.argmin(D, -1)
m = np.full(f1.shape, 8.0) if fold == "her" else (f1 + KW if fold == "f1kw" else f1.copy())
for k in range(D.shape[-1]):
d = D[..., k]
h = np.clip(.5 + .5 * (m - d) / KW, 0, 1); h = h * h * (3 - 2 * h)
mn = m + (d - m) * h - h * (1 - h) * KW / (1 + 3 * KW)
m = np.where((fold == "m0f1") & (a1 == k), m, mn)
soft = np.clip((f1 - m) / GAP0, 0, 1); node.m = m; node.a1 = a1
return f1, f2, soft, np.maximum(soft ** 2, np.exp(-((f2 - f1) / EW) ** 2))

View file

@ -0,0 +1,143 @@
# 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:
| | committed (CultMath.glsl, .Cycles, .Phacelle) | dxc + SPIRV-Cross |
|---|---|---|
| lines | 1035 + Cycles + Phacelle | 3458, one file (the MPL and Apache files are merged) |
| function definitions | 173 (121 + 51 + 1) | 127 |
| 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?
Yggdrasil, ubuntu:24.04, apt packages: mesa-vulkan-drivers 25.2.8 (lavapipe), glslang-tools 15.1.0,
spirv-tools 2025.1~rc1, libvulkan-dev, and the same dxc. There is no GPU.
- Device: `llvmpipe (LLVM 20.1.2, 256 bits)`, Vulkan 1.4. Float controls for f32: denormPreserve 0,
flushToZero 0, RTE 1, RTZ 0, signedZeroInfNanPreserve 1.
- HLSL route: a generated `cs_6_0` compute shader that includes CultMath.hlsl, through `dxc -spirv
-fspv-target-env=vulkan1.2`. 105 of 105 families compile and run. Fma instructions in the SPIR-V: 0.
- GLSL route: the committed CultMath.glsl, .Cycles.glsl and .Phacelle.glsl concatenated after `#version
310 es` and highp precision, as a compute shader through `glslangValidator -V`. 105 of 105 compile and
run.
- Library route: the dxc + SPIRV-Cross library from Q1 through glslang. 103 of 105 run; the 2 missing
ones are the dropped overloads. Against the fixture: 75 exact, 4 within 4 u1, 7 within 64, 17 above
64. Its worst rows are the same figures as the HLSL route's.
- Time: all 105 dispatches together take 8 s on a re-run. Each job runs about 4 minutes, almost all of
it apt, the dxc download and the SPIRV-Cross build.
- Determinism: re-running all 105 SPIR-V modules gave bit-identical outputs.
- The HLSL and GLSL routes are bit-identical to each other in 85 of 105 families.
- Against the C# fixture, HLSL route: 75 exact, 6 within 4 u1, 7 within 64, 17 above 64. GLSL route:
75, 4, 8 and 18. The largest errors are in the `coordinate bands 1e2 to 4e6` families of snoise_grad
and snoise_hessian (13e6 and 1.6e8 u1), then fbm_grad and ridged_grad (about 3.5e3 to 5e3 u1), Cycles
arcsine (2.7e3) and arccosine (1.0e3), and the other hessian families (1e3 to 2.4e3).
**Answer:** yes. The image needs nothing beyond distro packages and the pinned dxc. It is
deterministic and fast, and it runs both the HLSL and the lowered GLSL on the same implementation.
## Q3. A real-device run on Starfire without a browser: native ANGLE
Starfire. The host is `angle_native.py`, Python ctypes over the libEGL.dll and libGLESv2.dll that ship
with Docker Desktop 4.71.0 (an Electron app): ANGLE 2.1.26626, git hash a4490148bd3d. Display:
`EGL_PLATFORM_ANGLE_TYPE_D3D11`. Context: ES 3.0, optionally `EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE`.
Each family is a `#version 300 es` fragment shader, with arguments read by texelFetch from an RGBA32UI
texture and results written to up to 3 RGBA32UI targets over a 16x16 grid, then read back with
glReadPixels. The same RGBA32UI readback is used by the scratchpad browser pages (gpu.js).
- `GL_RENDERER`: `ANGLE (NVIDIA, NVIDIA GeForce GTX 1070 (0x00001B81) Direct3D11 vs_5_0 ps_5_0, D3D11)`.
`GL_VERSION`: `OpenGL ES 3.0 (ANGLE 2.1.26626 git hash: a4490148bd3d)`.
- 105 of 105 families compile and run. All 105 together take 6.3 s wall, process start included. The GL
error at the end is 0.
- The WebGL-compatibility context and the plain ES 3.0 context are bit-identical in 105 of 105 families.
- Against the C# fixture: 38 exact, 31 within 4 u1, 17 within 64, 19 above 64.
- It agrees with the earlier browser measurements on the same GPU. `cultmath_iv_exp`: 19 of 256 exact,
worst 12 ulp; the browser run of finding iv-exp-gpu-not-enclosed measured "up to 12 ulp".
`cultmath_iv_frustum_ball`: 213 of 256 exact, so 43 off, by at most 2 ulp; finding
frustum-exact-unsound-on-webgl2 measured "43/256 cases by up to 2 ulp".
- The dxc + SPIRV-Cross library from Q1 also runs on this ANGLE, as a WebGL-compatibility ES 3.00
fragment shader: 103 of 105 families (the 2 dropped overloads fail to compile), with 41 exact. It is
bit-identical to the committed lowering's ANGLE output in 71 of 103 families.
- Largest differences: the `coordinate bands 1e2 to 4e6` families again. 173 cases of snoise_grad and
415 component values of snoise_hessian differ by more than 1e-3 relative, up to 2 and 20. These
families exist for the span allowance's device margin (follow-up span-allowance-gpu-margin, cut
stage-shaders-device). Their judge is the allowance check, not an ulp bound.
**Answer:** yes. A pinned ANGLE build, loaded natively, runs the site's real translation path
(ES 3.00, then ANGLE, then HLSL, then D3DCompile, then the NVIDIA D3D11 driver) with no browser and no
SwiftShader fallback, because the backend is named explicitly and the renderer string is recorded. The
result agrees bit for bit with the WebGL-compatibility context, and it reproduces the two browser
measurements on record.
## What was not probed
- SwiftShader as a second conforming implementation.
- Amber as the dispatch host instead of a small owned one.
- WARP and native D3D11 HLSL on Starfire, which is the Unity consumer path.
- naga with dxc SPIR-V as input.
- Chrome's own ANGLE feature flags, which the native host does not reproduce.
- Mobile or mediump devices.
- An Electron release's ANGLE pinned by download. The probe used the copy Docker Desktop happens to
ship.

View file

@ -0,0 +1,96 @@
"""Probe: run the GLSL fixture families through a native ANGLE (DLLs shipped with an Electron app), D3D11
backend, ES 3.0 context, fragment-shader readback through RGBA32UI targets. No browser.
usage: py -3 -I angle_native.py <angle-dll-dir> <fixture.json> <shaders-dir> <outdir> [webgl]"""
import ctypes as C, json, os, struct, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import fam
dll, fx, sh, out = sys.argv[1:5]; webgl = len(sys.argv) > 5
os.add_dll_directory(dll)
egl = C.CDLL(os.path.join(dll, 'libEGL.dll')); gl = C.CDLL(os.path.join(dll, 'libGLESv2.dll'))
egl.eglGetProcAddress.restype = C.c_void_p
for n in ['eglInitialize', 'eglChooseConfig', 'eglCreatePbufferSurface', 'eglCreateContext', 'eglMakeCurrent', 'eglBindAPI', 'eglGetError']:
getattr(egl, n).restype = C.c_void_p if n.startswith('eglCreate') else C.c_uint
egl.eglCreatePbufferSurface.argtypes = [C.c_void_p, C.c_void_p, C.POINTER(C.c_int)]
egl.eglCreateContext.argtypes = [C.c_void_p, C.c_void_p, C.c_void_p, C.POINTER(C.c_int)]
egl.eglMakeCurrent.argtypes = [C.c_void_p, C.c_void_p, C.c_void_p, C.c_void_p]
egl.eglInitialize.argtypes = [C.c_void_p, C.c_void_p, C.c_void_p]
egl.eglChooseConfig.argtypes = [C.c_void_p, C.POINTER(C.c_int), C.POINTER(C.c_void_p), C.c_int, C.POINTER(C.c_int)]
GPD = C.CFUNCTYPE(C.c_void_p, C.c_uint, C.c_void_p, C.POINTER(C.c_int))(egl.eglGetProcAddress(b'eglGetPlatformDisplayEXT'))
def ia(*v): return (C.c_int * (len(v) + 1))(*v, 0x3038)
disp = GPD(0x3202, None, ia(0x3203, 0x3208)) # EGL_PLATFORM_ANGLE_TYPE_ANGLE = D3D11
assert egl.eglInitialize(disp, None, None), 'eglInitialize %x' % egl.eglGetError()
cfg = C.c_void_p(); ncfg = C.c_int()
egl.eglChooseConfig(disp, ia(0x3033, 0x1, 0x3040, 0x40), C.byref(cfg), 1, C.byref(ncfg)) # PBUFFER, ES3
surf = egl.eglCreatePbufferSurface(disp, cfg, ia(0x3057, 16, 0x3056, 16))
egl.eglBindAPI(0x30A0)
ctx_attr = (0x3098, 3) + ((0x33AC, 1) if webgl else ()) # CLIENT_VERSION 3 [, WEBGL_COMPATIBILITY_ANGLE]
ctx = egl.eglCreateContext(disp, cfg, None, ia(*ctx_attr))
assert ctx and egl.eglMakeCurrent(disp, surf, surf, ctx), 'context %x' % egl.eglGetError()
gl.glGetString.restype = C.c_char_p
print('GL_RENDERER', gl.glGetString(0x1F01).decode()); print('GL_VERSION', gl.glGetString(0x1F02).decode())
print('webgl compatibility context', webgl)
gl.glCreateShader.restype = C.c_uint; gl.glCreateProgram.restype = C.c_uint
def shader(kind, src):
s = gl.glCreateShader(kind); b = C.c_char_p(src.encode()); gl.glShaderSource(s, 1, C.byref(b), None); gl.glCompileShader(s)
ok = C.c_int(); gl.glGetShaderiv(s, 0x8B81, C.byref(ok))
if not ok.value:
buf = C.create_string_buffer(4000); gl.glGetShaderInfoLog(s, 4000, None, buf); raise RuntimeError(buf.value.decode()[:300])
return s
VS = '#version 300 es\nvoid main() { vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); }\n'
lib = open(os.environ['LIBGLSL']).read() if os.environ.get('LIBGLSL') else ''.join(open(os.path.join(sh, f)).read() for f in ['CultMath.glsl', 'CultMath.Cycles.glsl', 'CultMath.Phacelle.glsl'])
vao = C.c_uint(); gl.glGenVertexArrays(1, C.byref(vao)); gl.glBindVertexArray(vao)
d = json.load(open(fx)); man = []; skipped = 0
for idx, f in enumerate(d['functions']):
r, name, ps = fam.parse(f['glsl']); rt = fam.ty(r); pts = [fam.ty(p) for p in ps]
if rt is None or any(p is None for p in pts): skipped += 1; continue
nin = sum(p[1] for p in pts); nout = rt[1]; tw = (nin + 3) // 4; nt = (nout + 3) // 4; tag = 'f%03d' % idx
words = [int(x, 16) for c in f['cases'] for x in c.split('->')[0].split()]
tex = []
for ci in range(256):
row = words[ci * nin:(ci + 1) * nin]; tex += row + [0] * (tw * 4 - nin)
lines, off = [], 0
for j, (p, (ht, n, k)) in enumerate(zip(ps, pts)):
src = ', '.join('w[%d]' % (off + t) for t in range(n)); v = ('uvec%d(%s)' % (n, src)) if n > 1 else src
conv = {'f': 'uintBitsToFloat', 'i': ('ivec%d' % n if n > 1 else 'int'), 'u': ''}[k]
lines.append(' %s a%d = %s(%s);' % (p, j, conv, v)); off += n
lines.append(' %s r = %s(%s);' % (r, name, ', '.join('a%d' % j for j in range(len(ps)))))
comps = ['r[%d][%d]' % (a, b) for a in range(3) for b in range(3)] if r == 'mat3' else ['r'] if nout == 1 else ['r[%d]' % i for i in range(nout)]
for i, c in enumerate(comps):
lines.append(' ow[%d] = %s(%s);' % (i, {'f': 'floatBitsToUint', 'i': 'uint', 'u': ''}[rt[2]], c))
outs = ''.join('layout(location = %d) out highp uvec4 o%d;\n' % (t, t) for t in range(nt))
wr = ''.join(' o%d = uvec4(ow[%d], ow[%d], ow[%d], ow[%d]);\n' % (t, 4 * t, 4 * t + 1, 4 * t + 2, 4 * t + 3) for t in range(nt))
FS = ('#version 300 es\nprecision highp float;\nprecision highp int;\n' + lib + '\nuniform highp usampler2D In;\n' + outs +
'void main() {\n int i = int(gl_FragCoord.y) * 16 + int(gl_FragCoord.x);\n uint w[%d];\n for (int k = 0; k < %d; k++) w[k] = texelFetch(In, ivec2(k / 4, i), 0)[k %% 4];\n'
' uint ow[%d];\n' % (tw * 4, nin, nt * 4) + '\n'.join(lines) + '\n' + ''.join(' ow[%d] = 0u;\n' % z for z in range(nout, nt * 4)) + wr + '}\n')
try:
prog = gl.glCreateProgram(); gl.glAttachShader(prog, shader(0x8B31, VS)); gl.glAttachShader(prog, shader(0x8B30, FS)); gl.glLinkProgram(prog)
except RuntimeError as e:
print('compile fail', f['glsl'], str(e).replace('\n', ' ')[:200]); continue
gl.glUseProgram(prog)
t_in = C.c_uint(); gl.glGenTextures(1, C.byref(t_in)); gl.glActiveTexture(0x84C0); gl.glBindTexture(0x0DE1, t_in)
gl.glTexParameteri(0x0DE1, 0x2801, 0x2600); gl.glTexParameteri(0x0DE1, 0x2800, 0x2600)
buf = (C.c_uint * len(tex))(*tex); gl.glTexImage2D(0x0DE1, 0, 0x8D70, tw, 256, 0, 0x8D99, 0x1405, buf)
gl.glUniform1i(gl.glGetUniformLocation(prog, b'In'), 0)
fbo = C.c_uint(); gl.glGenFramebuffers(1, C.byref(fbo)); gl.glBindFramebuffer(0x8D40, fbo)
tos = (C.c_uint * nt)(); gl.glGenTextures(nt, tos)
for t in range(nt):
gl.glBindTexture(0x0DE1, tos[t]); gl.glTexImage2D(0x0DE1, 0, 0x8D70, 16, 16, 0, 0x8D99, 0x1405, None)
gl.glFramebufferTexture2D(0x8D40, 0x8CE0 + t, 0x0DE1, tos[t], 0)
gl.glDrawBuffers(nt, (C.c_uint * nt)(*[0x8CE0 + t for t in range(nt)]))
gl.glActiveTexture(0x84C0); gl.glBindTexture(0x0DE1, t_in)
gl.glViewport(0, 0, 16, 16); gl.glDrawArrays(0x0004, 0, 3)
res = [[0] * 1024 for _ in range(nt)]
for t in range(nt):
gl.glReadBuffer(0x8CE0 + t); px = (C.c_uint * 1024)(); gl.glReadPixels(0, 0, 16, 16, 0x8D99, 0x1405, px); res[t] = list(px)
flat = []
for ci in range(256):
allw = sum((res[t][ci * 4:ci * 4 + 4] for t in range(nt)), []); flat += allw[:nout]
open(os.path.join(out, tag + '.in'), 'wb').write(b'\0')
open(os.path.join(out, tag + '.angle.out'), 'wb').write(struct.pack('<%dI' % len(flat), *flat))
man.append({'tag': tag, 'glsl': f['glsl'], 'n': 256, 'nout': nout, 'kind': rt[2], 'tol': f['tolerance'], 'platform': f.get('platform')})
gl.glDeleteFramebuffers(1, C.byref(fbo)); gl.glDeleteTextures(nt, tos); gl.glDeleteTextures(1, C.byref(t_in)); gl.glDeleteProgram(prog)
print('gl error at end 0x%x' % gl.glGetError(), '; ran', len(man), 'skipped struct', skipped)
json.dump(man, open(os.path.join(out, 'manifest.json'), 'w'))
fam.cmp(fx, out, 'angle')

View file

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Probe: per fixture family, generate an HLSL and a GLSL ES 3.10 compute shader that call the family's
function on the fixture's argument bits, and compare device results with the fixture's result bits.
usage: fam.py gen <fixture> <outdir> | fam.py cmp <fixture> <outdir> <tag>"""
import json, re, struct, sys, os
SCAL = {'float': ('float', 1, 'f'), 'int': ('int', 1, 'i'), 'uint': ('uint', 1, 'u')}
def ty(t):
if t in SCAL: return SCAL[t]
m = re.fullmatch(r'(i|u|)vec([234])', t)
if m:
k = {'': 'f', 'i': 'i', 'u': 'u'}[m.group(1)]; n = int(m.group(2))
return ({'f': 'float', 'i': 'int', 'u': 'uint'}[k] + str(n), n, k)
if t == 'mat3': return ('float3x3', 9, 'f')
return None
def parse(sig):
m = re.fullmatch(r'(\w+)\s+(\w+)\((.*)\)', sig)
ps = [p.strip() for p in m.group(3).split(',') if p.strip()]
return m.group(1), m.group(2), ps
def hlsl_read(k, words):
return {'f': 'asfloat', 'i': 'asint', 'u': ''}[k] + '(' + words + ')'
def gen(fx, out):
d = json.load(open(fx)); man = []; skipped = []
for idx, f in enumerate(d['functions']):
r, name, ps = parse(f['glsl'])
rt = ty(r); pts = [ty(p) for p in ps]
if rt is None or any(p is None for p in pts):
skipped.append(f['glsl']); continue
nin = sum(p[1] for p in pts); nout = rt[1]
tag = 'f%03d' % idx
words = []
for c in f['cases']:
a, b = c.split('->'); aw = a.split(); assert len(aw) == nin, (f['glsl'], len(aw), nin)
words += [int(x, 16) for x in aw]
open(os.path.join(out, tag + '.in'), 'wb').write(struct.pack('<%dI' % len(words), *words) if words else b'\0\0\0\0')
# HLSL
hl, gl, off = [], [], 0
for j, (p, (ht, n, k)) in enumerate(zip(ps, pts)):
idxs = ', '.join('In[b + %d]' % (off + t) for t in range(n))
gidx = ', '.join('In.a[b + %du]' % (off + t) for t in range(n))
if ht == 'float3x3': raise SystemExit('mat3 arg unsupported')
vec = ('uint%d(%s)' % (n, idxs)) if n > 1 else idxs
hl.append(' %s a%d = %s;' % (ht, j, hlsl_read(k, vec) if k != 'u' else vec))
gvec = ('uvec%d(%s)' % (n, gidx)) if n > 1 else gidx
gfun = {'f': 'uintBitsToFloat', 'i': ('ivec%d' % n if n > 1 else 'int'), 'u': ''}[k]
gl.append(' %s a%d = %s(%s);' % (p, j, gfun, gvec))
off += n
args = ', '.join('a%d' % j for j in range(len(ps)))
hl.append(' %s r = %s(%s);' % (rt[0], name, args))
gl.append(' %s r = %s(%s);' % (r, name, args))
if rt[0] == 'float3x3':
comps = ['r[%d][%d]' % (i, j) for i in range(3) for j in range(3)]
elif rt[1] == 1: comps = ['r']
else: comps = ['r[%d]' % i for i in range(rt[1])]
for i, c in enumerate(comps):
hl.append(' Out[o + %d] = %s;' % (i, {'f': 'asuint', 'i': 'asuint', 'u': ''}[rt[2]] + '(' + c + ')'))
gl.append(' Out.a[o + %du] = %s;' % (i, {'f': 'floatBitsToUint', 'i': 'uint', 'u': ''}[rt[2]] + '(' + c + ')'))
n = len(f['cases'])
open(os.path.join(out, tag + '.hlsl'), 'w').write(
'#include "CultMath.hlsl"\n[[vk::binding(0)]] StructuredBuffer<uint> In;\n[[vk::binding(1)]] RWStructuredBuffer<uint> Out;\n'
'[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'
% (n, nin, nout, '\n'.join(hl)))
open(os.path.join(out, tag + '.body.glsl'), 'w').write(
'layout(local_size_x = 64) in;\nlayout(std430, binding = 0) readonly buffer IB { uint a[]; } In;\nlayout(std430, binding = 1) buffer OB { uint a[]; } Out;\n'
'void main() {\n uint i = gl_GlobalInvocationID.x; if (i >= %du) return; uint b = i * %du; uint o = i * %du;\n%s\n}\n'
% (n, nin, nout, '\n'.join(gl)))
man.append({'tag': tag, 'glsl': f['glsl'], 'n': n, 'nout': nout, 'kind': rt[2], 'tol': f['tolerance'], 'platform': f.get('platform')})
json.dump(man, open(os.path.join(out, 'manifest.json'), 'w'))
print('generated', len(man), 'families; skipped (struct types)', len(skipped), ':', '; '.join(skipped))
def ordered(u): # float bits -> monotonic int for ulp distance
return u if u < 0x80000000 else 0x80000000 - u
def cmp(fx, out, tag):
d = json.load(open(fx)); man = json.load(open(os.path.join(out, 'manifest.json')))
rows = []; buckets = {'exact': 0, '<=4': 0, '<=64': 0, '>64': 0, 'missing': 0}
for m in man:
p = os.path.join(out, m['tag'] + '.' + tag + '.out')
if not os.path.exists(p): buckets['missing'] += 1; rows.append((m['glsl'], 'MISSING', '')); continue
got = struct.unpack('<%dI' % (m['n'] * m['nout']), open(p, 'rb').read())
f = d['functions'][int(m['tag'][1:])]
worst = 0; exact = 0; nanmis = 0
for ci, c in enumerate(f['cases']):
exp = [int(x, 16) for x in c.split('->')[1].split()]
g = got[ci * m['nout']:(ci + 1) * m['nout']]
if list(g) == exp: exact += 1; continue
for e, h in zip(exp, g):
if m['kind'] != 'f': worst = max(worst, 1 << 40); continue
en = (e & 0x7f800000) == 0x7f800000 and e & 0x7fffff; hn = (h & 0x7f800000) == 0x7f800000 and h & 0x7fffff
if en or hn:
if not (en and hn): nanmis += 1
continue
fe = struct.unpack('<f', struct.pack('<I', e))[0]; fh = struct.unpack('<f', struct.pack('<I', h))[0]
if fe != fe or fh != fh or abs(fe) == float('inf') or abs(fh) == float('inf'):
if not (fe == fh or (fe != fe and fh != fh)): nanmis += 1
continue
worst = max(worst, abs(fe - fh) / max(abs(fe), 1.0) / 2.0 ** -23)
b = 'exact' if exact == m['n'] else '<=4' if worst <= 4 and not nanmis else '<=64' if worst <= 64 and not nanmis else '>64'
if worst >= 1 << 40: b = '>64'
buckets[b] += 1
rows.append((m['glsl'], '%d/%d exact' % (exact, m['n']), 'max err %s ulp(max(|v|,1))%s' % (('%.1f' % worst) if worst < 1 << 40 else 'int-mismatch', ', nan-mismatch %d' % nanmis if nanmis else '')))
print(tag, 'buckets', buckets)
for r in sorted(rows, key=lambda r: r[1])[:0]: pass
rows.sort(key=lambda r: -float(re.search(r'max err ([0-9.]+)', r[2]).group(1)) if re.search(r'max err ([0-9.]+)', r[2]) else -1e18)
for r in rows[:int(os.environ.get('CMP_TOP', '25'))]:
if not r[1].startswith('256/256'): print(' ', r[0], '|', r[1], '|', r[2])
if __name__ == '__main__':
{'gen': lambda: gen(sys.argv[2], sys.argv[3]), 'cmp': lambda: cmp(sys.argv[2], sys.argv[3], sys.argv[4])}[sys.argv[1]]()

View file

@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Probe job, runs in ubuntu:24.04 with /src = CultLib at the probed revision. Q1 library path, Q2 lavapipe runs.
set -u
P=/tmp/p; W=/tmp/w; mkdir -p $W/fam $W/tools; cd $W
step() { echo; echo "=== $*"; }
step apt
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 \
mesa-vulkan-drivers libvulkan-dev vulkan-tools glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
dpkg-query -W -f='${Package} ${Version}\n' mesa-vulkan-drivers glslang-tools spirv-tools libvulkan-dev
step dxc
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
step spirv-cross
git clone -q --depth 1 https://github.com/KhronosGroup/SPIRV-Cross.git tools/sc && git -C tools/sc log -1 --format='SPIRV-Cross %H %cs' && \
cmake -S tools/sc -B tools/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF >/dev/null && cmake --build tools/scb --target spirv-cross -j4 >/dev/null 2>&1; SC=tools/scb/spirv-cross; ls -la $SC | cut -c1-120
SH=/src/packages/cultmath/shaders
python3 - "$SH" <<'PY'
import re,sys,os,shutil
src=sys.argv[1]; dst='/tmp/w/shx'; shutil.copytree(src,dst,dirs_exist_ok=True); n=0
for fn in os.listdir(dst):
if fn.endswith('.hlsl'):
t=open(os.path.join(dst,fn)).read(); t2,k=re.subn(r'(?m)^(?!static|export)(\w+(?:\dx\d)?\s+cultmath_\w+\s*\(([^)]*)\))', lambda m: m.group(1) if re.search('Texture2D|SamplerState', m.group(2)) else 'export ' + m.group(1), t); k=len(re.findall(r'(?m)^export ', t2)); n+=k; open(os.path.join(dst,fn),'w').write(t2)
print('export keywords added', n)
PY
step "L1 dxc lib_6_3 universal1.5, export on non-texture defs"
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -I shx shx/CultMath.hlsl -Fo lib.spv 2>&1 | head -3; echo "exit ${PIPESTATUS[0]}"
spirv-dis lib.spv -o lib.spvasm; echo "exports $(grep -c 'LinkageAttributes.*Export' lib.spvasm); capabilities $(grep -c OpCapability lib.spvasm)"
step "L1b -default-linkage external, no export keyword"
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -default-linkage external -I $SH $SH/CultMath.hlsl -Fo libd.spv 2>&1 | head -3; echo "exit ${PIPESTATUS[0]}"
[ -s libd.spv ] && { spirv-dis libd.spv -o libd.spvasm; echo "exports $(grep -c 'LinkageAttributes.*Export' libd.spvasm)"; }
step "L2 strip every capability but Shader and Linkage, every extension; SPIRV-Cross ES 310 and 300"
grep -vP '^\s*OpCapability (?!Shader$|Linkage$)|^\s*OpExtension ' lib.spvasm > s.spvasm; spirv-as --target-env spv1.5 s.spvasm -o s.spv; echo "as exit $?"
for v in 310 300; do $SC s.spv --es --version $v --output s.$v.glsl 2>&1 | head -2; echo "es$v exit ${PIPESTATUS[0]}"; done
G=s.310.glsl; echo "G=$G"; [ -s "$G" ] || { echo PROBE-DONE; exit 0; }
step "L3 shape of $G"
echo "lines $(wc -l < $G); defs cultmath_: $(grep -cE '^\w+ cultmath_\w+\(' $G); distinct: $(grep -oE '^\w+ cultmath_\w+\(' $G | sed -E 's/^\w+ //' | sort -u | wc -l); renamed _N: $(grep -oE '^\w+ cultmath_\w+_[0-9]+\(' $G | wc -l); _NNN temporaries: $(grep -oE '\b_[0-9]+\b' $G | wc -l); non-cultmath defs: $(grep -cE '^\w+ (?!cultmath_)\w+\(' -P $G)"
grep -nE '^#|^layout|precision|^struct|^void main' $G | head -12
echo "--- renamed defs sample:"; grep -oE '^\w+ cultmath_\w+_[0-9]+\([^)]*\)' $G | head -6
echo "--- snoise(vec3):"; awk '/^(highp )?float cultmath_snoise\((highp )?vec3/{p=1} p{print; if(++n>=22) exit}' $G
sed -E '/^#version/d; /^layout\(local_size/d' $G | awk 'BEGIN{s=0} /^void main\(\)/{s=1} s==0{print} s==1&&/^\}/{s=0}' > lib.body.glsl
{ printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl; printf 'out highp vec4 c;\nvoid main() { c = vec4(cultmath_snoise(gl_FragCoord.xyz)); }\n'; } > es300.frag
glslangValidator es300.frag > es300.txt 2>&1; echo "glslang ES 3.00 fragment with the library: exit $? $(grep -m3 ERROR es300.txt | tr '\n' ' ' | cut -c1-300)"
step "L4 library through glslang on lavapipe"
gcc -O2 -o vkrun $P/vkrun.c -lvulkan 2>/dev/null; python3 $P/fam.py gen /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam >/dev/null
okl=0; for b in fam/*.body.glsl; do t=${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'])")
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl $b; } > $t.l.comp
glslangValidator -V --target-env vulkan1.2 -o $t.l.spv $t.l.comp >$t.l.log 2>&1 && ./vkrun $t.l.spv $t.in $t.lib.out $n && okl=$((okl+1)); done
echo "library path ran $okl of 105"; grep -h -m1 ERROR fam/*.l.log | sed -E 's/[0-9]+:/N:/g' | sort | uniq -c | sort -rn | head -6
CMP_TOP=8 python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam lib
echo "LIBGZ-BEGIN"; gzip -9c lib.body.glsl | base64 -w0; echo; echo "LIBGZ-END"
echo PROBE-DONE

View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Probe: by-value parameter lowering on a SPIR-V module. dxc -Od passes every parameter as a Function pointer and the
caller copies in; a callee that assigns its parameter then comes out of SPIRV-Cross as `inout`. This pass makes each
Function-pointer parameter a value parameter: the old param id becomes a local variable in the entry block, stored from the
new value parameter; each call loads the pointer it passed. usage: byvalue.py <in.spv> <out.spv>"""
import struct, sys
def load(p):
b = open(p, 'rb').read(); w = list(struct.unpack('<%dI' % (len(b) // 4), b)); ins = []; i = 5
while i < len(w): n = w[i] >> 16; ins.append(w[i:i + n]); i += n
return w[:5], ins
def op(x): return x[0] & 0xffff
def mk(o, *ws): return [((len(ws) + 1) << 16) | o, *ws]
h, ins = load(sys.argv[1]); bound = h[3]
ptr = {x[1]: (x[2], x[3]) for x in ins if op(x) == 32} # id -> (storage, pointee)
ftype = {x[1]: x for x in ins if op(x) == 33}
fnparams = {}; cur = None; fntype = {}
for x in ins: # analysis: per function, which params are Function pointers
if op(x) == 54: cur = x[2]; fnparams[cur] = []; fntype[cur] = x[4]
elif op(x) == 55 and cur is not None:
t = x[1]; conv = t in ptr and ptr[t][0] == 7
fnparams[cur].append((x[2], t, ptr[t][1] if conv else None, bound if conv else None)); bound += conv
newtype = {}; typeins = []; sig = {tuple(x[2:]): x[1] for x in ins if op(x) == 33}
for f, ps in fnparams.items():
if any(p[2] for p in ps):
ft = ftype[fntype[f]]; key = (ft[2], *[p[2] if p[2] else p[1] for p in ps])
if key not in sig: bound += 1; sig[key] = bound; typeins.append(mk(33, bound, *key))
newtype[f] = sig[key]
names = {x[1]: x for x in ins if op(x) == 5}
out = []; cur = None; pend = []; need_stores = False; first = True; pc = 0
for x in ins:
o = op(x)
if o == 54 and first: out += typeins; first = False
if need_stores and o != 59:
out += [mk(62, p, q) for p, _, q in pend]; pend = []; need_stores = False
if o == 54:
cur = x[2]; pc = 0
if cur in newtype: x = x[:4] + [newtype[cur]] + x[5:]
elif o == 55:
_, t, pointee, q = fnparams[cur][pc]; pc += 1
if pointee: out.append(mk(55, pointee, q)); pend.append((x[2], t, q)); continue
elif o == 248 and pend:
out.append(x); out += [mk(59, t, p, 7) for p, t, _ in pend]; need_stores = True; continue
elif o == 57:
callee = x[3]; args = x[4:]; new = []
for a, (_, _, pointee, _) in zip(args, fnparams[callee]):
if pointee: bound += 1; out.append(mk(61, pointee, bound, a)); new.append(bound)
else: new.append(a)
x = x[:4] + new; x[0] = (len(x) << 16) | 57
out.append(x)
if o == 5 and x[1] in [p[0] for ps in fnparams.values() for p in ps if p[2]]:
q = next(p[3] for ps in fnparams.values() for p in ps if p[0] == x[1]); out.append([x[0], q] + x[2:])
h[3] = bound + 1; w = h + [v for x in out for v in x]; open(sys.argv[2], 'wb').write(struct.pack('<%dI' % len(w), *w))
print('byvalue: %d functions changed, %d params' % (len(newtype), sum(1 for ps in fnparams.values() for p in ps if p[2])))

View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Imagination probe: what dxc -Od leaves in the SPIRV-Cross GLSL and what cleans it. ubuntu:24.04, /src = CultLib, /cache persists tools.
set -u; P=/src/docs/research/native-shader-probes; O=$P/overload-wrappers; W=/tmp/w; mkdir -p $W /cache; cd $W
step() { echo; echo "=== $*"; }
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 gcc \
mesa-vulkan-drivers libvulkan-dev glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
dpkg-query -W -f='${Package} ${Version}\n' spirv-tools glslang-tools mesa-vulkan-drivers
if [ ! -x /cache/sc/spirv-cross ]; then
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Cross.git /cache/scsrc
cmake -S /cache/scsrc -B /cache/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF -DSPIRV_CROSS_CLI=ON >/dev/null && cmake --build /cache/scb --target spirv-cross -j8 >/dev/null 2>&1
mkdir -p /cache/sc; cp /cache/scb/spirv-cross /cache/sc/
fi
if [ ! -x /cache/dxc/bin/dxc ]; then
curl -sL --fail -o /cache/dxc.tgz https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.9.2607/linux_dxc_2026_07_29.x86_x64.tar.gz
mkdir -p /cache/dxc && tar xzf /cache/dxc.tgz -C /cache/dxc
fi
DXC=$(find /cache/dxc -name dxc -type f | head -1); SC=/cache/sc/spirv-cross; export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:${LD_LIBRARY_PATH:-}
if [ ! -x /cache/st/spirv-opt ]; then
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Tools.git /cache/stsrc
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Headers.git /cache/stsrc/external/spirv-headers
cmake -S /cache/stsrc -B /cache/stb -DCMAKE_BUILD_TYPE=Release -DSPIRV_SKIP_TESTS=ON -DSPIRV_WERROR=OFF >/dev/null && cmake --build /cache/stb --target spirv-opt spirv-val spirv-dis spirv-as -j8 >/dev/null 2>&1
mkdir -p /cache/st; cp /cache/stb/tools/spirv-* /cache/st/
fi
export PATH=/cache/st:$PATH; echo "spirv-opt from $(which spirv-opt)"
echo "dxc $DXC"; $DXC --version 2>&1|head -2; spirv-opt --version | head -1
SH=/src/packages/cultmath/shaders; FX=/src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json
step "1 build the -Od library as in Pass 9b"
python3 $O/wrapgen.py $SH shw
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od -I shw shw/cm_wrappers.hlsl -Fo w.spv 2>&1 | head -3
python3 $O/spvedit.py edit w.spv e.spv shw/wrappers.tsv export; spirv-val --target-env spv1.5 e.spv && echo "val ok"
nf() { spirv-dis "$1" | grep -c ' = OpFunction '; }; echo "functions in e.spv: $(nf e.spv)"
step "2 variants: by-value lowering, then spirv-opt (never an inliner)"
cp e.spv v0.spv
python3 $P/od-temporaries/byvalue.py e.spv b0.spv; spirv-val --target-env spv1.5 b0.spv && echo "b0 val ok"
spirv-opt --target-env=spv1.5 --ssa-rewrite --eliminate-dead-code-aggressive --simplify-instructions e.spv -o v2.spv
SSA="--ssa-rewrite --eliminate-dead-code-aggressive"
declare -A VP=([b1]="--eliminate-local-single-store --eliminate-dead-code-aggressive" [b2]="$SSA" [b3]="$SSA --simplify-instructions" [b4]="--eliminate-local-single-block --eliminate-local-single-store --eliminate-local-multi-store --eliminate-dead-code-aggressive --merge-blocks")
for v in b1 b2 b3 b4; do spirv-opt --target-env=spv1.5 ${VP[$v]} b0.spv -o $v.spv 2>$v.err; echo "$v opt exit $? functions $(nf $v.spv 2>/dev/null) $(head -c 160 $v.err)"; done
cp b1.spv b1r.spv; VS="v0 b0 b1 b1r b3"
for v in $VS; do [ -s $v.spv ] || continue
spirv-val --target-env spv1.5 $v.spv >$v.val 2>&1; vx=$?
$SC $v.spv --es --version 310 $([ $v = b1r ] && echo --relax-nan-checks) --output $v.glsl >$v.sc 2>&1; sx=$?
[ -s $v.glsl ] || { echo "$v val $vx sc exit $sx"; head -3 $v.val $v.sc; continue; }
python3 - $v $vx $sx <<'PY'
import re,sys
v,vx,sx=sys.argv[1:4]; g=open(f'/tmp/w/{v}.glsl').read()
defs=re.findall(r'(?m)^(?:\w+ )*?(\w+) (cultmath_\w+)\(([^)]*)\)\n\{',g)
ids=re.findall(r'\b_\d+\b',g)
print(f'{v}: val={vx} sc={sx} lines={g.count(chr(10))} bytes={len(g)} cultmath_defs={len(defs)} distinct={len(set(d[1] for d in defs))} id_tokens={len(ids)} distinct_ids={len(set(ids))} param_var={g.count("param_var_")} inout={len(re.findall(r"inout",g))} suffixed={len(re.findall(r"cultmath_\w+_\d+\(",g))}')
PY
done
mkdir -p /cache/out; cp *.glsl *.spv /cache/out/ 2>/dev/null
step "3 SPIRV-Cross options"
for opt in "--remove-unused-variables"; do for v in v0 b2; do $SC $v.spv --es --version 310 $opt --output $v.o.glsl >/dev/null 2>&1; echo "$v + $opt: lines $(wc -l < $v.o.glsl)"; done; done
step "4 lerp(vec3) as emitted: v0 vs b2, vs committed"
for v in v0 b2; do echo "--- $v lerp(vec3)"; awk '/^(highp )?vec3 cultmath_lerp\((highp )?vec3/{p=1} p{print} p&&/^}/{exit}' $v.glsl; done
echo "--- committed lerp(vec3)"; grep -n -A3 -E '^(highp )?vec3 cultmath_lerp\(' $SH/CultMath.glsl | head -6
for v in v0 b2; do echo "--- $v snoise(vec3) head"; awk '/^(highp )?float cultmath_snoise\((highp )?vec3/{p=1} p{print; if(++n>=40) exit}' $v.glsl; done
step "5 conformance on lavapipe, every family, per variant vs committed GLSL and HLSL"
mkdir -p fam; gcc -O2 -o vkrun $P/vkrun.c -lvulkan && python3 $P/fam.py gen $FX fam >/dev/null
cat $SH/CultMath.glsl $SH/CultMath.Cycles.glsl $SH/CultMath.Phacelle.glsl > all.glsl
body() { sed -E '/^#version/d; /^#ifdef SPIRV_CROSS/d; /^#endif/d; /^layout\(local_size/d' $1 | awk 'BEGIN{s=0} /^void main\(\)/{s=1} s==0{print} s==1&&/^\}/{s=0}'; }
for v in $VS; do [ -s $v.glsl ] && body $v.glsl > $v.body; done
run() { local t=fam/$1 n; n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$1'];print(m['n'],m['nout'])")
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat $3 $t.body.glsl; } > $t.$2.comp
glslangValidator -V --target-env vulkan1.2 -o $t.$2.spv $t.$2.comp >$t.$2.log 2>&1 && ./vkrun $t.$2.spv $t.in $t.$2.out $n; }
for b in fam/*.body.glsl; do tag=$(basename ${b%.body.glsl})
n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$tag'];print(m['n'],m['nout'])")
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH fam/$tag.hlsl -Fo fam/$tag.h.spv >/dev/null 2>&1 && ./vkrun fam/$tag.h.spv fam/$tag.in fam/$tag.h.out $n
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; do echo "--- cmp vs C# fixture: $t"; CMP_TOP=3 python3 $P/fam.py cmp $FX fam $t 2>&1 | grep -v "^ *$" | head -8; done
echo PROBE-DONE

View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Imagination probe, lavapipe leg: every fixture family through three routes on lavapipe.
# h = HLSL by dxc cs_6_0, g = committed GLSL by glslang, r = restored overloaded library GLSL (lib.body.glsl) by glslang.
set -u; P=/tmp/p; W=/tmp/w; cd $W; SH=/src/packages/cultmath/shaders; FX=/src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json
echo; echo "=== 2b lavapipe: same computation"
mkdir -p fam; gcc -O2 -o vkrun $P/vkrun.c -lvulkan && python3 $P/fam.py gen $FX fam >/dev/null
cat $SH/CultMath.glsl $SH/CultMath.Cycles.glsl $SH/CultMath.Phacelle.glsl > all.glsl
ok=(0 0 0); first=1
for b in fam/*.body.glsl; do t=${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'])")
$DXC_PATH -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH $t.hlsl -Fo $t.h.spv >$t.h.log 2>&1 && ./vkrun $t.h.spv $t.in $t.h.out $n && ok[0]=$((ok[0]+1))
for r in g r; do src=all.glsl; [ $r = r ] && src=lib.body.glsl
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat $src $b; } > $t.$r.comp
glslangValidator -V --target-env vulkan1.2 -o $t.$r.spv $t.$r.comp >$t.$r.log 2>&1 && ./vkrun $t.$r.spv $t.in $t.$r.out $n && { [ $r = g ] && ok[1]=$((ok[1]+1)) || ok[2]=$((ok[2]+1)); }
done
done
echo "ran on lavapipe: hlsl ${ok[0]}, committed glsl ${ok[1]}, restored glsl ${ok[2]} (of $(ls fam/*.body.glsl | wc -l))"
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)]
rg=same('r','g'); rh=same('r','h'); gh=same('g','h')
print('bit-identical: restored vs committed glsl',len(rg),'| restored vs hlsl',len(rh),'| committed glsl vs hlsl',len(gh),'of',len(tags))
print('restored differs from committed glsl in:',[t for t in tags if t not in rg][:12])
print('restored differs from hlsl but committed glsl matches hlsl:',[t for t in gh if t not in rh][:12])
PY
CMP_TOP=10 python3 $P/fam.py cmp $FX fam r 2>&1 | head -16

View file

@ -0,0 +1,39 @@
import re,sys,os,collections
sh,g,out=sys.argv[1:4]; os.makedirs(out,exist_ok=True)
origin={}; owner={'CultMath.hlsl':'core','CultMath.Interval.hlsl':'core','CultMath.Affine.hlsl':'core','CultMath.Cycles.hlsl':'cycles','CultMath.Phacelle.hlsl':'phacelle'}
for fn,o in owner.items():
t=open(os.path.join(sh,fn)).read()
for m in re.finditer(r'(?m)^(?:export\s+)?\w+\s+(cultmath_\w+)\s*\(',t): origin.setdefault(m.group(1),o)
for m in re.finditer(r'(?m)^struct\s+(\w+)',t): origin.setdefault(m.group(1),o)
text=open(g).read()
# top-level chunks: struct blocks and function definitions; everything else is prologue
lines=text.split('\n'); chunks=[]; i=0; pro=[]
while i<len(lines):
l=lines[i]
m=re.match(r'^struct (\w+)$',l) or re.match(r'^\w+ (\w+)\(.*\)$',l)
if m and m.group(1)!='main':
j=i
while lines[j]!='};' and lines[j]!='}': j+=1
chunks.append((m.group(1),'\n'.join(lines[i:j+1]))); i=j+1
elif m:
j=i
while lines[j]!='}': j+=1
i=j+1
else:
if l.strip(): pro.append(l)
i+=1
print('prologue lines',len(pro),':',' | '.join(pro[:6])[:300])
cnt=collections.Counter(); unknown=[]; bad=collections.Counter(); files=collections.defaultdict(list)
rank={'core':0,'cycles':1,'phacelle':1}
for name,body in chunks:
o=origin.get(name)
if o is None: unknown.append(name); o='core'
cnt[o]+=1; files[o].append(body)
for callee in set(re.findall(r'\b(cultmath_\w+|[A-Z]\w+)\b',body))-{name}:
co=origin.get(callee)
if co and co!=o and not (rank[co]==0): bad[(o,co,name,callee)]+=1
print('chunks',len(chunks),dict(cnt),'unknown-origin',len(unknown),unknown[:8])
print('cross-licence calls (non-core callee):',len(bad),list(bad)[:6])
seen=collections.Counter(n for n,_ in chunks); print('duplicate chunk names',[n for n,c in seen.items() if c>1][:6])
for o,b in files.items(): open(os.path.join(out,o+'.glsl'),'w').write('\n'.join(b)+'\n')
print('split sizes', {o:len('\n'.join(b).split('\n')) for o,b in files.items()})

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Probe: instruction-level SPIR-V edit (binary, no text regex).
stats <spv> <wrappers.tsv> how each wrapper looks after dxc
edit <in> <out> <wrappers.tsv> <mode> mode drop: strip caps (keep Shader, Linkage) and extensions, delete
wrapper functions and their names/decorations, set each callee's OpName to the overloaded source name.
mode export: as drop, but each wrapper's Export decoration moves to its callee instead of being deleted."""
import struct, sys, collections
def load(p):
b = open(p, 'rb').read(); w = list(struct.unpack('<%dI' % (len(b) // 4), b)); ins = []; i = 5
while i < len(w): n = w[i] >> 16; ins.append(w[i:i + n]); i += n
return w[:5], ins
def s_dec(ws):
b = struct.pack('<%dI' % len(ws), *ws); return b[:b.index(0)].decode()
def s_enc(s):
b = s.encode() + b'\0'; b += b'\0' * (-len(b) % 4); return list(struct.unpack('<%dI' % (len(b) // 4), b))
def op(x): return x[0] & 0xffff
def funcs(ins):
fs = {}; cur = None
for k, x in enumerate(ins):
if op(x) == 54: cur = x[2]; fs[cur] = [k, None, [], set()]
if cur is not None:
if op(x) == 57: fs[cur][2].append(x[3])
o = op(x)
if o == 248: fs[cur][3].add(x[1])
elif o not in (8, 317, 62, 246, 247, 249, 250, 253, 254, 252) and len(x) > 2: fs[cur][3].add(x[2])
if op(x) == 56: fs[cur][1] = k; cur = None
return fs
def names(ins): return {x[1]: s_dec(x[2:]) for x in ins if op(x) == 5}
def wrappers(tsv): return [l.split('\t') for l in open(tsv).read().splitlines() if l]
def analyse(ins, tsv):
nm = names(ins); fs = funcs(ins); byname = {v: k for k, v in nm.items() if k in fs}; res = []
for wn, name, fn, ps in wrappers(tsv):
fid = byname.get(wn); callees = fs[fid][2] if fid else None
res.append((wn, name, fid, callees))
return nm, fs, res
if sys.argv[1] == 'stats':
h, ins = load(sys.argv[2]); nm, fs, res = analyse(ins, sys.argv[3])
missing = [r[0] for r in res if r[2] is None]; inl = [r[0] for r in res if r[2] and not r[3]]
multi = [r[0] for r in res if r[3] and len(r[3]) != 1]
callee = collections.Counter(r[3][0] for r in res if r[3] and len(r[3]) == 1)
wrong = [(r[0], nm.get(r[3][0])) for r in res if r[3] and len(r[3]) == 1 and nm.get(r[3][0]) != r[1]]
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(1 for c in callee.values() if c > 1)))
print('callee OpName != source name: %d %s' % (len(wrong), wrong[:4]))
calls = sum(len(f[2]) for f in fs.values()); print('total OpFunctionCall %d; non-wrapper functions %d' % (calls, len(fs) - len(res) + len(missing)))
print('sample', [(r[0], nm.get(r[3][0]) if r[3] else None) for r in res[:3]], 'missing sample', missing[:3], 'inlined sample', inl[:3])
sys.exit(0)
_, src, dst, tsv, mode = sys.argv[1:6]
h, ins = load(src); nm, fs, res = analyse(ins, tsv)
drop_k = set(); drop_ids = set(); setname = {}; retarget = {}
for wn, name, fid, callees in res:
if fid is None or not callees or len(callees) != 1: sys.exit('refuse: wrapper %s is not one call' % wn)
a, b, _, ids = fs[fid]; drop_k.update(range(a, b + 1)); drop_ids |= ids | {fid}
setname[callees[0]] = name; retarget[fid] = callees[0]
drop_ids |= set(retarget)
outi = []; named = set()
for k, x in enumerate(ins):
if k in drop_k: continue
o = op(x)
if o == 17 and x[1] not in (1, 5): continue
if o == 10: continue
if o == 5 and x[1] in setname: x = [0] + [x[1]] + s_enc(setname[x[1]]); x[0] = (len(x) << 16) | 5; named.add(x[1])
elif o in (5, 6) and x[1] in drop_ids: continue
if o == 71 and x[1] in drop_ids:
if mode == 'export' and x[1] in retarget and x[2] == 41: x = [x[0], retarget[x[1]]] + x[2:]
else: continue
outi.append(x)
for fid, name in setname.items():
if fid not in named: outi.insert(next(i for i, x in enumerate(outi) if op(x) == 5), [((2 + len(s_enc(name))) << 16) | 5, fid] + s_enc(name))
w = h + [v for x in outi for v in x]; open(dst, 'wb').write(struct.pack('<%dI' % len(w), *w))
print('edit %s: dropped %d wrapper functions, renamed %d callees, kept %d instructions' % (mode, len(res), len(setname), len(outi)))

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Imagination probe: build-only export wrappers -> dxc lib_6_3 -spirv -> SPIR-V edit -> SPIRV-Cross ES. /src = CultLib.
set -u; P=/tmp/p; W=/tmp/w; mkdir -p $W/tools; cd $W
step() { echo; echo "=== $*"; }
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 \
mesa-vulkan-drivers libvulkan-dev glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
curl -sL --fail -o dxc.tgz https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.9.2607/linux_dxc_2026_07_29.x86_x64.tar.gz
echo "dxc sha ok: $(sha256sum dxc.tgz | cut -c1-64 | grep -c 55665c87824051ed4774ff3280a79ccbbb7d39243b9736ca5e98222134112d54)"
mkdir -p tools/dxc && tar xzf dxc.tgz -C tools/dxc; DXC=$(find tools/dxc -name dxc -type f | head -1)
git clone -q --depth 1 --branch vulkan-sdk-1.4.363.0 https://github.com/KhronosGroup/SPIRV-Cross.git tools/sc
cmake -S tools/sc -B tools/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF -DSPIRV_CROSS_CLI=ON >/dev/null && cmake --build tools/scb --target spirv-cross -j4 >/dev/null 2>&1; SC=tools/scb/spirv-cross; ls $SC >/dev/null && echo "spirv-cross built"
SH=/src/packages/cultmath/shaders
step "1 wrappers"
python3 $P/wrapgen.py $SH shw; head -3 shw/cm_wrappers.hlsl | cut -c1-160; grep -c '' shw/wrappers.tsv
echo "source defs per overloaded name: $(cut -f2 shw/wrappers.tsv | sort | uniq -c | awk '$1>1{n++; d+=$1} END{print n" names, "d" defs"}')"
step "2 dxc lib_6_3 on source plus wrappers"
for v in default Od; do f=""; [ $v = Od ] && f="-Od"
$DXC -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 $f -I shw shw/cm_wrappers.hlsl -Fo w.$v.spv 2>&1 | head -3; echo "[$v] dxc exit ${PIPESTATUS[0]}"
[ -s w.$v.spv ] && python3 $P/spvedit.py stats w.$v.spv shw/wrappers.tsv; done
V=default; python3 $P/spvedit.py stats w.default.spv shw/wrappers.tsv | grep -q 'inlined (no call) 0,' || V=Od; echo "using variant $V"
step "3 SPIR-V edit, validate, SPIRV-Cross ES 310"
for m in drop export; do python3 $P/spvedit.py edit w.$V.spv e.$m.spv shw/wrappers.tsv $m
spirv-val --target-env spv1.5 e.$m.spv 2>&1 | head -2; echo "[$m] val exit ${PIPESTATUS[0]}"
$SC e.$m.spv --es --version 310 --output e.$m.glsl 2>&1 | head -2; echo "[$m] spirv-cross exit ${PIPESTATUS[0]}, lines $(wc -l < e.$m.glsl 2>/dev/null)"
spirv-dis e.$m.spv | grep -E 'OpCapability|OpExtension' | tr -s ' ' | sort | uniq -c | tr '\n' ' '; echo; done
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; do G=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"
sed -E '/^#version/d; /^#ifdef SPIRV_CROSS/d; /^#endif/d; /^layout\(local_size/d' $G | awk 'BEGIN{s=0} /^void main\(\)/{s=1} s==0{print} s==1&&/^\}/{s=0}' > lib.body.glsl
{ printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl; printf 'out highp vec4 c;\nvoid main() { vec3 p = gl_FragCoord.xyz; float a = cultmath_lerp(1.0, 2.0, p.x); vec3 b = cultmath_lerp(p, p.yzx, vec3(0.5)); vec4 s = cultmath_saturate(vec4(p, a)); c = vec4(b, cultmath_saturate(a)) + s + vec4(cultmath_snoise(p)); }\n'; } > es300.frag
glslangValidator es300.frag > es300.txt 2>&1; echo "glslang ES 3.00 fragment: exit $? $(grep -m3 ERROR es300.txt | tr '\n' ' ' | cut -c1-300)"
sed -i 's/cultmath_saturate(a))/cultmath_saturate(ivec2(1)))/' es300.frag; glslangValidator es300.frag > neg.txt 2>&1; echo "negative (no ivec2 overload) refused: exit $? $(grep -m1 ERROR neg.txt | cut -c1-160)"
step "4 split, constants, capability strip"
python3 $P/split.py $SH $G $W/split 2>&1 | head -6
for combo in "core" "core cycles" "core phacelle" "core cycles phacelle"; do f=$W/combo.frag; { printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; for c in $combo; do cat $W/split/$c.glsl; done; printf 'out highp vec4 c;\nvoid main() { c = vec4(cultmath_snoise(gl_FragCoord.xyz)) + vec4(cultmath_lerp(gl_FragCoord.xyz, vec3(1.0), vec3(0.5)), 1.0); }\n'; } > $f; glslangValidator $f > $W/c.txt 2>&1; echo "glslang [$combo]: exit $? $(grep -m2 ERROR $W/c.txt | tr '\n' ' ' | cut -c1-200)"; done
echo "static const lines: $(grep -hcE '^static const' $SH/*.hlsl | awk '{s+=$1}END{print s}'); of form 'static const float CULTMATH_X = lit;': $(grep -hcE '^static const float CULTMATH_\w+ = [-0-9.eEfx]+;' $SH/*.hlsl | awk '{s+=$1}END{print s}'); CULTMATH_ in output $(grep -c 'CULTMATH_' $G); in committed glsl $(cat $SH/*.glsl | grep -cE '^(const )?\w* ?float CULTMATH_')"
DXC_PATH=$DXC bash $P/lava.sh
echo PROBE-DONE

View file

@ -0,0 +1,22 @@
#!/usr/bin/env python3
"""Probe: build-only export wrappers. One exported, uniquely named wrapper per top-level cultmath_ definition
(texture-taking ones skipped). Source is copied unchanged. usage: wrapgen.py <shaders> <outdir>
Writes <outdir>/cm_wrappers.hlsl and <outdir>/wrappers.tsv (wrapper, name, file, params)."""
import os, re, sys, shutil
sh, out = sys.argv[1:3]; shutil.copytree(sh, out, dirs_exist_ok=True)
SIG = re.compile(r'(?m)^(\w+)\s+(cultmath_\w+)\s*\(([^)]*)\)')
files = ['CultMath.hlsl', 'CultMath.Interval.hlsl', 'CultMath.Affine.hlsl', 'CultMath.Cycles.hlsl', 'CultMath.Phacelle.hlsl']
w = ['#include "CultMath.hlsl"']; rows = []; k = 0
for fn in files:
for m in SIG.finditer(open(os.path.join(sh, fn)).read()):
ret, name, ps = m.group(1), m.group(2), m.group(3)
if re.search(r'Texture|Sampler', ps): continue
params = [p.strip() for p in ps.split(',') if p.strip()]
args = [re.split(r'\s+', p)[-1] for p in params]
wn = 'cmw%03d_%s' % (k, name); k += 1
call = '%s(%s)' % (name, ', '.join(args))
w.append('export %s %s(%s) { %s%s; }' % (ret, wn, ', '.join(params), '' if ret == 'void' else 'return ', call))
rows.append('\t'.join([wn, name, fn, ', '.join(params)]))
open(os.path.join(out, 'cm_wrappers.hlsl'), 'w').write('\n'.join(w) + '\n')
open(os.path.join(out, 'wrappers.tsv'), 'w').write('\n'.join(rows) + '\n')
print('wrappers', k)

View file

@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Probe job, runs in ubuntu:24.04 with /src = CultLib at the probed revision. Q1 library path, Q2 lavapipe runs.
set -u
P=/tmp/p; W=/tmp/w; mkdir -p $W/fam $W/tools; cd $W
step() { echo; echo "=== $*"; }
step apt
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 \
mesa-vulkan-drivers libvulkan-dev vulkan-tools glslang-tools spirv-tools >/dev/null 2>&1; echo "apt exit $?"
dpkg-query -W -f='${Package} ${Version}\n' mesa-vulkan-drivers glslang-tools spirv-tools libvulkan-dev
step dxc
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
step spirv-cross
git clone -q --depth 1 https://github.com/KhronosGroup/SPIRV-Cross.git tools/sc && git -C tools/sc log -1 --format='SPIRV-Cross %H %cs' && \
cmake -S tools/sc -B tools/scb -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF >/dev/null && cmake --build tools/scb --target spirv-cross -j4 >/dev/null 2>&1; SC=tools/scb/spirv-cross; ls -la $SC | cut -c1-120
SH=/src/packages/cultmath/shaders
step "Q1a dxc library SPIR-V, no wrapper (lib_6_6, vulkan1.2)"
$DXC -T lib_6_6 -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH $SH/CultMath.hlsl -Fo lib.spv 2>&1 | head -8; echo "dxc exit ${PIPESTATUS[0]}"
[ -f lib.spv ] && { spirv-dis lib.spv -o lib.spvasm; echo "functions: $(grep -c ' = OpFunction ' lib.spvasm) exports: $(grep -c 'LinkageAttributes.*Export' lib.spvasm) entrypoints: $(grep -c OpEntryPoint lib.spvasm)"; }
step "Q1b dxc library with 'export' added to every top-level cultmath_ definition"
python3 - "$SH" <<'PY'
import re,sys,os,shutil
src=sys.argv[1]; dst='/tmp/w/shx'; shutil.copytree(src,dst,dirs_exist_ok=True)
n=0
for fn in os.listdir(dst):
if not fn.endswith('.hlsl'): continue
t=open(os.path.join(dst,fn)).read()
t2,k=re.subn(r'(?m)^(?!static|export)(\w+(?:\dx\d)?\s+cultmath_\w+\s*\()', r'export \1', t); n+=k
open(os.path.join(dst,fn),'w').write(t2)
print('export keywords added', n)
PY
for v in "universal1.5" "vulkan1.2 -Vd" "vulkan1.3 -Vd"; do echo "--- variant: -fspv-target-env=$v"; rm -f libx.spv
$DXC -T lib_6_6 -HV 2021 -spirv -fspv-target-env=$v -I shx shx/CultMath.hlsl -Fo libx.spv 2>&1 | head -4; echo "dxc exit ${PIPESTATUS[0]}"
[ -s libx.spv ] && { spirv-dis libx.spv -o libx.spvasm; echo "functions: $(grep -c ' = OpFunction ' libx.spvasm) exports: $(grep -c 'LinkageAttributes.*Export' libx.spvasm) OpName cultmath_: $(grep -c 'OpName.*"cultmath_' libx.spvasm) Fma: $(grep -c ' Fma ' libx.spvasm) NoContraction: $(grep -c NoContraction libx.spvasm)"; spirv-val --target-env vulkan1.2 libx.spv 2>&1 | head -2; break; }
done
LIB=libx.spv
step "Q1c SPIRV-Cross ES output of $LIB"
for v in 300 310; do $SC $LIB --es --version $v --output lib$v.glsl 2>&1 | head -3; echo "spirv-cross --es --version $v exit ${PIPESTATUS[0]}"; done
for v in 300 310; do [ -s lib$v.glsl ] || continue
echo "lib$v.glsl: $(wc -l < lib$v.glsl) lines; defs named cultmath_: $(grep -cE '^\w+ cultmath_\w+\(' lib$v.glsl); distinct names: $(grep -oE '^\w+ cultmath_\w+\(' lib$v.glsl | sed -E 's/^\w+ //' | sort -u | wc -l); suffixed _N names: $(grep -oE '^\w+ cultmath_\w+_[0-9]+\(' lib$v.glsl | wc -l); _NNN temporaries: $(grep -oE '\b_[0-9]+\b' lib$v.glsl | wc -l); precision lines: $(grep -c precision lib$v.glsl); main: $(grep -c 'void main' lib$v.glsl)"; done
echo "committed CultMath.glsl: $(wc -l < $SH/CultMath.glsl) lines; defs $(grep -cE '^\w+ cultmath_\w+\(' $SH/CultMath.glsl); distinct names $(grep -oE '^\w+ cultmath_\w+\(' $SH/CultMath.glsl | sed -E 's/^\w+ //' | sort -u | wc -l)"
F=$(ls lib310.glsl lib300.glsl 2>/dev/null | head -1); [ -n "$F" ] && { echo "--- head of $F"; head -12 $F; echo "--- cultmath_snoise(vec3) as emitted (first 25 lines)"; awk '/^float cultmath_snoise\(/{p=1} p{print; if(++n>=25) exit}' $F; }
for v in 300 310; do [ -s lib$v.glsl ] && { glslangValidator -S comp lib$v.glsl >gv$v.txt 2>&1; echo "glslang -S comp lib$v.glsl exit $? : $(head -3 gv$v.txt | tr '\n' ' ' | cut -c1-300)"; }; done
step "Q2 lavapipe runner"
gcc -O2 -o vkrun $P/vkrun.c -lvulkan && echo "vkrun built"
python3 $P/fam.py gen /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam
cat $SH/CultMath.glsl $SH/CultMath.Cycles.glsl $SH/CultMath.Phacelle.glsl > all.glsl
python3 - <<'PY'
import json; print('families', len(json.load(open('/tmp/w/fam/manifest.json'))))
PY
ok_h=0; ok_g=0; fail=0; first=1
for h in fam/*.hlsl; do t=${h%.hlsl}; 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'])")
if $DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I $SH $h -Fo $t.h.spv >$t.h.log 2>&1; then
if [ $first = 1 ]; then VKRUN_INFO=1 ./vkrun $t.h.spv $t.in $t.hlsl.out $n; first=0; else ./vkrun $t.h.spv $t.in $t.hlsl.out $n; fi && ok_h=$((ok_h+1)) || echo "run fail hlsl $tag"
else fail=$((fail+1)); echo "dxc fail $tag: $(head -2 $t.h.log | tr '\n' ' ' | cut -c1-200)"; fi
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat all.glsl $t.body.glsl; } > $t.comp
if glslangValidator -V --target-env vulkan1.2 -o $t.g.spv $t.comp >$t.g.log 2>&1; then ./vkrun $t.g.spv $t.in $t.glsl.out $n && ok_g=$((ok_g+1)) || echo "run fail glsl $tag"
else echo "glslang fail $tag: $(grep -m2 ERROR $t.g.log | tr '\n' ' ' | cut -c1-200)"; fi
done
echo "ran: hlsl->dxc->lavapipe $ok_h, committed glsl->glslang->lavapipe $ok_g, dxc compile failures $fail"
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))")"
python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam hlsl
python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam glsl
python3 - <<'PY2'
import os,filecmp
fs=sorted(f[:-9] for f in os.listdir('/tmp/w/fam') if f.endswith('.hlsl.out'))
same=sum(filecmp.cmp('/tmp/w/fam/%s.hlsl.out'%f,'/tmp/w/fam/%s.glsl.out'%f,shallow=False) for f in fs if os.path.exists('/tmp/w/fam/%s.glsl.out'%f))
print('lavapipe: HLSL-path and GLSL-path outputs bit-identical in',same,'of',len(fs),'families')
PY2
t0=$(date +%s); for h in fam/*.h.spv; do t=${h%.h.spv}; n=$(python3 -c "import json;m={x['tag']:x for x in json.load(open('fam/manifest.json'))}['$(basename $t)'];print(m['n'],m['nout'])"); ./vkrun $h $t.in $t.rerun.out $n; cmp -s $t.rerun.out $t.hlsl.out || echo "NONDETERMINISTIC $(basename $t)"; done; echo "rerun of 105 SPIR-V modules on lavapipe: $(( $(date +%s) - t0 ))s, determinism check done"
step "Q2b SPIRV-Cross ES 3.10 library through glslang on lavapipe (only if Q1 produced one)"
if [ -s lib310.glsl ]; then sed -E '/^#version/d; /^void main\(\)/,/^\}/d' lib310.glsl > lib.body.glsl; okl=0
for b in fam/*.body.glsl; do t=${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'])")
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n'; cat lib.body.glsl $b; } > $t.l.comp
glslangValidator -V --target-env vulkan1.2 -o $t.l.spv $t.l.comp >$t.l.log 2>&1 && ./vkrun $t.l.spv $t.in $t.lib.out $n && okl=$((okl+1))
done; echo "lib path ran $okl"; grep -h -m1 ERROR fam/*.l.log | sort | uniq -c | sort -rn | head -5
python3 $P/fam.py cmp /src/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json fam lib | head -15
fi
echo PROBE-DONE

View file

@ -0,0 +1,11 @@
import re,sys
src,dst=sys.argv[1:3]
L=open(src).read().split('\n')
ids=[]
for l in L:
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
if m: ids.append(m.group(1))
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
open(dst,'w').write('\n'.join(L))
print(src,len(ids))

View file

@ -0,0 +1,19 @@
set -e
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
P=packages/cultmath/tools
python3 - <<'PY'
p='packages/cultmath/tools/device/run-lavapipe.sh'
s=open(p).read()
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
open(p,'w').write(s)
PY
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
W=/tmp/device/work
for t in f103 f104; do for r in h g; do
spirv-dis --raw-id $W/$t.$r.spv -o /tmp/$t.$r.dis
echo "== $t $r: NoContraction $(grep -c NoContraction /tmp/$t.$r.dis) Fma $(grep -c ' Fma ' /tmp/$t.$r.dis) FMul $(grep -c OpFMul /tmp/$t.$r.dis) FAdd $(grep -c OpFAdd /tmp/$t.$r.dis) FSub $(grep -c OpFSub /tmp/$t.$r.dis) lines $(wc -l </tmp/$t.$r.dis)"
done; done
mkdir -p /tmp/dump; cp /tmp/f10?.?.dis /tmp/dump/; cp $W/f103.hlsl $W/f103.comp /tmp/dump/; cp /tmp/glslout/*.glsl /tmp/dump/
echo DUMP-BEGIN; tar czf - -C /tmp dump | base64 -w0; echo; echo DUMP-END

View file

@ -0,0 +1,46 @@
set -e
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
cat >/tmp/nc.py <<'PYEOF'
import re,sys
src,dst=sys.argv[1:3]
L=open(src).read().split('\n')
ids=[]
for l in L:
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
if m: ids.append(m.group(1))
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
open(dst,'w').write('\n'.join(L))
print(src,len(ids))
PYEOF
P=packages/cultmath/tools
python3 - <<'PY'
p='packages/cultmath/tools/device/run-lavapipe.sh'
s=open(p).read()
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
open(p,'w').write(s)
PY
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
W=/tmp/device/work; R=/tmp/device
mkdir -p $R/nc/lavapipe-hlsl $R/nc/lavapipe-glsl
while IFS=$'\t' read -r tag _ _ n _ wout; do
for r in h g; do
spirv-dis --raw-id $W/$tag.$r.spv -o /tmp/$tag.$r.dis
python3 /tmp/nc.py /tmp/$tag.$r.dis /tmp/$tag.$r.nc.dis
spirv-as --target-env vulkan1.2 /tmp/$tag.$r.nc.dis -o $W/$tag.$r.nc.spv
spirv-val --target-env vulkan1.2 $W/$tag.$r.nc.spv
d=hlsl; [ $r = g ] && d=glsl
$R/vkrun $W/$tag.$r.nc.spv $W/$tag.in $R/nc/lavapipe-$d/$tag.out $n $wout >/dev/null
done
echo "$tag nc: hlsl==glsl $(cmp -s $R/nc/lavapipe-hlsl/$tag.out $R/nc/lavapipe-glsl/$tag.out && echo YES || echo NO); nc-hlsl==plain-hlsl $(cmp -s $R/nc/lavapipe-hlsl/$tag.out $R/results/lavapipe-hlsl/$tag.out && echo YES || echo NO); nc-glsl==plain-glsl $(cmp -s $R/nc/lavapipe-glsl/$tag.out $R/results/lavapipe-glsl/$tag.out && echo YES || echo NO)"
done <$W/manifest.txt
cp /tmp/device/vkrun /tmp/ 2>/dev/null || true
export CULTMATH_DEVICE_NAME=$(sort -u $R/device.txt | head -1 | sed 's/^device=//') CULTMATH_DEVICE_IDENTITY=x CULTMATH_WRITE_DEVICE_BOUNDS=1
for v in results nc; do
export CULTMATH_DEVICE_RESULTS=$R/$v
dotnet test packages/cultmath/tests/CultMath.Tests --no-build --filter "FullyQualifiedName~GlslMirrorTests.DeviceResultsHoldFixture" >/tmp/judge.$v.log 2>&1 || true
echo "== judge $v"; grep -E "^(lavapipe-)" packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt | grep -E " 10[34] " | cut -f1,2,5,6 || tail -5 /tmp/judge.$v.log
tail -3 /tmp/judge.$v.log
done

View file

@ -0,0 +1,53 @@
set -e
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
cat >/tmp/nc.py <<'PYEOF'
import re,sys
src,dst=sys.argv[1:3]
L=open(src).read().split('\n')
ids=[]
for l in L:
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
if m: ids.append(m.group(1))
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
open(dst,'w').write('\n'.join(L))
print(src,len(ids))
PYEOF
P=packages/cultmath/tools
python3 - <<'PY'
p='packages/cultmath/tools/device/run-lavapipe.sh'
s=open(p).read()
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
open(p,'w').write(s)
PY
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
W=/tmp/device/work; R=/tmp/device
DXC=$(find $R/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
CM=packages/cultmath
cat >/tmp/cnt.py <<'PYEOF'
import sys,struct
a=open(sys.argv[1],'rb').read();b=open(sys.argv[2],'rb').read()
n=len(a)//4;x=struct.unpack('<%dI'%n,a);y=struct.unpack('<%dI'%n,b)
d=[i for i in range(n) if x[i]!=y[i]]
print(len(d),'of',n,'cols',sorted(set(i%6 for i in d)))
PYEOF
run() { # name spv tag n wout
$R/vkrun $2 $W/$3.in /tmp/o.$1.$3 $4 $5 >/dev/null; }
OPT="--target-env=spv1.5 --eliminate-local-single-store --eliminate-dead-code-aggressive"
while IFS=$' ' read -r tag _ _ n _ wout; do
for r in h g; do spirv-dis --raw-id $W/$tag.$r.spv -o /tmp/$tag.$r.dis; python3 /tmp/nc.py /tmp/$tag.$r.dis /tmp/$tag.$r.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/$tag.$r.nc.dis -o $W/$tag.$r.nc.spv; done
# h variants
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Od -I $CM/shaders $W/$tag.hlsl -Fo $W/$tag.hOd.spv >/dev/null 2>&1
spirv-opt $OPT $W/$tag.hOd.spv -o $W/$tag.hOdO.spv
for v in hOd hOdO; do spirv-dis --raw-id $W/$tag.$v.spv -o /tmp/$tag.$v.dis; python3 /tmp/nc.py /tmp/$tag.$v.dis /tmp/$tag.$v.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/$tag.$v.nc.dis -o $W/$tag.$v.nc.spv; done
for v in h g hOd hOdO; do run $v $W/$tag.$v.spv $tag $n $wout; run $v.nc $W/$tag.$v.nc.spv $tag $n $wout; done
for v in h hOd hOdO; do
echo "$tag $v vs g: $(python3 /tmp/cnt.py /tmp/o.$v.$tag /tmp/o.g.$tag)"
echo "$tag $v.nc vs g.nc: $(python3 /tmp/cnt.py /tmp/o.$v.nc.$tag /tmp/o.g.nc.$tag)"
done
echo "$tag h.nc vs h: $(python3 /tmp/cnt.py /tmp/o.h.nc.$tag /tmp/o.h.$tag)"
echo "$tag g.nc vs g: $(python3 /tmp/cnt.py /tmp/o.g.nc.$tag /tmp/o.g.$tag)"
echo "$tag hOd.nc vs hOd: $(python3 /tmp/cnt.py /tmp/o.hOd.nc.$tag /tmp/o.hOd.$tag)"
done <$W/manifest.txt

View file

@ -0,0 +1,53 @@
set -e
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
cat >/tmp/nc.py <<'PYEOF'
import re,sys
src,dst=sys.argv[1:3]
L=open(src).read().split('\n')
ids=[]
for l in L:
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
if m: ids.append(m.group(1))
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
open(dst,'w').write('\n'.join(L))
print(src,len(ids))
PYEOF
P=packages/cultmath/tools
python3 - <<'PY'
p='packages/cultmath/tools/device/run-lavapipe.sh'
s=open(p).read()
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\\1\\2/g" "$W"/work/*.comp\n')
open(p,'w').write(s)
PY
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
set +e
W=/tmp/device/work; R=/tmp/device
DXC=$(find $R/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
CM=packages/cultmath
for d in plain ncod gis; do mkdir -p $R/$d/lavapipe-hlsl $R/$d/lavapipe-glsl; done
while IFS=$'\t' read -r tag _ _ n _ wout; do
spirv-dis --raw-id $W/$tag.g.spv -o /tmp/g.dis; python3 /tmp/nc.py /tmp/g.dis /tmp/g.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/g.nc.dis -o /tmp/g.nc.spv
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Od -I $CM/shaders $W/$tag.hlsl -Fo /tmp/hOd.spv >/dev/null 2>&1
spirv-dis --raw-id /tmp/hOd.spv -o /tmp/hOd.dis; python3 /tmp/nc.py /tmp/hOd.dis /tmp/hOd.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/hOd.nc.dis -o /tmp/hOd.nc.spv
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Gis -I $CM/shaders $W/$tag.hlsl -Fo /tmp/hGis.spv >/dev/null 2>&1
spirv-dis --raw-id /tmp/hGis.spv -o /tmp/hGis.dis; python3 /tmp/nc.py /tmp/hGis.dis /tmp/hGis.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/hGis.nc.dis -o /tmp/hGis.nc.spv
cp $R/results/lavapipe-hlsl/$tag.out $R/plain/lavapipe-hlsl/; cp $R/results/lavapipe-glsl/$tag.out $R/plain/lavapipe-glsl/
$R/vkrun /tmp/hOd.nc.spv $W/$tag.in $R/ncod/lavapipe-hlsl/$tag.out $n $wout >/dev/null
$R/vkrun /tmp/g.nc.spv $W/$tag.in $R/ncod/lavapipe-glsl/$tag.out $n $wout >/dev/null
$R/vkrun /tmp/hGis.nc.spv $W/$tag.in $R/gis/lavapipe-hlsl/$tag.out $n $wout >/dev/null
cp $R/ncod/lavapipe-glsl/$tag.out $R/gis/lavapipe-glsl/
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
export CULTMATH_DEVICE_WORK=$W CULTMATH_DEVICE_NAME=$(sort -u $R/device.txt | head -1 | sed 's/^device=//') CULTMATH_DEVICE_IDENTITY=x CULTMATH_WRITE_DEVICE_BOUNDS=1
B=packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt
for v in plain ncod gis; do
export CULTMATH_DEVICE_RESULTS=$R/$v
dotnet test packages/cultmath/tests/CultMath.Tests --no-build --filter "FullyQualifiedName~GlslMirrorTests.DeviceResultsHoldFixture" >/tmp/judge.$v.log 2>&1 || true
grep -aE "Error Message" -A8 /tmp/judge.$v.log | head -14; ls -la $B; cp $B /tmp/bounds.$v; echo "judge $v: $(tail -3 /tmp/judge.$v.log | tr '\n' ' ')"
done
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

View file

@ -0,0 +1,87 @@
// Probe runner: one compute dispatch, binding 0 = input uints, binding 1 = output uints.
// usage: vkrun <spv> <in.bin> <out.bin> <ncases> <out_words_per_case> [entry]
#include <vulkan/vulkan.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CK(x) do { VkResult r_ = (x); if (r_ != VK_SUCCESS) { fprintf(stderr, "FAIL %s = %d\n", #x, r_); exit(2); } } while (0)
static void *slurp(const char *p, size_t *n) {
FILE *f = fopen(p, "rb"); if (!f) { perror(p); exit(2); }
fseek(f, 0, SEEK_END); *n = ftell(f); fseek(f, 0, SEEK_SET);
void *b = malloc(*n ? *n : 4); fread(b, 1, *n, f); fclose(f); return b;
}
int main(int argc, char **argv) {
if (argc < 6) { fprintf(stderr, "usage\n"); return 2; }
size_t spvn, inn; void *spv = slurp(argv[1], &spvn); void *in = slurp(argv[2], &inn);
unsigned ncases = atoi(argv[4]), ow = atoi(argv[5]); const char *entry = argc > 6 ? argv[6] : "main";
size_t outn = (size_t)ncases * ow * 4;
VkApplicationInfo ai = { VK_STRUCTURE_TYPE_APPLICATION_INFO, 0, "probe", 1, "probe", 1, VK_API_VERSION_1_2 };
VkInstanceCreateInfo ici = { VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, 0, 0, &ai };
VkInstance inst; CK(vkCreateInstance(&ici, 0, &inst));
uint32_t np = 1; VkPhysicalDevice pd; vkEnumeratePhysicalDevices(inst, &np, &pd); if (!np) { fprintf(stderr, "no device\n"); return 2; }
VkPhysicalDeviceFloatControlsProperties fc = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT_CONTROLS_PROPERTIES };
VkPhysicalDeviceProperties2 pp = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &fc };
vkGetPhysicalDeviceProperties2(pd, &pp);
if (getenv("VKRUN_INFO"))
printf("device=%s api=%u.%u driver=%u f32: denormPreserve=%u ftz=%u rte=%u rtz=%u signedZeroInfNan=%u\n",
pp.properties.deviceName, VK_VERSION_MAJOR(pp.properties.apiVersion), VK_VERSION_MINOR(pp.properties.apiVersion),
pp.properties.driverVersion, fc.shaderDenormPreserveFloat32, fc.shaderDenormFlushToZeroFloat32,
fc.shaderRoundingModeRTEFloat32, fc.shaderRoundingModeRTZFloat32, fc.shaderSignedZeroInfNanPreserveFloat32);
float prio = 1; VkDeviceQueueCreateInfo qci = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, 0, 0, 0, 1, &prio };
VkDeviceCreateInfo dci = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, 0, 0, 1, &qci };
VkDevice dev; CK(vkCreateDevice(pd, &dci, 0, &dev)); VkQueue q; vkGetDeviceQueue(dev, 0, 0, &q);
VkPhysicalDeviceMemoryProperties mp; vkGetPhysicalDeviceMemoryProperties(pd, &mp);
VkBuffer buf[2]; VkDeviceMemory mem[2]; size_t sz[2] = { inn, outn }; void *map[2];
for (int i = 0; i < 2; i++) {
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 0, 0, sz[i], VK_BUFFER_USAGE_STORAGE_BUFFER_BIT };
CK(vkCreateBuffer(dev, &bci, 0, &buf[i])); VkMemoryRequirements mr; vkGetBufferMemoryRequirements(dev, buf[i], &mr);
uint32_t t = 0; VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
for (; t < mp.memoryTypeCount; t++) if ((mr.memoryTypeBits & (1u << t)) && (mp.memoryTypes[t].propertyFlags & want) == want) break;
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, 0, mr.size, t };
CK(vkAllocateMemory(dev, &mai, 0, &mem[i])); CK(vkBindBufferMemory(dev, buf[i], mem[i], 0));
CK(vkMapMemory(dev, mem[i], 0, sz[i], 0, &map[i]));
}
memcpy(map[0], in, inn); memset(map[1], 0xEE, outn);
VkDescriptorSetLayoutBinding b[2] = { { 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT },
{ 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT } };
VkDescriptorSetLayoutCreateInfo lci = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, 0, 0, 2, b };
VkDescriptorSetLayout dsl; CK(vkCreateDescriptorSetLayout(dev, &lci, 0, &dsl));
VkPipelineLayoutCreateInfo plci = { VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, 0, 0, 1, &dsl };
VkPipelineLayout pl; CK(vkCreatePipelineLayout(dev, &plci, 0, &pl));
VkShaderModuleCreateInfo smci = { VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, 0, 0, spvn, spv };
VkShaderModule sm; CK(vkCreateShaderModule(dev, &smci, 0, &sm));
VkComputePipelineCreateInfo cpci = { VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, 0, 0,
{ VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, 0, 0, VK_SHADER_STAGE_COMPUTE_BIT, sm, entry }, pl };
VkPipeline pipe; CK(vkCreateComputePipelines(dev, 0, 1, &cpci, 0, &pipe));
VkDescriptorPoolSize ps = { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2 };
VkDescriptorPoolCreateInfo dpci = { VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, 0, 0, 1, 1, &ps };
VkDescriptorPool dp; CK(vkCreateDescriptorPool(dev, &dpci, 0, &dp));
VkDescriptorSetAllocateInfo dsai = { VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, 0, dp, 1, &dsl };
VkDescriptorSet ds; CK(vkAllocateDescriptorSets(dev, &dsai, &ds));
VkDescriptorBufferInfo dbi[2] = { { buf[0], 0, VK_WHOLE_SIZE }, { buf[1], 0, VK_WHOLE_SIZE } };
VkWriteDescriptorSet w[2];
for (int i = 0; i < 2; i++) { VkWriteDescriptorSet x = { VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, 0, ds, i, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, &dbi[i] }; w[i] = x; }
vkUpdateDescriptorSets(dev, 2, w, 0, 0);
VkCommandPoolCreateInfo cpi = { VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, 0, 0, 0 };
VkCommandPool cp; CK(vkCreateCommandPool(dev, &cpi, 0, &cp));
VkCommandBufferAllocateInfo cbai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, 0, cp, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1 };
VkCommandBuffer cb; CK(vkAllocateCommandBuffers(dev, &cbai, &cb));
VkCommandBufferBeginInfo cbbi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
CK(vkBeginCommandBuffer(cb, &cbbi));
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pl, 0, 1, &ds, 0, 0);
vkCmdDispatch(cb, (ncases + 63) / 64, 1, 1);
CK(vkEndCommandBuffer(cb));
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO, 0, 0, 0, 0, 1, &cb };
CK(vkQueueSubmit(q, 1, &si, 0)); CK(vkQueueWaitIdle(q));
FILE *f = fopen(argv[3], "wb"); fwrite(map[1], 1, outn, f); fclose(f);
return 0;
}

View file

@ -0,0 +1,178 @@
# 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) |
| Slang | Slang (accepts HLSL-like syntax) | SPIR-V, HLSL, GLSL desktop (`-profile glsl_N`), WGSL (WIP), CPU, others | not examined | active |
| Unity HLSLcc | DX bytecode (DXBC) | GLSL 3.2+, GLSL ES 2.0+ ("OpenGL ES 3.0+" used for Unity), Metal, Vulkan GLSL | n/a (bytecode) | `pushed_at` 2024-07-16, not archived, 30 open issues, README "used to generate all Unity shaders for OpenGL, OpenGL ES 3.0+, Metal, Vulkan"; supports `min16float`, infers sampler precision |
| naga | WGSL, SPIR-V, GLSL 440+ | SPIR-V, WGSL, GLSL ES 300+, HLSL SM5+, MSL | n/a | active (wgpu) |
| Tint | WGSL (SPIR-V reader optional) | SPIR-V, HLSL, MSL, GLSL (per 2021 snapshot) | n/a | unconfirmed |
- [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).

View file

@ -0,0 +1,221 @@
# Payload nesting limits in comparable libraries
Fork: `cultlib-gaps:question:payload-depth-limit` (options reference-500, shared-256, shared-128).
Raised in `cultlib-gaps:cut_spec:cut-union-bounds-cs.r1`. Gathered 2026-10-11 by Eyes
(`eyes-depth-limit-prior-art`). Facts only; no recommendation. Source files were fetched
from each project's default branch on 2026-10-11 (raw.githubusercontent.com); line numbers
refer to those fetches and move with the branch. "(inferred)" marks a reading of code or
text that the source does not state.
Terms: "read" = decode/parse; "write" = encode/serialize; "skipped" = content a decoder
walks past without materialising (ignored fields, unknown fields).
## 1. Summary table
| Library | Default | Counts | Read / write | Source |
|---|---|---|---|---|
| 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 C# | 100 (`DefaultRecursionLimit`) | CodedInputStream recursion | read | CodedInputStream.cs:57,120-147 |
| protobuf Java | 100 (`defaultRecursionLimit`) | CodedInputStream recursion | read | CodedInputStream.java:45,52,521 |
| 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 |
| simdjson | 1024 (`DEFAULT_MAX_DEPTH`) | parser allocation parameter | read | common_defs.h:49; dom/parser.h:596,599 |
## 2. Detail by library
### 2.1 MessagePack-CSharp (the C# reference's decoder)
- `UntrustedData` and `TrustedData` both set `MaximumObjectGraphDepth = 500`
(MessagePackSecurity.cs:31, 41; property initialiser 92). `TrustedData` was lowered to 500
by commit 6b025eab "Limit stack depth to 500 even for trusted data" (2024-10-29) and
set from `UntrustedData` by 7c087905 (2024-11-28), per the commit history of
MessagePackSecurity.cs (api.github.com/repos/MessagePack-CSharp/MessagePack-CSharp/commits?path=src/MessagePack/MessagePackSecurity.cs).
The commit messages give no rationale for the value 500.
- `DepthStep(ref MessagePackReader)` throws `InsufficientExecutionStackException` when
`reader.Depth >= MaximumObjectGraphDepth`, else increments (MessagePackSecurity.cs:266-273).
The doc comment says callers must decrement after leaving the edge (:254-260).
- Counting is by contract with formatters, not by the reader: README.md says `DepthStep` and
`Depth--` "should surround the bulk of any `IMessagePackFormatter<T>.Deserialize` method"
(README lines ~1161-1175). So content no formatter reads is not counted (inferred from the
README contract; the CultLib measurement in the question confirms it: an extra trailing slot
of 100000 nested arrays is accepted).
- Write: no depth check in the serializer or writer in the code fetched (MessagePackReader.cs
holds `Depth` at :53-75,133; the writer was not fetched). The CultLib question reports a
3001-deep payload written and then refused on read.
- Advisory GHSA-7q36-4xx7-xcxf / CVE-2020-5234 (published 2020-01-31): "Untrusted data can lead
to DoS attack due to hash collisions and stack overflow in MessagePack". Affected < 1.9.11 and
>= 2.0.0 < 2.1.90. Mitigation is opting into `UntrustedData`; the advisory text does not state
the depth value.
- Advisory GHSA-cj9g-3mj2-g8vv / CVE-2026-48512 (published 2026-06-25), fixed in 2.5.301 and
3.1.7: the JSON conversion helpers (`ConvertFromJson`, `TinyJsonReader.ReadNextToken`, the
ext-100 branch of `ConvertToJson`) recursed without consulting `MaximumObjectGraphDepth`; the
advisory says `UntrustedData` "does not fully mitigate" them. This is the class of defect
"a depth limit exists but one recursive path skips it".
### 2.2 rmp-serde (the Rust runtime's decoder)
- `Deserializer` has `depth: u16`, initialised to 1024 (decode.rs:181, 213, 294);
`set_max_depth` clamps to `u16::MAX` (:309-310).
- `depth_count!` decrements, returns `DepthLimitExceeded` when the counter reaches 0, runs the
body, increments (decode.rs:51-62). Used for arrays (:566), maps (:583) and ext newtype (:616).
So the usable nesting is 1023 levels (inferred from decrement-then-test-zero).
- `ignored_any` is forwarded to `deserialize_any` (decode.rs:~749-753 `forward_to_deserialize_any!`),
which goes through the same counted paths: skipped containers are counted.
- Write: `Error::DepthLimitExceeded` exists in encode.rs (:37, 49, 64) but a grep finds no site
that constructs it (only definition, `source`, `Display`). The writer has no depth check
(inferred from the three matches).
- Crate version at fetch: 1.3.1 (Cargo.toml). No RUSTSEC advisory about depth was found for
rmp-serde, rmpv or ciborium in a web search (search-engine result, not a database query).
### 2.3 serde_json
- `remaining_depth: u8 = 128` (de.rs:34, 63); `check_recursion!` decrements and errors with
`RecursionLimitExceeded` when it reaches 0 (de.rs:1372-1387), used at the `[`/`{` branches of
`deserialize_any` and the typed deserialize methods (de.rs:1434, 1445, 1750, 1801, 1838, 1849,
1882). `disable_recursion_limit()` exists behind the `unbounded_depth` feature (de.rs:197-216,
1357-1370). A third-party analysis (search result, not fetched) states the deepest enterable
container is level 127.
- `deserialize_ignored_any` calls `ignore_value()` (de.rs:1910-1916). `ignore_value` (:1102-~1215)
is an explicit loop with a heap `scratch` stack of frames and contains no `check_recursion!`
(the `check_recursion!` lines in the file are all at 1372+). Reading: ignored values in
serde_json are walked iteratively and are not depth-limited (inferred from the code; not
tested). This differs from the question's statement that "serde_json ... count ignored values
the same way".
- Serializer: grep of ser.rs for depth/recurs returns no match.
- Related: serde-json-wasm GHSA-rr69-rxr6-8qwf / RUSTSEC-2024-0012 (2024-02-09), fixed in 1.0.1
and 0.5.2: "This was fixed by adding a check for recursion depth"; the advisory text gives no value.
rustc_serialize RUSTSEC-2022-0004: parsing had no stack-depth limit and "can cause a stack
overflow, which aborts the process".
- serde_stacker (crates.io description) grows the stack dynamically and is documented as paired
with `disable_recursion_limit()`; its README warns that Drop, Debug and Display on deep values
can still overflow. A forum thread reports dropping a deeply nested `Value` (~10 million
levels) overflowing after a successful parse.
### 2.4 msgpack-python
- C unpacker: `MSGPACK_EMBED_STACK_SIZE` 1024 (unpack.h:19); `start_container` returns -3 when
`top >= 1024` before it checks for an empty container (unpack_template.h:117-127), and -3 is
raised as `StackError` (_unpacker.pyx:208-211, 521-524). The docstring says "Raises `StackError`
when `packed` contains too nested" (_unpacker.pyx:158, 313). Empty containers at depth 1025 are
therefore also refused (inferred from check order). `skip()` runs the same state machine
(fallback.py:599-606 shows `skip` raising `StackError`; the C path is the same unpack function,
inferred).
- Pure-Python fallback: depth is Python's recursion limit; `RecursionError` is converted to
`StackError` (fallback.py:586-606).
- Packer: `DEFAULT_RECURSE_LIMIT = 1024`; `_pack` raises `ValueError("recursion limit exceeded.")`
when `nest_limit < 0` and decrements per container (_packer.pyx:42, 259-262; fallback.py:66, 699-711, 801-804).
### 2.5 JavaScript
- @msgpack/msgpack Decoder: iterative, with a heap `StackPool` (Decoder.ts:111-190, loop :640-643);
no depth option appears in the decoder (grep of "depth" returns only stack-pool lines). The
README option table lists `maxDepth` default 100 (README.md:119) and Encoder.ts uses it
(`DEFAULT_MAX_DEPTH = 100`, :9; `doEncode` throws `Too deep objects in depth N` when
`depth > maxDepth`, :173). So the library is asymmetric: encode bounded, decode unbounded in
depth (limited by memory). The CultLib question states TS "parse the whole payload"; consistent.
- msgpackr: grep of unpack.js and pack.js finds no maxDepth option or depth counter.
- msgpack5 (another npm package) GHSA-24ch-f2g6-9hhh (2026-10-08): "The decoder has no
nesting-depth limit for arrays and maps ... exhaust the JavaScript call stack"; fix in 6.1.0
limits depth to 100 by default, option `maxDepth`, error "Maximum decode depth exceeded".
- msgpack-java: CVE record at vulncheck.com ("through 0.9.12", GitHub issue #1015):
`MessageUnpacker.unpackValue()` "recursively deserializes arrays and maps without nesting
depth limits", causing StackOverflowError. Fix value not retrieved.
### 2.6 .NET JSON
- System.Text.Json: `JsonReaderOptions.DefaultMaxDepth = 64` (JsonReaderOptions.cs:13);
`JsonWriterOptions.DefaultMaxDepth = 1000` (JsonWriterOptions.cs:18);
`JsonSerializerOptions.DefaultMaxDepth` = the reader's 64, with a comment that it is the
minimum of the reader and writer defaults (JsonSerializerOptions.cs:27-28). So the serializer
limit applies to both directions at 64; the bare writer at 1000.
- Newtonsoft.Json: `JsonReader` constructor sets `_maxDepth = 64` (JsonReader.cs:332); exceeding
throws `JsonReaderException` "The reader's MaxDepth of {0} has been exceeded" (:356-359).
`JsonWriter.cs` has no MaxDepth match. Advisory GHSA-5crp-9r3c-p9vr / CVE-2024-21907
(published 2022-06-22, affects < 13.0.1): "Insecure Defaults ... improper handling of
expressions with high nesting level that lead to StackOverFlow exception or high CPU and RAM
usage". Advisory figures: deserialising needs >10 million nesting (9.5 MB of `{a:{a:`) for >10 s
of CPU; serialising (`SerializeObject`, `JObject.ToString`) overflowed the stack at about
20,000 levels. The advisory's own mitigation example sets `MaxDepth = 128`; the 13.0.1 default
is 64 (the changelog line was not retrieved; the 64 is read from the code above).
### 2.7 Protobuf
- Default recursion limit 100 in C++ (coded_stream.h:630 comment "100 by default"), C#
(CodedInputStream.cs:57), Java (CodedInputStream.java:45) and upb (constants.h:11). C# rejects a
non-positive limit (cs:142-144); upb stores it in a 16-bit field (`upb_DecodeOptions_MaxDepth`).
- Skipped content: GHSA-735f-pc8j-v9w8 / CVE-2024-7254 (published 2024-09-19): in protobuf Java
(full and lite), Kotlin and JRuby, "Parsing nested groups as unknown fields with
DiscardUnknownFieldsParser or Java Protobuf Lite parser, or against Protobuf map fields,
creates unbounded recursions". Fixed in 3.25.5, 4.27.5, 4.28.2. The advisory implies unknown
(skipped) content had escaped the recursion limit until the fix.
- No write-side limit was looked up.
### 2.8 CBOR, BSON, Go, simdjson
- ciborium: `recurse: 256` in `from_reader` and `deserializer_from_reader`
(de/mod.rs:812, 854); `from_reader_with_recursion_limit` documents "Set a high recursion limit at
your own risk (of stack exhaustion)!" (:819-822, 859-862). `recurse()` errors
`RecursionLimitExceeded` at 0 (:64-74). No depth/recurs match in ser/mod.rs.
- cbor2: advisory GHSA-3c37-wwvx-h642 / CVE-2026-26209 (published 2026-03-23, fixed 5.9.0):
"uncontrolled recursion" in both the pure-Python and C decoders; PoC is 1000 nested `0x81`
bytes raising `RecursionError`. The C decoder used `Py_EnterRecursiveCall` (Python's
interpreter limit). Fix: `max_depth` parameter, default 400, in 5.9.0 (versionhistory.rst:172-173).
6.0.0 (2026-04-28): decoder rewritten in Rust, iterative, depth "limited only by the available
memory" (versionhistory.rst:138-140).
- BSON: MongoDB documents "no more than 100 levels of nesting for BSON documents. Each object or
array adds a level." The page does not state read vs write.
- Go encoding/json: `maxNestingDepth = 10000` "limits the max nesting depth to prevent stack
overflow" (scanner.go:146-148); `pushParseState` errors "exceeded max depth" when the parse stack
exceeds it (:181-185). `checkValid` scans the whole input before decode (scanner.go:30), so every
container, including ones the target type ignores, is counted (inferred from `checkValid` being
the first step of `Unmarshal`; not read in decode.go). Encoder: `ptrLevel` over 1000 starts
cycle detection, which is not a depth limit (encode.go:298-305).
- simdjson: `constexpr size_t DEFAULT_MAX_DEPTH = 1024` (common_defs.h:49); `allocate(capacity,
max_depth = DEFAULT_MAX_DEPTH)`. (The PHP binding defaults to 512 per the PHP manual.)
## 3. Read-side vs write-side, and skipped content
Enforces on write: rmp-serde (variant defined, no construction found: no); msgpack-python Packer
(1024); @msgpack/msgpack Encoder (100); System.Text.Json (serializer 64, writer 1000); Go
encoding/json (no depth limit; cycle detection only); MessagePack-CSharp (no); serde_json (no);
ciborium (no); Newtonsoft (no match in JsonWriter.cs).
Counts skipped/ignored content: rmp-serde (yes, via `ignored_any` to `deserialize_any`);
msgpack-python C unpacker (yes, `skip()` shares the stack); Go (scanner pre-pass, inferred);
serde_json (apparently no: `ignore_value` is iterative and uncounted, inferred);
MessagePack-CSharp (no: counting is by formatter contract); protobuf Java (unknown groups were
unbounded before CVE-2024-7254).
## 4. Stack use per nesting level
No library documents bytes of stack per nesting level. What exists:
- ciborium: "Set a high recursion limit at your own risk (of stack exhaustion)!"
- Newtonsoft advisory: serialisation overflowed the default stack at about 20,000 levels
(a count, not bytes; thread stack size not stated in the advisory).
- cbor2 advisory PoC: 1000 nested arrays reached the Python recursion limit.
- Rhai docs (search snippet, a different system): default call depth 64 in release, 8 in debug,
an illustration that debug frames are larger.
- CultLib's own measurement (from the question, not a prior-art source): Rust debug on 2 MiB
aborts at about 408 containers, which is about 5 KiB per level (2 MiB / 408, inferred);
release carries 1200+; Python near 1000; TS near 4000.
- serde_json and rmp-serde pick 128 and 1024 respectively; neither source documents how the value
was derived. Arrow Parquet Variant (docs.rs search result) says it picked MAX_NESTING_DEPTH 128
"to match serde_json" because validation recurses.
## 5. Not retrieved
The rationale for 500 in MessagePack-CSharp; the Newtonsoft 13.0.1 release note; the fix depth
chosen by msgpack-java and serde-json-wasm; protobuf write-side behaviour; decode.go's use of
`checkValid`; MessagePack-CSharp writer code; simdjson's depth-error semantics.

View file

@ -0,0 +1,22 @@
# Python MessagePack default payload: history and probes (2026-10-11)
Serves `cultlib-gaps:ruling:py-msgpack-default` and `cultlib-gaps:cut_spec:cut-py-msgpack-default.r1`.
## How a JSON payload default got into .cc
1. Ghostlight `2531b42` (2026-05-06), the first consumer: "The JSONL store is deliberate bootstrap plumbing", msgpack optional.
2. CultLib `b214daab` (2026-05-24) squashed in `python/cultcache-py` from GameCult/cultcache-py@9b4bbb8a with
`define_document_type` already defaulting to `_json_payload_encoder`; msgpack was an optional extra and the README
promised a standard-library-only bootstrap. JSON was the default because it was the only codec without msgpack.
3. CultLib `a1ef58cb` (2026-06-13) made msgpack a hard dependency but kept the JSON default and the optional-import
guards, so the default outlived its reason.
4. Earlier history is unreachable (GameCult/cultcache-py no longer exists; CultLib holds only the squash). voidbot
`search_history` found no discussion of the choice.
## Probes
- `probe1.py`, `probe2.py`: no runtime records a payload's format in the store, so a reader cannot tell JSON from
MessagePack except by trying; a one-byte JSON payload `0`-`9` is valid MessagePack (48-57); a MessagePack map with
a non-str/bytes key packs but `unpackb` refuses it.
- `rmpprobe/`: `rmp_serde::from_slice` reads a JSON object's bytes as `Number(123)`, which is why Brokkr's daemon must
read exactly one MessagePack value with a trailing-bytes check.

View file

@ -0,0 +1,47 @@
import json, sys, msgpack
print(sys.version.split()[0], msgpack.version)
def pk(v): return msgpack.packb(v, use_bin_type=True)
def up(b): return msgpack.unpackb(b, raw=False)
def jd(v): return json.dumps(v, ensure_ascii=False, sort_keys=True).encode()
cases = {"dict": {"schema":"x","n":1}, "list":[1,2], "str":"hi", "digit":5, "neg":-3, "true":True, "null":None, "float":1.5, "bigdigit":12}
print("== msgpack.unpackb on JSON payloads")
for k,v in cases.items():
b = jd(v)
try: r = up(b); print(k, b, "->ACCEPTED", repr(r))
except Exception as e: print(k, b, "->", type(e).__module__+"."+type(e).__name__, isinstance(e, ValueError), str(e)[:60])
print("== json.loads on msgpack payloads")
for k,v in cases.items():
b = pk(v)
try: r = json.loads(b.decode("utf-8")); print(k, b, "->ACCEPTED", repr(r))
except Exception as e: print(k, b, "->", type(e).__name__, str(e)[:50])
print("== semantic differences, value -> json roundtrip vs msgpack roundtrip")
diffs = {"intkeys": {1:"a"}, "tuple": (1,2), "bytes": b"ab", "bigint": 2**70, "nan": float("nan"), "boolkey": {True:1}, "nested_intkey": {"a":{2:3}}, "set": {1,2}, "order": {"b":1,"a":2}}
for k,v in diffs.items():
out=[]
try: out.append("json:"+repr(json.loads(jd(v))))
except Exception as e: out.append("json:"+type(e).__name__)
try: b=pk(v)
except Exception as e: out.append("pack:"+type(e).__name__+":"+str(e)[:40]); print(k, out); continue
try: out.append("mp:"+repr(up(b)))
except Exception as e: out.append("unpack:"+type(e).__name__+":"+str(e)[:60])
try: out.append("mp_nonstrict:"+repr(msgpack.unpackb(b, raw=False, strict_map_key=False)))
except Exception as e: out.append("ns:"+type(e).__name__)
print(k, out)
print("== custom node classes")
import collections, types
class OnlyIter:
def __iter__(self): return iter([1])
for k,v in {"deque":collections.deque([1]), "mappingproxy":types.MappingProxyType({"a":1}), "userlist":collections.UserList([1]), "onlyiter":OnlyIter(), "frozenset":frozenset([1]), "userdict":collections.UserDict({"a":1})}.items():
r=[]
for name,f in (("pack",pk),("json",jd)):
try: f(v); r.append(name+":ok")
except Exception as e: r.append(name+":"+type(e).__name__)
print(k, r)
print("== JSON read recursion threshold")
for d in (900, 1000, 3000, 5000, 10000, 100000):
try: json.loads("["*d + "]"*d); print(d, "ok")
except RecursionError: print(d, "RecursionError")
print("== msgpack read deep")
for d in (300, 1000, 2000, 100000):
try: up(b"\x91"*d + b"\xc0"); print(d,"ok")
except Exception as e: print(d, type(e).__module__, type(e).__name__)

View file

@ -0,0 +1,33 @@
# Brokkr-shaped flow at CultLib 9d7b5842: a store written by today's JSON default, then read by a MessagePack-default definition.
import sys, tempfile, os, json, msgpack, timeit, dataclasses
sys.path.insert(0, r"F:/Projects/CultLib/packages/cultcache-py/src")
import cultcache_py as cc
from cultcache_py.stores import SingleFileMessagePackBackingStore
d = tempfile.mkdtemp(); path = os.path.join(d, "blender-editor.ccmp")
def open_cache(defn):
c = cc.CultCache.builder().register_document_type(defn).add_generic_store(SingleFileMessagePackBackingStore(path)).build()
c.pull_all_backing_stores(); return c
old = cc.define_document_type("brokkr.sync.var.v0")
value = {"schema": "brokkr.sync.var.v0", "syncVarId": "v1", "enabled": True, "kind": "transform", "updatedAt": "2026-10-11T00:00:00Z"}
c = open_cache(old); c.put(old, "sync/vars/v1", value)
raw = msgpack.unpackb(open(path, "rb").read(), raw=False)
print("store header", raw[0], "catalog entry", raw[1][0][:5] if raw[1] else None)
print("record", raw[2][0][:3], "payload", raw[2][0][3][:40])
# MessagePack-default reader, today's _messagepack pair standing in for the proposed default
new = cc.define_document_type("brokkr.sync.var.v0", payload_encoder=lambda t: msgpack.packb(t, use_bin_type=True), payload_decoder=lambda b: msgpack.unpackb(b, raw=False))
try: open_cache(new); print("new reader: ACCEPTED")
except Exception as e: print("new reader refused:", type(e).__module__, type(e).__name__, str(e)[:80])
# opt-in JSON pair reads it
print("json opt-in reads:", open_cache(old).get(old, "sync/vars/v1") == value)
# migration: read with JSON definition, write with MessagePack definition, same key
v = open_cache(old).get(old, "sync/vars/v1"); c2 = cc.CultCache.builder().register_document_type(new).add_generic_store(SingleFileMessagePackBackingStore(path)).build()
c2._state # not pulled: pull would fail on the JSON doc
try:
c2.put(new, "sync/vars/v1", v); print("put without pull ok")
except Exception as e: print("put without pull:", type(e).__name__, str(e)[:80])
print("after migration new reader reads:", open_cache(new).get(new, "sync/vars/v1") == value)
# size and speed, one Brokkr-shaped document
jb = json.dumps(value, ensure_ascii=False, sort_keys=True).encode(); mb = msgpack.packb(value, use_bin_type=True)
print("bytes json", len(jb), "msgpack", len(mb))
print("encode us json %.2f msgpack %.2f" % (timeit.timeit(lambda: json.dumps(value, ensure_ascii=False, sort_keys=True).encode(), number=100000)*10, timeit.timeit(lambda: msgpack.packb(value, use_bin_type=True), number=100000)*10))
print("decode us json %.2f msgpack %.2f" % (timeit.timeit(lambda: json.loads(jb.decode()), number=100000)*10, timeit.timeit(lambda: msgpack.unpackb(mb, raw=False), number=100000)*10))

View file

@ -0,0 +1,8 @@
[package]
name = "rmpprobe"
version = "0.1.0"
edition = "2021"
[dependencies]
rmp-serde = "=1.3.1"
serde_json = "1"
serde = "1"

View file

@ -0,0 +1,21 @@
fn main() {
let json_obj = br#"{"enabled":true,"kind":"transform","schema":"brokkr.sync.var.v0"}"#;
let r: Result<serde_json::Value, _> = rmp_serde::from_slice(json_obj);
println!("rmp_serde on a JSON object payload: {:?}", r);
let mp = rmp_serde::to_vec(&serde_json::json!({"schema":"x","n":1})).unwrap();
println!("rmp_serde::to_vec of a json! object, first byte {:#04x} len {}", mp[0], mp.len());
let j: Result<serde_json::Map<String, serde_json::Value>, _> = serde_json::from_slice(&mp);
println!("serde_json on that MessagePack map: {:?}", j.map(|_| ()).map_err(|e| e.to_string()));
let mut de = rmp_serde::Deserializer::from_read_ref(&json_obj[..]);
let v: serde_json::Value = serde::Deserialize::deserialize(&mut de).unwrap();
println!("one_value(json obj) = {:?}; one_value(mp) = {:?}", one_value(json_obj), one_value(&mp).map(|_| ()));
println!("consumed {} of {} bytes, value {:?}", de.get_ref().len() as usize, json_obj.len(), v);
}
#[allow(dead_code)]
fn one_value(bytes: &[u8]) -> Result<serde_json::Value, String> {
let mut de = rmp_serde::Deserializer::new(std::io::Cursor::new(bytes));
let v: serde_json::Value = serde::Deserialize::deserialize(&mut de).map_err(|e| e.to_string())?;
let used = de.get_ref().position() as usize;
if used != bytes.len() { return Err(format!("{} trailing bytes after the first MessagePack value", bytes.len() - used)); }
Ok(v)
}

Some files were not shown because too many files have changed in this diff Show more