Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
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 portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Waiting to run
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Waiting to run
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
683 changed files with 183047 additions and 4539 deletions

3
.gitattributes vendored
View file

@ -38,3 +38,6 @@ packages/cultmesh-ts/native/MANIFEST.txt text eol=lf
# 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:

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; scripts/release-after-suite.mjs refuses the release 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*"
@ -74,10 +85,11 @@ jobs:
# 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.
# 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')
run: node scripts/release-after-suite.mjs npm-publish packages/cultcache-ts "$GITHUB_REF"
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 }}
@ -145,10 +157,11 @@ jobs:
# 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.
# 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: node scripts/release-after-suite.mjs python-build packages/${{ matrix.package }} "$GITHUB_REF"
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

@ -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.

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,14 @@
# CultMath tapes: interval-pruned field evaluation on CPU and GPU
Status: **parked**, 2026-10-02. The operator wants Asura working first. This
note records the direction so it can be picked up without re-deriving it. It is
not a cut map: no Imagination pass has run, and nothing here is ruled except
that the campaign exists and waits.
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
@ -103,12 +108,18 @@ source.
- Asura does not need this campaign. Its field is noise-dominated, and its
`A_max` shell and band-limiting already give the cheap culling.
## Open before unparking
## Decided since the unpark, and what stays open
- Run the (a) spike first, or go straight to (c)?
- Pick the interval representation (plain intervals or affine arithmetic) and
the noise bound strategy.
- Choose the bytecode register model for the HLSL interpreter, given GPU
register pressure.
- Decide whether the brush-grammar station project lives in Aetheria or in
its own repo.
- 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

@ -52,6 +52,160 @@ Other Body facts that shape the map:
AetheriaEve `AetheriaRuntimeCatalogStore` (directory-store pages).
- **Kotlin reads no store files.** It sees CultNet raw snapshots only.
### Body at 2026-10-12 (Imagination refresh, `imagination-variants-refresh`)
Probes run 2026-10-12 from `F:\Projects\CultLib-c2afix` with git only (no builds), after
`git fetch origin`. Scratch worktrees under the session scratchpad were removed afterwards.
- **C2a head.** `hands/variants-c2a` = `origin/hands/variants-c2a` = `de401056`, worktree clean.
`main` = `origin/main` = `b3bd8dd5`. Merge base `f789c8d1` (2026-09-30). C2a is 207 commits
ahead, main 453. `15ccdbb9`, the base of the five 2026-10-02 specs, is a C2a commit, not a
main commit; C2a has never merged main.
- **Overlap.** `git diff --name-only` from the merge base: C2a touches 141 files, main 285, 33 in
common. In C#, main changed `CultCache.cs` (+18 -4: union guard, internal `Clock` seam),
`CultDocumentMessagePackSerialization.cs` (+9 -3: nesting bound 256), and the persistence and
schema-compatibility contracts (+7, +21). Everything else in common is under `packages/`:
cultmesh-ts `src/index.ts` (main +372 -433, C2a +189 -356), cultcache-rs `lib.rs` (main +276,
C2a +761 -171), cultcache-ts store and cache, cultnet-ts replication.
- **Merge probe.** `git merge --no-ff --no-commit b3bd8dd5` onto `de401056` in a throwaway
worktree: 16 files conflict, 32 hunks. `CultCache.cs` has one: C2a made `MintStoredAt` an
`internal static` (the store's `StoredAtOver` calls it), main made it an instance method over a
new `Clock` seam for its union golden. The cultmesh-ts conflicts carry a semantic one: main
added payload-parse acceptance to the store document (an `accepts` predicate that decodes the
payload, and the test "store document handles choose compatible foreign schema payloads"),
which C2a's retire-schema-alias rule forbids. Main still carries `preserveIncomingSchemaId`
and `resolve_document_and_schema_id_for_raw_record`, which C2a deleted.
- **The stash.** `stash@{0}` in `CultLib-c2afix`, "h3 WIP 2026-10-03: commit-time check-only
keys, stopped for quota; unverified", based on `de401056`, 2 files, +186 -47
(`CultCache.cs`, `DirectoryMessagePackBackingStore.cs`). It is a draft of the operator's
at-commit ruling (`unheld-removal-refused-at-commit`): `CultCommitRequest.Checks`, staged
checks on each writable store judged under the lock by `RefuseUndeclaredOnDisk`, both stores'
unlocked removal reads deleted, and `ExpectUnchanged` comparing foreign records at their read
versions (`CultForeignRecord.StoredVersion`). No tests, no contract text. Read with
`git stash show -p`; not applied or dropped.
- **The High finding is already fixed.** `rawbytes-in-place-edit-lost` (content-version s2) was
fixed by h3 (`2a3773a4`, range head `a935d15c`: `RawBytes` deleted, the store owns a file's
bytes) and Soul's verdict `cut-c2a-content-version.s3` holds it, with `rawbytes-key-drop-unpinned`
retired by deletion and the five s1 pins (`cache-wrote-version-unpinned`,
`expect-on-removed-unpinned`, `no-condition-hash-unpinned`, `staleness-own-upsert-unpinned`,
`unstored-same-tick-unpinned`) caught at head. `git grep RawBytes de401056 -- src tests`: no
match. Those findings were never closed in the mind.
### Body at 2026-10-15: element ids and the MessagePack walk (Modeling, from the Imagination and Soul passes)
Probes ran on Yggdrasil (`mcr.microsoft.com/dotnet/sdk:10.0`, MessagePack 3.1.7 in every csproj) as probe
tests that were never committed: at `6afa8ed9` for I1 to I4, at `494ad9ab` for r4 and r5, at `6e70626b` for r6.
Logs and sources are under `F:\Projects\eureka-handoff-aux-2026-10-15\scratchpad-self-2026-10-14b-eureka\`
(`imag\`, `imag-r4\`, `imag-r5\`; Soul's `soul-c2a-id-holes-s5\` is in that session's scratchpad). The typed
record of each fact is in the mind: `variants:cut_spec:cut-c2a-element-id-holes.r5` and `.r6` (`first`),
verdicts `cut-c2a-element-id-holes.s5` and `.s6`, spec `cut-c2a-ids-header.r1` (`first`). This section keeps
what was run and what it showed.
**What an id is on disk and in memory.**
- An id is an ordinary keyed string in the payload. A list element whose `[CultElementId]` member holds
`a1b2c3d4e5f6` and one whose plain `Tag` holds the same value encode to identical bytes
(`92A1649192AC61316232633364346535663607`; `c2a-ids-header` r1 probe, base `2774a7fe`). No reader can tell
from a record's bytes that it holds ids; only the store header says so.
- `StubArm`, a union arm that is not an object type, is never minted (`imag\imag1.log`, I1): for a document of
`StubArm` and `Unit8`, `Plan` counts 1, `StubArm.Id` stays `''`, `Unit8.Id` is minted, `Inspect` reports 0
problems.
- Resolver over-mark (I2): a consumer `[assembly: CultCacheFormatterResolver]` claims a keyed arm with a
formatter that writes only `N`. The cache mints an id in memory, `Holds` is true and the header is stamped
`v3`, but the payload (`92A263319192009104`) carries no id, and a reopen mints a different one (`8f5e929888ca`
in memory, `a1ea2b683d98` after reload). The header claims ids the disk lacks.
- `[MessagePackFormatter]` on an element type is honoured (I3): the attribute formatter wrote
`92A26131919104`, no id, and `Inspect` reported 0 problems at that base. Registration refuses it from r5 on
(`declares [MessagePackFormatter]`).
- Python store (I4): `py-store/leaf-record.cc` (header `v1`, ids `000000000021/22`) loads in C# with
`HoldsIds = true`, `IdsInMemoryOnly = false`. A C# write that adds an unrelated record copies it forward,
the header stays `v1`, and the ids survive reload. cultcache-py, -ts and -rs have no declaration of which
member is an id, so they keep a `v3` they found and never decide one.
- Header narrowing (`c2a-ids-header` r1): per-record `StoredHoldsIds`, set from the type that decoded a
record, is falsified by the identical-bytes probe above. The durable header owns "these stored bytes hold
ids" at store granularity. A write that copies any record forward keeps a marked header; only a write that
encodes every record decides by content. `StoredHoldsIds` and `KnownIdFree` are deleted.
- Object list versus value list (r4, `imag-r4\probe1.log`, `probe2.log`): ids sit on object-list elements only
(ruling `q7-object-list-elements-only`). `Dictionary`, `ConcurrentDictionary`, `SortedDictionary`,
`IReadOnlyDictionary`, and `ILookup` of keyed arms round-trip with the id at
the element's own slot (e.g. `ConcDict` `81A16B92AC30313233343536373839616200`: key, then `[id, 0]`). A list
or map of scalars has no element to carry one and never enters the id path.
**How MessagePack decides what it writes** (r5 `imag-r5\probe1.log`, `probe3.log`; Soul s5 `job2.log`).
- Member discovery, `MessagePack.Internal.ObjectSerializationInfo.CreateOrNull(Type, bool forceStringKey,
bool contractless, bool allowPrivate)`, is internal, and so is `StandardResolver.Resolvers`. The public
surface allows one test: `StandardResolver`'s formatter for `T` is reference-equal to the formatter one
public singleton hands out for `T`, each caching one instance per type. `DynamicObjectResolver.Instance`
matches keyed classes and structs, `DynamicUnionResolver.Instance` `[Union]` types,
`DynamicGenericResolver.Instance` List, arrays, maps, KeyValuePair, tuples, Lazy, Memory, ArraySegment and
Nullable, and `ImmutableCollectionResolver.Instance` (namespace `MessagePack.ImmutableCollection`)
`ImmutableArray` and `ImmutableList`. Asking `DynamicObjectResolver` for a collection type throws
`TypeInitializationException`, so a family probe swallows exceptions.
- Kind facts (`probe3`): a `[MessagePackObject]` class implementing only `IEnumerable<T>` or
`IReadOnlyList<T>` (`KBag`, `KNode`, `KRo`) is written by key (object resolver). One that is also
`ICollection<T>`, a `List<T>` subclass or a `Dictionary` subclass (`KC2`, `KColl`, `KList2<T>`, `KDict`) is
written as a collection (generic resolver) and its `[Key]` members are dropped.
- Member facts (`probe3`; `ap` is `AllowPrivate`, `R` written, `W` read back). A member is written when it
has an integer `[Key]`, no `[IgnoreMember]`, and is a public field or a property with a public getter. It is
read back when it is a public non-readonly field, a property with a public setter (`init` counts), or a
`[SerializationConstructor]` parameter. At `ap=False`: `PG` (private getter) `R=False W=True`, `PS`
(private setter) `R=True W=False`, `GetOnly` `R=True W=False`, `InitOnly` both. `IdPriv`
(`Id { get; private set; }`) is written and not read back (wire `91AC62...`, back `<null>`), and `IdCtor` is
`W=False`. At `ap=True` every one of these, `priv` included, is `R=True W=True`, except `GetOnly`
(`W=False`).
- AllowPrivate attribute form (Soul s5 `SoulS5Probe.C`, `ApTrueArm`): on a type declared
`[MessagePackObject(AllowPrivate = true)]`, `StandardResolver` writes private `[Key]` members and a
private-setter id round-trips. The oracle test calls `CreateOrNull(type, false, false, false)` and reports
none of them, so the walk's member set misses that form.
- Collection written as object (r6 `probe2.log`): a `[DataContract]` class implementing only `IEnumerable<T>`
over an id-carrying arm (`DcSeq`) is a collection in the walk, yet `StandardResolver`'s formatter is
`DynamicObjectResolver`'s (wire `91 91 91 92 AC..`, written as an object). A `[DataContract]` `List<T>`
subclass (`DcColl`) is written by the generic resolver, and the object resolver hands out a different
instance for it. For `DcBad` (two members on one `Order`) `StandardResolver` uses the generic resolver, and
Soul s5 measured both `DcColl` and `DcBad` passing the door.
- Geometry (r6 `probe2.log`, `probe3.log`): Geometry has a direct MessagePack 3.1.7 `PackageReference`, so the
source generator runs, and its assembly carries `[GeneratedAssemblyMessagePackResolverAttribute(
typeof(GeneratedMessagePackResolver), 3, 1)]`. For `CultGeometryDomainNode`, `DomainDocument` and
`FeatureClaim`, `StandardResolver` returns `GeneratedMessagePackResolver`'s formatter and every public
singleton returns a different instance or null. For `CultGeometryDomainNode[]` and `FeatureClaim[]` it is
the generated resolver's `ArrayFormatter`, not `DynamicGenericResolver`'s. No Geometry id-path type matches
a family singleton. Caching.Tests has no generator (analyzers do not flow through a `ProjectReference`), so
its keyed types match `DynamicObjectResolver`.
- Generated assemblies (Soul s5, fresh generator-enabled assembly `SoulGen`, `job2.log` GEN rows): a keyed
`List` or `Dictionary` subclass (`GKColl`, `GKDict`) is written by key, wire `["c",[7]]`, with its elements
dropped by MessagePack; the walk reads it by key too (Plan 0). A generated-assembly `[DataContract]`
enumerable (`GDcSeq`) is still written by `DynamicObjectResolver`, because the generator emits no
DataContract formatters.
- Null standard (Soul s6 finding `null-standard-untyped`): for a non-generic interface or abstract class that
enumerates arms, `StandardResolver` returns null instead of throwing, and the door neither refuses nor
types it.
- Wire pin (Soul s5, s6 `SoulS5Pin`): a deterministic document with 12 minted ids, encoded by the codec and
decoded independently with `MessagePackSerializer.ConvertToJson`, carries every id. The ids are
byte-identical at `494ad9ab` and `57fdf70c`, `0f8964b58a08` and `7eb4fb13d530` among them.
- Not measured: Unity/IL2CPP, where the package bundles its own `MessagePack.dll` (version not read) and the
dynamic resolvers cannot emit, and MessagePack versions other than 3.1.7.
**Whole-list holders at `57fdf70c`** (Imagination r2 for `c2a-id-closed-world`, 2026-10-16; probe tests in a
scratch commit `dc629459` that was never pushed, Yggdrasil `sdk:10.0`; logs `job1.log` and `job2.log` in the
Imagination scratchpad `imagination-c2a-closed-world-r2`).
- Shapes the walk does not own already behave as whole values. A keyed document holding
`ImmutableArray<PElem>`, `(List<PElem>, int)`, `KeyValuePair<string, List<PElem>>`, a non-enumerable
`[DataContract]` holder, or a string-keyed holder (`[Key("items")]`) gives `Inspect` problems 0, an empty
IdPath, `CanHoldIds` false, `Plan` 0 (random and deterministic), and `Holds` false with ids set.
- The marker and the copy check do not count them. Committed with ids `aaaaaaaaaaaa`/`bbbbbbbbbbbb`, the store
header stays `cultcache.store.v1`. A fresh cache that writes another record over that file (the copy-forward
check) keeps `v1`. Reload returns the ids exactly as written, and unset ids stay null after write and reload.
- Variants replace them whole, and that is the C2a resolver's behaviour for every list. A variant `Set` of the
`ImmutableArray` slot resolved to `[9:eeeeeeeeeeee]` with the base untouched, header `v2`, `HoldsIds` false.
A path step `[1:aaaaaaaaaaaa]` is refused: "this runtime resolves top-level Set overrides only". No C2a gap.
The gap belongs to C2b, in the cut sequence below.
- AllowPrivate is read from the most derived `[MessagePackObject]`. `ApDerived` (AllowPrivate over a base
without it) is written `[[],[],4,[]]`: base-chain privates and a private-getter property, with
`ObjectSerializationInfo` at `allowPrivate: true` = `[baseStash, own, N, Pg]`. `NoApDerived` (plain over an
AllowPrivate base) is written `[nil, 5]`. The walk read only `[N]` for both.
- A `[DataContract]` `List<T>` subclass is written by `NonGenericListFormatter`, and its record does not read
back ("System.Object[] is not of type PElem"), whatever the walk does. That is a MessagePack limit, not an
id fact.
## 2. Model page (step 0b)
| Kind | Named by | Over time | Decided by |
@ -60,16 +214,16 @@ Other Body facts that shape the map:
| Base reference | the base's key, in the record's variant slot | changed only by an explicit rebase; a base cannot be deleted while it has variants, unless the same batch rebases or removes them | author; the cache refuses a cycle, a missing base, a different concrete type, a global type |
| Override entry | `(op, path, id, value)`; op is Set, Insert or Remove | added by editing; removed by clearing; survives base edits while its path resolves | author; the cache refuses entries a base edit strands (Q2); an entry whose member left the type soft-drifts with a warning (Q4 c) |
| Member path | a sequence of `(slot, elementId)` steps; `slot` is the MessagePack `[Key]`; `elementId` is `""` unless the step enters an object-list element | a type change can invalidate it | the type's owner (slot authority); ids by the element's creator |
| Element id | a `[CultElementId]` string member on an object-list element type; `""` means unset | minted by the cache on write when unset; copied unchanged into variants; a moved element is a new element | cache mints it; it is never retargeted |
| Element id | a `[CultElementId]` string member on the element type of an object list in a shape the walk owns (arrays, `IEnumerable<T>`, `IDictionary`/`IReadOnlyDictionary` values, `[MessagePackObject]`/`[Union]` types with integer keys), one MessagePack writes and reads back (integer `[Key]`, no `[IgnoreMember]`; public, or any visibility under the most derived `AllowPrivate`); `""` means unset. **Exception (q3 amended 2026-10-16):** a shape the walk does not own is one whole value: `ImmutableArray`, `ArraySegment`, `Memory`, `KeyValuePair`, tuples, `Lazy`, unkeyed or `[DataContract]` classes, string-keyed types, a type or member with `[MessagePackFormatter]`, and a non-public member without `AllowPrivate`. Its elements carry no ids: a `[CultElementId]` inside is a plain string | minted by the cache on write when unset; copied unchanged into variants; a moved element is a new element. Inside a whole value: never minted, never checked, never counted for `v3` | cache mints it; it is never retargeted. `CultElementIds.Classify` alone decides owned versus whole |
| Resolved view | `CultStoredDocument.Document` for a variant key | recomputed inside the hold that admits the variant, or any base on its chain | the cache only; never persisted, never written back (Q1) |
| Index and name entries | as today, from the resolved view | a dependent's entries are dropped and re-added in the same `Apply` as its base change | the cache; a variant must override the `[CultName]` member (rule R6) |
| Watches | as today; each re-resolved dependent emits `Updated` with its own `Sequence` | published in the same hold as the base change | the cache |
| Compare-exchange | `(schemaId, storedAt)` of the stored delta (ruling 2) | a base change does not stamp its variants | the cache; the resolved view is not an identity |
| Persisted format version | store header `formatVersion` | `cultcache.store.v2` when the store holds at least one variant; `v1` otherwise (Q5) | the writing store, from content |
| Persisted format version | store header `formatVersion` | `cultcache.store.v2` when the store holds at least one variant; `v1` otherwise (Q5); a marked `v3` (`v5` directory) stays while a write copies any record forward, and only a write that encodes every record drops it by content | the writing store, from content; the durable header alone says the bytes hold element ids |
| Single-file store file | the path; one MessagePack array `[header, catalog, records]` | replaced whole and atomically; refused when unreadable; see "Store authorities" | each runtime's one reader decides what is a store; the writer derives the header from it |
| Store lock and staging files | `<file>.lock`; a unique temp beside the store | the lock is taken by C# and Rust writers (CultMesh included, through `ReplaceDurable`); staging is removed on a failed write | the store, never CultMesh or a consumer |
| Variant slot on the wire (CultNet) | **empty cell**: no CultNet message carries a delta today | C5 | ruling 4 decides deltas; the message shape is C5's design |
| Dictionary entries, scalar lists, arrays | **empty cell**: no stable identity exists | whole-member Set only (Q7) | Q7 |
| Dictionary entries, scalar lists, arrays, whole-list holders | **empty cell**: no stable identity exists | whole-member Set only (Q7; whole-list holders by q3 amended) | Q7, q3 amended |
| Template (non-item) bases | **empty cell**: parked by ruling 6 | none | not in these cuts |
## 3. Where the Body contradicts the target
@ -181,6 +335,43 @@ Dictionary entries, scalar lists and arrays are replaced whole.
- **Recommend yes.** Aetheria's cases (`Hardpoints` whole; `Behaviors[id]...Max`) fit. Keyed
dictionary addressing can be added later without a format change. Affects C2.
## Cut sequence (2026-10-15)
Landed since 2026-10-12: `c2a-declared-apply` (r3, Soul s5) and `main-into-c2a` (r2, Soul s2); the
reconcile step is done, `hands/variants-c2a` is at the reconciled head `6afa8ed9`. Element ids part 1,
`c2a-element-id-holes` (r6), is held by Soul s5/s6 on `hands/variants-c2a-element-id-holes`. Mapped
now: part 2, `c2a-id-closed-world` r2 (2026-10-16, under the amended q3), not blocked. Its open fork
`variants:question:c2a-whole-list-opt-in` holds under every option. Later cuts, one line each, mapped
only when Self is about to dispatch them:
1. **reconcile**: done at `6afa8ed9`.
2. **parity-ts**: TypeScript write set, one record per key, undeclared records carried and refused,
unheld removals judged at commit. Waits on element ids parts 1 and 2 merged into C2a.
3. **parity-rs**: Rust `push_all` and `put_prepared_batch` apply a write set, ownership by
declaration, the same commit-time checks. Waits as parity-ts; can run beside it.
4. **parity-py**: Python write-set apply. Waits as parity-ts, and on `cultlib-gaps` cut
`py-msgpack-default` landing on main (then a main merge into C2a first).
5. **write-set-vectors**: one cross-runtime manifest, read and acted on by all four runtimes;
carries follow-ups `c0-bad-canonical-in-manifest`, `legacy-vectors-all-runtimes`,
`directory-at-path-ts-py-pinned`. Waits on the three parity cuts.
6. **land-c2a**: final main merge, full verification, Soul, then `git merge --no-ff` onto main
with an empty diff against the verified head; no tag. Waits on write-set-vectors, Soul holding,
and `asura:cut_spec:cut-semver-measured` landed. Asura Cut 3 bases on its merge commit.
Element-id part 2: `c2a-id-closed-world` r2 decides deferred findings `s5.member-set-misses-allowprivate`, `s5.enumerating-rule-unpinned`, `s6.null-standard-untyped`, `s6.two-recognisers-one-rule`, `s4.unrecognised-holders-fail-open` and follow-up `c2a-datacontract-holder-unwalked` (see its `first`).
- **marker-for-whole-lists** (only if `c2a-whole-list-opt-in` is ruled `marker-when-ids-declared` or `marker-for-any-keyed-object`): a CarriesElements predicate, a refusal unless the member is marked, and a public attribute. It is additive after part 2.
- **C2b constraint** (owed to `variants:follow_up:remaining-variant-cuts`): nested path resolution must refuse a step `[slot:elementId]` that enters a whole value, naming the holder. A whole value is any shape `CultElementIds.Classify` calls Leaf, so C2b asks Classify and keeps no recogniser of its own. Today's C2a resolver refuses every non-top-level step, so no store can hold such an override yet.
Rationale. The 2026-10-02 order (parity, vectors, then merge main) was right when main was close.
Main has since moved 453 commits and rebuilt exactly what the parity cuts change: cultcache-ts
payload ownership, cultmesh-ts, cultcache-rs. Parity mapped before the merge would be mapped
twice. Merging first means every later cut is mapped once, on the code it changes. The C# fix
batch goes first because the operator's at-commit ruling is the contract the parity runtimes
copy; and it touches C# files main barely changed, so it can run beside the merge with one
small reconcile (the `MintStoredAt` region). The High finding is not on this path: h3 fixed it
and Soul s3 held it (body facts above).
## 5. Cuts, in order
All cuts are in `F:\Projects\CultLib`. Rebase `codex/document-variants` (docs only) onto
@ -1414,7 +1605,7 @@ The one door per runtime, and who walks through it:
- **Inputs.** The bytes at the path. **Outputs.** The decoded store with header and catalog; or a typed refusal (`CultStoreUnreadableException` in C#; `CultCacheStoreUnreadable{path, kind}` in Rust, kind `Undecodable` or `UnsupportedFormat`; `StoreUnreadableError` in TS and Python). A refusal names a header only as `cultcache.store.v<digits>`.
- **Derived state.** The write header is never decided by the writer: it is the reader's verdict (header read, whole-store flag, whether ids or variants are held). A rewrite of a file that cannot be read cannot happen, because the reader throws first.
- **Forbidden writers.** A second header classifier. Deleted: Rust `leading_header`, `read_array_header`, `read_string`; cultnet-ts's own decode of the store header; the TS async `readStore`; Rust `StoreUnreadableError` (negative grep `StoreUnreadableError` in `packages/cultcache-rs`: no match); Python `_decode_v1_snapshot`.
- **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore declares cultcache-ts `^0.15.0`, the first release that exports it.
- **Shared paths.** cultnet-ts reaches the header decision only through `readSingleFileStore` (AUTH:4, :321) and checks only record count, schema id and type. cultnet-ts therefore needs a cultcache-ts release that exports it, which is the release that ships with this change; the manifests move to that version then (c0-ships-with-c2a), not before.
- **Cut line.** `merge-r1` h3 (Rust) and h4 (TS, cultnet-ts); spec `cut-merge-r1.r3` authority map.
- **Pinned by.** `tests/vectors/document-variants-c2a/readability/manifest.txt`, walked by `StoreReadabilityTests.OpenReadsExactlyTheFilesTheVectorsSayItReads` (C#), `CultMeshSingleFileReadabilityTests.AWriteReplacesAFileExactlyWhenItReads` (Mesh), `a_file_is_replaced_exactly_when_it_reads` (RS:5132), "SingleFileMessagePackBackingStore replaces a file exactly when it reads" (TS test :2321) and `test_single_file_replaces_a_file_exactly_when_it_reads` (Python). AUTH cases: `packages/cultnet-ts/test/idunn-runtime-authority.test.ts`. Declared divergences in the manifest: `legacy-envelopes.bin` (C# refuses; the others read and replace it) and `variant-slot-v3.bin` (C# reads; the others refuse).
- **Known gaps.** `variants:follow_up:c0-bad-canonical-in-manifest` (`current-catalog-bad-canonical.msgpack` is not in the manifest; TS reads it, Rust refuses). `variants:follow_up:legacy-vectors-all-runtimes` (the three `legacy-catalog-*` c0 vectors have a verdict only in C# tests; all four runtimes refuse them, probed). `variants:follow_up:range-check-in-ci` (`npm run test:pack:ts` builds every admitted tag and checks exports, but `.github/workflows/publish-packages.yml` does not run it).

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

@ -25,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,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,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)
}

View file

@ -0,0 +1,160 @@
# Python slot scalar types vs the C# reference: facts and pointers
Fork served: cultlib-gaps: Python slot scalar types vs the C# reference's refusals, raised by cut-union-bounds-py.h1 undone.
Date: 2026-10-11. Gathered by eyes-py-slot-scalar-types (read-only). Facts and pointers only; no recommendation.
Checkout caveat: line numbers below are the operator checkout at `main` 4ea296ae. cultcache-py's bounds cut lives on
branch `hands/union-bounds-py` (head 4328ddb4); cultcache-ts's union codec lives on `hands/union-codec-ts`
(absent from `main`: `git ls-tree HEAD packages/cultcache-ts/src` has no `codec.ts`).
## 1. In CultLib
### 1.1 The measured gap (the trigger)
Verdict `cultlib-gaps:verdict:cut-union-bounds-py.s1`, claim `scalar-type-parity-unproven`, outcome Unproven, no finding
(spec did not promise it). Probe `cross.py g-` files, 168 cases, job1.log:
- 13 scalar slots x 8 wrong values; agree 86; C# refuses and Python accepts 82; C# accepts and Python refuses 0.
- String slots (Id, Box.Id, Text.Id, Text.Content, Image.Uri, Image.Alt, Comp.Name, Pending.Message, Tool.Name):
Python accepts int, float, true, bin, array, map (refuses neither str nor nil, as C#).
- Int slots (Version, Completed.SourceVersion, Tool.Uses, Part.Count): Python accepts float, str, true, bin, array, map
and nil; int5 agrees.
- All 8 container-shape slots (children list, record, slot list, outcomes list, arm slots, union x2, document) agree on
every wrong value (64 shape replacements agree). So only scalar leaves differ.
- Related earlier finding: `cultlib-gaps:finding:cut-union-codec-py.s2.python-decode-slot-strictness-differs`
(Python accepts Tool wire value [1,['hammer',12]] as Text(content=12), C# refuses; Python refuses short Gem value,
C# accepts with default). Resolved Deferred to `cultlib-gaps:cut_spec:cut-union-bounds-py.r1`.
### 1.2 C# reference: how a slot's scalar type is declared and refused
- Declaration is the C# member type plus `[Key(n)]`; there is no CultLib scalar-type vocabulary.
`src/GameCult.Caching/README.md:16-27` (example), `src/GameCult.Caching/Contracts/cultcache-schema-compatibility.md:31-35`
("A member's slot is its MessagePack `[Key(n)]` integer").
- Registry checks are structural only (CultCache.cs:1015-1053, messages 1430-1461: needs [MessagePackObject], [Key] on every
public member, no string keys, distinct keys, constructor shape).
- The wrong-scalar refusal is made by MessagePack-CSharp's per-type formatter (external library, not CultLib code); CultLib
contains no scalar-type check of its own. (Grep of `src/` for scalar/wrong-type checks found none; unions get a guard:
`src/GameCult.Caching.MessagePack/CultUnionGuard.cs:44-50` refuses non-integer key, key outside Int32, undeclared key,
nil arm slots.)
- Published schema: each member's `TypeName` is generated from the member's CLR type,
`CultCache.cs:647` `TypeName = CultSchemaTypeNames.FromType(member.MemberType)`; `CultSchemaTypeNames.cs:9-31` yields
`System.String`, `System.Int32`, `System.String[]`, `System.Collections.Generic.List<...>`.
Catalog members persisted with `Slot, MemberName, TypeName, IsReference, IsMany, TargetSchemaName, IndexAlias, IsName`
(`CultDocumentMessagePackSerialization.cs:465-496`; `CultCache.cs:39,326`).
- The catalog TypeName is load-bearing at open: `CultCache.cs:874-877` a persisted member with a different TypeName is an
error (`slot N changed type 'A' -> 'B'`); an absent member is only a warning (`:863-872`). The catalog names a type; no
code reads it to validate payload scalars on read.
### 1.3 cultcache-rs
- Slot types are the Rust field types, enforced by serde. `packages/cultcache-rs/cultcache-rs-derive/src/lib.rs`:
`#[cultcache(key = N)]` per field (`:126-171`), `slot_codec` (`:177`) generates `Serialize` as a positional tuple with nil
gaps and `Deserialize` by slot (`:213-258`), extra trailing slots skipped with `IgnoredAny` (`:233`, `:305`). `bytes` and
`default` attributes are the only extras (`:175-176`). Union: `CultUnion` derive, arm key Int32, unknown key a decode error
(`:87-97`, `:326-340`; `packages/cultcache-rs/README.md:187-196`).
- No separate scalar-type vocabulary; no catalog-type derivation was looked for in rs here (not read).
### 1.4 cultcache-ts
- On `main`: no slot codec. `packages/cultcache-ts/src/document.ts:51-64` takes author-declared schema members and refuses
an empty `typeName`; `types.ts:22-24` member has `slot`, `typeName`. `swarm-documents.ts:91-95` hand-writes
`typeName: "string"|"long"|"object"`. Payload encode/decode is a user function (`types.ts:17-18`).
- On branch `hands/union-codec-ts`, `packages/cultcache-ts/src/codec.ts`: typed leaf codecs `string`, `boolean`, `bytes`,
`int32`, `int64`, `float32`, `float64`, `list`, `array`, `nullable`, each with the reference's `typeName`
(`"System.String"`, `"System.Int32"`, ... lines 39-128) and `expect`/`refuse` on a wrong kind in both encode and decode
(lines 31-42, 60-79). `SlotsCodec.members` derives the schema catalog from the slot codecs (lines 17-20). Header
comment lines 5-8 say the reference refuses a store whose slot names another type.
- Precedent in the mind: finding `cultlib-gaps:finding:cut-union-codec-ts.s1.codec-catalog-refused-by-reference` (High,
Fixed 0882eee9): TS codec first derived `System.Object` for every slot; C# refused the whole store (7 slots).
### 1.5 cultcache-py today
- Slot = `DatabaseEntryField(name, key, default, codec, many)`; `codec` is a `SlotCodec` (encode/decode callables) only for
nested record or union (`documents.py:61-92`). A scalar slot has no codec and no type: `encode_slot`/`decode_slot`
return the value unchanged when `field.codec is None` (`:370-386`). Consumers write `("name", 0)` tuples, 3-tuples with a
default, or `database_entry_field("body", 1, codec=..., many=True)` (`:276-283, 417-`;
`packages/cultcache-py/tests/test_union_parity.py:25-59`; README `packages/cultcache-py/README.md:30,114-148`).
- Python-side checks that exist: record is a list (`:394-396`), missing required slot (`:398-409`), union shape/arm key
(`:313-325`, bool excluded by `_is_key` `:346-347`), many-slot is a list (`:383-384`), key >= 0 (`:88-89`).
- Catalog: `define_database_entry_type` emits every member with `type_name="System.Object"` (`documents.py:244`) and
`canonical_schema_json` built from the members (`:140-160`). `interop.py:38-46` instead hard-codes C# names
(`System.String`, `System.String[]`) for its one fixed document.
Same `System.Object` default earlier caused the TS finding in 1.4 (finding text: "the codec's default copies Python's
define_database_entry_type, whose own stores would hit the same refusal").
- Published schema carrying slot types for Python to read: the C# catalog carries `TypeName` per slot (1.2), persisted in
the store and compared at open; `interop.py` shows Python reading catalog entries. No JSON Schema / CultNet schema with
scalar types was found by grep in cultcache-py; `canonical_schema_json` in Python is built from `type_name` members.
Whether a CultNet schema catalog outside the store (cultnet-py) exposes slot scalar types was not checked.
### 1.6 Probe: the C# reference opening Python-written stores (2026-10-11, imagination-cl-py-slot-types)
Section 1.5 left this untested. Python 3.13 and cultcache-py at main 2037dd44 wrote four single-file stores of
`cultcache.interop-note`. The C# reference at 2037dd44 read them through
`tests/GameCult.Caching.InteropPeer read --file` (dotnet sdk 10.0 on Yggdrasil, ygg-verify job1). The scripts and
log are in the Imagination scratchpad `imagination-cl-py-slot-types/` (`write_stores.py`, `cmd.txt`, `job1.log`).
| Store | Python definition | Payload | C# result |
|---|---|---|---|
| a | `interop.py` (`define_document_type`, catalog members hand-typed `System.String`, `System.String[]`) | correct | opens, record read |
| b | `define_database_entry_type`, same schema name and version as C# | correct | refused: `Persisted schema 'cultcache.interop-note' ... is incompatible ...: slot 0 changed type 'System.Object' -> 'System.String'; slot 1 ...` (every slot) |
| c | as b | `title = 12` | refused at the catalog, as b |
| d | as a (typed catalog) | `title = 12`, accepted by Python on write | refused at decode: `Record 'note:python-d' ... cannot be decoded ... Unexpected msgpack code 12 (positive fixint)` |
Mechanism (source read): the C# schema id is a SHA-256 of the semantic fingerprint (`CultCache.cs:656-658`), while
Python's defaults to the type name. A Python-written entry therefore never matches by id. It resolves by schema name
(`CultCache.cs:518-521`), and the content hashes differ, so `CompareSchemaShapes` runs and refuses any TypeName mismatch
(`:874-877`). The refusal is thrown from the pull, so the C# cache does not open the store at all.
Consequences:
- `define_database_entry_type` has no `members` or per-slot type parameter (`documents.py:214-229`). An ordinary
Python consumer cannot write a store that a C# peer with the same schema name will open. The only way to get one is
to hand-build `define_document_type` with typed members, as `interop.py` does. This is the TS finding
`cut-union-codec-ts.s1.codec-catalog-refused-by-reference` again, this time in Python.
- Store d: a wrong scalar that Python accepts on write makes the store unreadable to the C# reference.
- A C# `object` member is also cataloged `System.Object`, and MessagePack-CSharp's object formatter accepts any
value. Python's current default is therefore true of a C# `object` slot, and false for every typed C# slot.
Real consumer definitions for scale (source read): `cultnet_py/cultmesh_contracts.py:626-698`
(`CultMeshSimulationFact`, a frozen dataclass annotated `str`, `int`, `float`, `str | None`, passed as `cls=`), and
`repixelizer/src/repixelizer/verse_state.py:77-118` (dict values with no `cls`; `("value", 0)` and `("surface", 4)`
hold whole nested structures in one slot, which is a genuine "any" slot). Python annotations do not carry the C# widths:
`int` cannot say Int32 or Int64, and `float` cannot say Single or Double.
## 2. Prior art (dynamic host, declared field types)
| System | Types come from | Write | Read | Evidence |
|---|---|---|---|---|
| protobuf python | .proto IDL, generated classes | type-checked on field assignment (`TypeError`; `ValueError` out of range; bool rejected for int) | wire-type mismatch handling not stated in docs | protobuf.dev/reference/python/python-generated ("setting `foo` to a value of the wrong type will raise a `TypeError`"); `type_checkers.py` (IntValueChecker rejects bool `TypeError`, out of range `ValueError`; UnicodeValueChecker non-UTF-8 `ValueError`) https://raw.githubusercontent.com/protocolbuffers/protobuf/main/python/google/protobuf/internal/type_checkers.py ; proto3 guide: wire format "doesn't provide a way to detect fields encoded using one definition and decoded using another" https://protobuf.dev/programming-guides/proto3/ |
| Apache Avro python | Avro schema (JSON), not host code | `DatumWriter.write` calls `validate(..., raise_on_error=True)` -> `AvroTypeException`; union branch picked by non-raising validate | reader does not validate values; checks schema resolution (`SchemaResolutionException`) and decodes by writer schema | https://raw.githubusercontent.com/apache/avro/main/lang/py/avro/io.py ; fastavro writer: `validator` parameter "validation will be done on the records" (opt-in) https://fastavro.readthedocs.io/en/latest/writer.html |
| Thrift python | .thrift IDL, generated code | generated `write` calls `validate()` which checks required-field presence only (not scalar types); `writeFieldBegin` writes the given type unchecked | generated read emits `if ftype == TType.X:` ... `else: iprot.skip(ftype)`: a wrong wire type is silently skipped, field left unset | t_py_generator.cc `generate_py_struct_reader`, `generate_py_struct_required_validator` https://raw.githubusercontent.com/apache/thrift/master/compiler/cpp/src/thrift/generate/t_py_generator.cc ; TBinaryProtocol.py `writeFieldBegin`/`readFieldBegin` have no type check |
| pydantic v2 | Python annotations in host code | validates input at construction / `validate_python` / `validate_json`; lax coercion default, strict opt-in per call, field or config | same entry points | https://pydantic.dev/docs/validation/latest/concepts/strict_mode/ ("Lax mode (default) coerces"; page does not say assignment or serialization validates) |
| msgspec | Python annotations (Struct) in host code | docs (not retrieved; search snippet only): encode serializes values without enforcing annotations | decode validates against the `type=` and raises `ValidationError`; `array_like=True` encodes Struct as positional array | search result only, unverified: https://jcristharif.com/msgspec/usage.html (fetch returned 404 for usage/structs/supported-types pages) |
| Cap'n Proto (pycapnp) | .capnp schema loaded at runtime (`capnp.load`) | assignment type-error behavior not documented on the fetched page | n/a (pointer-based, no parse step) | https://capnproto.github.io/pycapnp/capnp.html (schema loading: "much like loading a Python module") |
| FlatBuffers python | .fbs schema, `flatc --python` generated typed accessors/builders | no runtime type validation stated on the fetched page | typed accessors read at fixed offsets; no value validation stated | https://flatbuffers.dev/languages/python/ |
| MessagePack (msgpack-python) | none | none | none | no IDL; the format is schemaless (msgpack-python itself not fetched) |
Observed pattern in the fetched sources, stated as fact only:
- Type source is an IDL/schema in protobuf, Avro, Thrift, Cap'n Proto, FlatBuffers; host-code annotations in pydantic and
msgspec.
- Write-side type checking: protobuf (assignment), Avro (default on), pydantic/msgspec (on construction/decode of input);
Thrift write checks required presence only; fastavro opt-in.
- Read-side: Avro reader trusts the writer schema; Thrift generated reader skips mismatched wire types; protobuf docs silent.
Not retrieved / unverified: pycapnp assignment error; FlatBuffers runtime checks; Thrift generated `validate` on type;
msgspec authoritative statements; protobuf python parse behavior for wrong wire type.
## 3. Precedent in the mind
Tool `query` (`eureka-state`), semantic "python slot scalar type validation parity with C# reference", top_k 10, no schema
filter (a `schemas` filter named Ruling/Resolution/Finding was refused: no such schema; schema names are
`<campaign>.<kind>`-style, not enumerated here). Roots variants and cultlib-gaps both appeared; matched 40. Relevant hits:
- `cultlib-gaps:finding:cut-union-codec-py.s2.python-decode-slot-strictness-differs` (PreExisting, Low, Deferred to
union-bounds-py.r1): first record of Python decode strictness differing in both directions.
- `cultlib-gaps:finding:cut-union-codec-ts.s1.codec-catalog-refused-by-reference` (High, Fixed): `System.Object` catalog
types refused by the reference; evidence note says Python's `define_database_entry_type` default has the same shape.
- `cultlib-gaps:finding:cut-union-codec-py.s1.missing-field-typed-item` (Fixed 36aa1a44): Python fixture declared slot 6 as
Item union where C# says Node?; golden carried nil so tests passed.
- `aetheria-release:follow_up:py-float-width-parity`: Python msgpack round-trip widens float32 to float64 (204 -> 284 bytes);
float width is another undeclared-scalar-type consequence in Python (owner CultLib cultcache-py).
- `cultlib-gaps:cut_spec:cut-slot-readonly-parity.r2` and `cut-slot-tolerance.r*`: read-only/unread-slot rules, adjacent.
- No ruling or follow_up was returned that decides scalar-type parity for Python. A dedicated Ruling query was not run
(schema id unknown; the only attempted names were refused).

View file

@ -0,0 +1,100 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using MessagePack;
using NUnit.Framework;
namespace GameCult.Caching.Tests.PyLeavesProbe
{
[MessagePackObject] public sealed class ProbeInner { [Key(0)] public string N = ""; [Key(1)] public float W; }
[Union(0, typeof(ProbeCircle))] public interface IProbeShape { }
[MessagePackObject] public sealed class ProbeCircle : IProbeShape { [Key(0)] public float R; }
[CultDocument("cultcache.py-leaves-probe", "cultcache.py_leaves_probe.v1")]
[MessagePackObject]
public sealed class ProbeDoc
{
[Key(0)] [CultName] public string Id = "";
[Key(1)] public bool Flag;
[Key(2)] public byte[] Blob = Array.Empty<byte>();
[Key(3)] public int I;
[Key(4)] public long L;
[Key(5)] public float F;
[Key(6)] public double D;
[Key(7)] public int? MaybeI;
[Key(8)] public List<string> Tags = new();
[Key(9)] public string[] Names = Array.Empty<string>();
[Key(10)] public object? Any;
[Key(11)] public ProbeInner? Nested;
[Key(12)] public List<ProbeInner> Items = new();
[Key(13)] public IProbeShape? Shape;
[Key(14)] public float[] Floats = Array.Empty<float>();
[Key(15)] public float? MaybeF;
}
[MessagePackObject] public sealed class S1 { [Key(0)] public string? V; }
[MessagePackObject] public sealed class B1 { [Key(0)] public bool V; }
[MessagePackObject] public sealed class Y1 { [Key(0)] public byte[]? V; }
[MessagePackObject] public sealed class O1 { [Key(0)] public object? V; }
[MessagePackObject] public sealed class LS1 { [Key(0)] public List<string>? V; }
[MessagePackObject] public sealed class NI1 { [Key(0)] public int? V; }
[MessagePackObject] public sealed class R1 { [Key(0)] public ProbeInner? V; }
[MessagePackObject] public sealed class U1 { [Key(0)] public IProbeShape? V; }
[MessagePackObject] public sealed class LI1 { [Key(0)] public List<int>? V; }
public class PyLeavesProbeTests
{
static readonly MessagePackSerializerOptions Options = CultDocumentMessagePackSerialization.OptionsFor(typeof(PyLeavesProbeTests).Assembly);
[Test]
public void TypeNames()
{
var names = typeof(CultCache).Assembly.GetType("GameCult.Caching.CultSchemaTypeNames")!.GetMethod("FromType")!;
foreach (var t in new[] { typeof(string), typeof(bool), typeof(byte[]), typeof(int), typeof(long), typeof(float), typeof(double),
typeof(int?), typeof(bool?), typeof(float?), typeof(List<string>), typeof(string[]), typeof(List<int?>), typeof(object),
typeof(ProbeInner), typeof(List<ProbeInner>), typeof(IProbeShape), typeof(float[]), typeof(List<List<string>>),
typeof(Dictionary<string, int>), typeof(byte[][]) , typeof(List<byte[]>)})
TestContext.Out.WriteLine($"TYPENAME {t} => {names.Invoke(null, new object[] { t })}");
}
[TestCase("a-leaf-catalog.cc")]
[TestCase("b-wrong-leaf.cc")]
public async Task OpenPythonStore(string file)
{
var path = Path.Combine("/tmp/pyst", file);
try
{
var cache = new CultCache();
cache.AddBackingStore(new SingleFileMessagePackBackingStore(path));
await cache.PullAllBackingStoresAsync();
var doc = cache.AllEntries.OfType<ProbeDoc>().Single();
var bytes = MessagePackSerializer.Serialize(doc, Options);
TestContext.Out.WriteLine($"OPEN {file} OK id={doc.Id} F={doc.F:R} Shape={doc.Shape?.GetType().Name} Any={doc.Any?.GetType().Name} items={doc.Items.Count}");
TestContext.Out.WriteLine($"CSHARP-REWRITE {file} {Convert.ToHexString(bytes).ToLowerInvariant()}");
}
catch (Exception e)
{
TestContext.Out.WriteLine($"OPEN {file} REFUSED {e.GetType().Name}: {e.Message.Split('\n')[0]}");
}
}
static readonly string[] Values = { "05", "ff", "cb3ff8000000000000", "ca3fc00000", "c0", "c2", "c3", "a161", "d90161", "c40100", "90", "9101", "91a161", "80", "81a161a162", "920091ca3f800000" };
static string Try<T>(byte[] p) { try { var v = MessagePackSerializer.Deserialize<T>(p, Options); return "ok"; } catch { return "--"; } }
[Test]
public void ScalarReads()
{
foreach (var v in Values)
{
var p = Convert.FromHexString("91" + v);
TestContext.Out.WriteLine($"READ {v,-22} string={Try<S1>(p)} bool={Try<B1>(p)} bytes={Try<Y1>(p)} object={Try<O1>(p)} list<string>={Try<LS1>(p)} int?={Try<NI1>(p)} record={Try<R1>(p)} union={Try<U1>(p)} list<int>={Try<LI1>(p)}");
}
}
}
}

View file

@ -0,0 +1,17 @@
# Python typed leaves: probe evidence (2026-10-11)
Serves `cultlib-gaps:ruling:py-slot-typed-leaves` and the specs
`cultlib-gaps:cut_spec:cut-leaf-vectors-cs.r1` and `cut-py-typed-leaves.r1`.
Run by the Imagination pass `imagination-cl-py-typed-leaves` on Yggdrasil at CultLib main 4ec4bfb7.
- `write_probe_stores.py` + `PyLeavesProbe.cs`: a Python store whose catalog uses the leaf type names
(from `CultSchemaTypeNames.FromType`) is opened by the C# reference and its payload rewritten byte-identical,
float32 included (except inside an `object` slot, where C# itself rewrites a fixint as uint8). Negative control:
one slot named Int64 where C# has Int32 is refused ("slot 3 changed type").
- `proto_leaves.py` + `run_vectors.py`: prototype leaves reproduce the C# numeric-leaves vectors (160 reads, 46 writes,
the mixed record). `truth.py`: what C# accepts per slot type (byte[] accepts str; bool only false/true; record
accepts []; nil in an int slot refused).
- `perf.py`: float32 via a marker with packb-first fallback; a 1.5k-node record with float32 about 0.8 ms vs 0.1 ms.
- `job1.log`: the verify run.
Scripts are probes, not a suite; they are kept so the next pass can rerun them.

View file

@ -0,0 +1,22 @@
import sys, time, msgpack
sys.path.insert(0, sys.argv[1]); import proto_leaves as L
# a HullData-like record: 20 float32 slots, strings, nested list of 200 small records
rec = lambda i: ["part%d" % i, i, [L.Float32(0.5*i), L.Float32(1.5), L.Float32(2.5)], None]
tree = ["id", 3, [L.Float32(float(i)) for i in range(20)], [rec(i) for i in range(200)]]
plain = ["id", 3, [float(i) for i in range(20)], [["part%d" % i, i, [0.5*i,1.5,2.5], None] for i in range(200)]]
N=300
t=time.perf_counter(); [L.write(tree) for _ in range(N)]; a=time.perf_counter()-t
t=time.perf_counter(); [msgpack.packb(plain, use_bin_type=True) for _ in range(N)]; b=time.perf_counter()-t
t=time.perf_counter(); [msgpack.unpackb(msgpack.packb(plain)) for _ in range(N)]; c=time.perf_counter()-t
print(f"walk-writer {a/N*1e6:.0f} us/rec, packb {b/N*1e6:.0f} us/rec, ratio {a/b:.1f}x; size {len(L.write(tree))} vs {len(msgpack.packb(plain))}")
def pack_tree(t):
try: return msgpack.packb(t, use_bin_type=True)
except TypeError: return L.write(t)
assert pack_tree(plain) == L.write(plain) == msgpack.packb(plain, use_bin_type=True)
assert pack_tree(tree) == L.write(tree)
t=time.perf_counter(); [pack_tree(plain) for _ in range(N)]; p=time.perf_counter()-t
t=time.perf_counter(); [pack_tree(tree) for _ in range(N)]; q=time.perf_counter()-t
print(f"pack_tree plain {p/N*1e6:.0f} us, float32 tree {q/N*1e6:.0f} us")
try:
pack_tree([object()])
except TypeError as e: print("custom object:", e)

View file

@ -0,0 +1,87 @@
"""Scratch prototype of cultcache_py.leaves, to establish mechanisms against the C# vectors. Not for the repo."""
import math, struct, msgpack
class Refused(ValueError): pass
def kind(v):
if v is None: return "nil"
if isinstance(v, bool): return "a boolean"
if isinstance(v, int): return "an integer"
if isinstance(v, float): return "a float"
if isinstance(v, str): return "a string"
if isinstance(v, (bytes, bytearray, memoryview)): return "bytes"
if isinstance(v, (list, tuple)): return "an array"
if isinstance(v, dict): return "a map"
return type(v).__name__
def refuse(what, v): raise Refused(f"expected {what}, found {kind(v)}")
class Float32: # tree marker: written as MessagePack float 32
__slots__ = ("value",)
def __init__(self, v): self.value = v
def int_to_f32(v: int) -> float:
if abs(v) <= 2**53:
return f32(float(v))
m = abs(v); excess = m.bit_length() - 24
kept = m >> excess; rest = m - (kept << excess); half = 1 << (excess - 1)
if rest > half or (rest == half and kept & 1): kept += 1
r = float(kept) * 2.0**excess # kept<=2^24 exact; scaling exact unless overflow
return f32(-r if v < 0 else r)
def f32(x: float) -> float:
try:
return struct.unpack(">f", struct.pack(">f", x))[0]
except OverflowError:
return math.copysign(math.inf, x)
class Leaf:
def __init__(self, type_name, enc, dec, value_type=False):
self.type_name, self.encode, self.decode, self.value_type = type_name, enc, dec, value_type
def _int_leaf(name, what, lo, hi):
def enc(v):
if not isinstance(v, int) or isinstance(v, bool): refuse(what, v)
if not lo <= v <= hi: raise Refused(f"expected {what}, found an integer out of range")
return v
return Leaf(name, enc, enc, True)
int32 = _int_leaf("System.Int32", "an int32", -2**31, 2**31-1)
int64 = _int_leaf("System.Int64", "an int64", -2**63, 2**63-1)
def _f32_enc(v):
if isinstance(v, bool) or not isinstance(v, (int, float)): refuse("a float32", v)
return Float32(int_to_f32(v) if isinstance(v, int) else f32(v))
def _f32_dec(v):
if isinstance(v, bool) or not isinstance(v, (int, float)): refuse("a float32", v)
return int_to_f32(v) if isinstance(v, int) else f32(v)
float32 = Leaf("System.Single", _f32_enc, _f32_dec, True)
def _f64(v):
if isinstance(v, bool) or not isinstance(v, (int, float)): refuse("a float64", v)
return float(v) # int > 2^53 rounds once (Python int->float is correctly rounded)
float64 = Leaf("System.Double", _f64, _f64, True)
def _str(v):
if v is not None and not isinstance(v, str): refuse("a string", v)
return v
string = Leaf("System.String", _str, _str)
def _bool(v):
if not isinstance(v, bool): refuse("a boolean", v)
return v
boolean = Leaf("System.Boolean", _bool, _bool, True)
# writer: msgpack-python per value, float32 through a single-float packer, containers by header
_P = msgpack.Packer(use_bin_type=True)
_P32 = msgpack.Packer(use_bin_type=True, use_single_float=True)
def write(tree) -> bytes:
out = bytearray()
stack = [tree]
while stack:
n = stack.pop()
if isinstance(n, Float32): out += _P32.pack(n.value)
elif isinstance(n, list):
out += _P.pack_array_header(len(n)); stack.extend(reversed(n))
elif isinstance(n, dict):
out += _P.pack_map_header(len(n))
for k, v in reversed(list(n.items())): stack.append(v); stack.append(k)
else: out += _P.pack(n)
return bytes(out)

View file

@ -0,0 +1,25 @@
import sys, struct, msgpack, math
sys.path.insert(0, sys.argv[2])
import proto_leaves as L
d = sys.argv[1]
leaves = {"int32": L.int32, "int64": L.int64, "float32": L.float32, "float64": L.float64}
rows = msgpack.unpackb(open(d+"/reads.msgpack","rb").read(), raw=False)
bad = 0
for leaf, payload, ok, bits in rows:
raw = msgpack.unpackb(payload, raw=False)[0]
try:
v = leaves[leaf].decode(raw); got = True
b = struct.pack(">q", v) if leaf.startswith("int") else struct.pack(">d", v)
except L.Refused: got, b = False, b""
if got != ok or (ok and b != bits):
bad += 1; print("READ DIFF", leaf, payload.hex(), "C#", ok, bits.hex(), "py", got, b.hex())
print("reads", len(rows), "diffs", bad)
wr = msgpack.unpackb(open(d+"/writes.msgpack","rb").read(), raw=False)
bad = 0
for leaf, text, want in wr:
v = int(text) if leaf.startswith("int") else struct.unpack(">d", bytes.fromhex(text))[0]
got = L.write([leaves[leaf].encode(v)])
if got != want: bad += 1; print("WRITE DIFF", leaf, text, want.hex(), got.hex())
print("writes", len(wr), "diffs", bad)
mixed = L.write([L.string.encode("n1"), L.int32.encode(7), L.int64.encode(8), L.float32.encode(1.5), L.float64.encode(2.5)])
print("mixed equal:", mixed == open(d+"/mixed-record.msgpack","rb").read(), mixed.hex())

View file

@ -0,0 +1,18 @@
import msgpack, struct, sys
d = sys.argv[1]
rows = msgpack.unpackb(open(d+"/reads.msgpack","rb").read(), raw=False)
from collections import defaultdict
t = defaultdict(list)
for leaf, payload, ok, bits in rows:
tok = payload[1:].hex()
t[leaf].append((tok, ok, bits.hex()))
for leaf, cases in t.items():
print(leaf, "accepts", sum(ok for _,ok,_ in cases), "of", len(cases))
print(" refused:", [tok for tok,ok,_ in cases if not ok])
# float accepted from integer? int from float?
f = msgpack.unpackb(open(d+"/float32-nan.msgpack","rb").read(), raw=False)
for p, b in f:
v = msgpack.unpackb(p)[0]
bits = struct.pack(">d", v).hex()
repacked = msgpack.packb([v], use_single_float=True).hex()
print("nan in", p.hex(), "C# out", b.hex(), "py double", bits, "py repack f32", repacked)

View file

@ -0,0 +1,42 @@
"""Writes Python stores whose catalog carries the C# type names the proposed leaves would derive, for the C# reference to open."""
import sys, os
sys.path.insert(0, r"F:\Projects\CultLib\packages\cultcache-py\src"); sys.path.insert(0, os.path.dirname(__file__))
import proto_leaves as L
from cultcache_py import CultCache, SingleFileMessagePackBackingStore
from cultcache_py.documents import define_document_type
from cultcache_py.backing_store import CultCacheSchemaCatalogMember as M
NS = "GameCult.Caching.Tests.PyLeavesProbe"
LIST = "System.Collections.Generic.List<{}>"
MEMBERS = [ # slot, member, C# type name the leaf derives
(0, "Id", "System.String"), (1, "Flag", "System.Boolean"), (2, "Blob", "System.Byte[]"),
(3, "I", "System.Int32"), (4, "L", "System.Int64"), (5, "F", "System.Single"), (6, "D", "System.Double"),
(7, "MaybeI", "System.Nullable<System.Int32>"), (8, "Tags", LIST.format("System.String")),
(9, "Names", "System.String[]"), (10, "Any", "System.Object"), (11, "Nested", NS + ".ProbeInner"),
(12, "Items", LIST.format(NS + ".ProbeInner")), (13, "Shape", NS + ".IProbeShape"),
(14, "Floats", "System.Single[]"), (15, "MaybeF", "System.Nullable<System.Single>"),
]
F32 = lambda x: L.Float32(L.f32(x))
def payload(doc_id):
inner = lambda n, w: [n, F32(w)]
return [doc_id, True, b"\x01\x02", 7, 2**40, F32(0.1), 0.1, None, ["a", "b"], ["x"], {"k": [1, "two"]},
inner("n", 1.25), [inner("i0", 0.5), inner("i1", 2.0)], [0, [F32(3.5)]], [F32(1.0), F32(0.1)], F32(2.5)]
def write(path, members, tree_for):
doc = define_document_type(
"cultcache.py-leaves-probe", encode=lambda v: v, decode=lambda v: v,
payload_encoder=L.write, payload_decoder=lambda b: b, name=lambda v: v[0],
schema_name="cultcache.py-leaves-probe", schema_version="cultcache.py_leaves_probe.v1",
members=[M(slot=s, member_name=n, type_name=t, is_name=(s == 0)) for s, n, t in members])
if os.path.exists(path): os.remove(path)
cache = CultCache.builder().register_document_type(doc).add_generic_store(SingleFileMessagePackBackingStore(path)).build()
cache.pull_all_backing_stores()
tree = tree_for(os.path.basename(path))
cache.put(doc, tree[0], tree)
print(path, "payload", L.write(tree).hex())
out = sys.argv[1]
write(os.path.join(out, "a-leaf-catalog.cc"), MEMBERS, payload)
# negative control: the int32 slot cataloged as the int64 leaf would name it
wrong = [(s, n, "System.Int64" if s == 3 else t) for s, n, t in MEMBERS]
write(os.path.join(out, "b-wrong-leaf.cc"), wrong, payload)

View file

@ -0,0 +1,31 @@
# Prior art: refuse or walk a container the identity walk does not own
For `variants:question:c2a-unowned-holders` (campaign `variants`, cut `c2a-element-id-holes` r5).
These are from Imagination's reading. No web fetch was made in this pass, so check the
pointers before quoting them outside the mind.
| System | Unknown or unsupported type in a payload | Extension point | Pointer |
|---|---|---|---|
| Protocol Buffers | Not expressible. A field is a scalar, a message, `repeated`, or `map<scalar, V>`. No other container exists. | None. You model the data as messages. | protobuf.dev, Language Guide (proto3), "Repeated fields" and "Maps" |
| Microsoft Bond | Fixed container set: `list`, `vector`, `set`, `map`, `nullable`, `blob`, `bonded`. Any other type is a codegen error. | Type aliases with explicit converters, declared per type. | microsoft.github.io/bond/manual/bond_cs.html, "Type aliases" and "Custom type mappings" |
| Orleans 7+ serializer | Throws `CodecNotFoundException` when it serializes a type with no codec (not `[GenerateSerializer]`, not registered). | Surrogates via `[RegisterConverter]`, or codecs registered per type. Built-in codecs cover the BCL immutable collections. | learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization |
| MessagePack-CSharp 3.1.7 | `DynamicObjectResolver` refuses an unannotated type (formatter null, the serializer throws) unless the caller opts into contractless. | Resolvers, `[MessagePackFormatter]`. | Probed: `scratchpad/imag-r5/probe1.log` (NkHold: std=null, contractless builds one) |
| System.Text.Json (source gen) | `NotSupportedException` for types outside the generated metadata. Polymorphism needs `[JsonDerivedType]` registration. | `JsonConverter<T>`, `[JsonDerivedType]`. | learn.microsoft.com/dotnet/standard/serialization/system-text-json/polymorphism |
## What the table says for CultCache's id walk
1. Every system that guarantees something about a payload's structure fails closed on a
shape it does not own. The failure is loud and comes at build or first use. It never
drops data silently. Protobuf and Bond refuse when the schema is written, and Orleans,
MessagePack and STJ refuse when the type is first serialized. None of them walks an
arbitrary wrapper on the chance that it is a container.
2. Each system supports a fixed set of containers and walks those natively. Extension is
explicit and per type: Bond aliases, Orleans surrogates, STJ converters. The walker
never infers it from a type's shape.
3. Orleans covers the BCL immutable collections with built-in codecs. Each added container
is a deliberate, tested entry. A reasonable consumer therefore expects ImmutableArray to
be supported when the library says so, and refused loudly when it does not. Nobody
expects silent nil ids.
CultCache's id walk today is open-world: a shape it does not recognise passes as a leaf.
That is the opposite of every row above, and it is the common cause of findings s1-s4.

View file

@ -0,0 +1,111 @@
# Shader variable-length inputs: how shader libraries take a span, and what CultMath's build route allows
For `cultmath-tapes:follow_up:shader-weighted-lattice-loop` and `cultmath-tapes:follow_up:lattice-shader-pins` (campaign `cultmath-tapes`).
Imagination pass, agent `imagination-ag4-shader-loop`, session `self-2026-10-16-ag4`, 2026-10-17, against CultLib `origin/main` `e2051c5a`
and the build branch `hands/cultmath-glsl-build-dxc` `23ac4e89`. Probes are in `docs/research/shader-variable-inputs/`.
Tags: [src] primary source text read this pass; [web] search summary only; [probe] run this pass, output quoted below.
## 1. The question
C#'s `weighted_lattice_cells` reads the body span once per lattice cell (the weight is the exact power border of the
cell centre), and the ring search around it is the part a consumer cannot safely write by hand: the exit bound,
the two passes and the box-independent fold order. The shader port has the four steps only. A shader function cannot
take `ReadOnlySpan`, the library targets GLSL ES 3.00 fragment shaders (WebGL2: no storage buffers), and the GLSL is
built from the HLSL by dxc `lib_6_3` SPIR-V plus SPIRV-Cross (ruling `glsl-es-via-dxc-spirv-cross`).
So how do bodies reach a shader `weighted_lattice_cells`?
## 2. Prior art: how shader code that ships as a library takes a variable number of inputs
- [src] Unity URP (`ShaderLibrary/Input.hlsl`, `RealtimeLights.hlsl`, Graphics `master`): a fixed capacity
`MAX_VISIBLE_LIGHTS` chosen per platform (`MAX_VISIBLE_LIGHT_COUNT_LOW_END_MOBILE` under `SHADER_API_GLES30`,
`_MOBILE` for GLES3/WebGPU, `_DESKTOP` otherwise), constant-buffer arrays `float4 _AdditionalLightsPosition[MAX_VISIBLE_LIGHTS]`
and the like, with `StructuredBuffer<LightData>` only behind `USE_STRUCTURED_BUFFER_FOR_LIGHT_DATA` (defined 0). The
count is a uniform read through `GetAdditionalLightsCount()`, and `GetAdditionalLight(uint i, float3 positionWS)` is
the accessor; `LIGHT_LOOP_BEGIN(lightCount)` / `LIGHT_LOOP_END` macros hide the cluster versus plain loop. The
pipeline owns the storage and the accessor; user shaders call them. Capacity plus count, storage owned by the include.
- [src] Filament (`shaders/src/surface_light_punctual.fs`, `main`): the loop is the engine's, over a froxel record
(`froxelsUniforms.records`, then `froxelRecordUniforms.records`) giving `recordOffset` and `count`; each light is
`lightsUniforms.lights[lightIndex]`, a uniform-block array. Material code never sees the loop. The engine owns both
the storage and the loop because it also owns the uniform layout.
- [src] three.js (`ShaderChunk/lights_fragment_begin.glsl.js`, `dev`): `for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ )`
under `#pragma unroll_loop_start`, reading `pointLights[ i ]`; `NUM_POINT_LIGHTS` is a define the renderer writes,
so the shader is regenerated per light count. Count as a compile-time constant; the library is source text.
- [web] Bevy: clustered forward needs storage buffers; on WebGL2 it falls back to a uniform buffer with a capped
object count (`MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS` 204 in an older `bevy_pbr`, asserted to fit 16384 bytes; the
Many Lights example says 256 lights under `WGPU_SETTINGS_PRIO=webgl2`). Capacity plus count where storage is absent.
- [web] lygia (README conventions, via search): reuse by `#ifndef` / `#define` options the caller sets before the
include (`SAMPLER_FNC(TEX, UV)`, `MYFUNC_TYPE`, per-function sampler overrides). Caller-supplied accessor macros; the
library ships as source text, which is what lets a macro reach inside a function body.
The pattern that splits them: a caller-supplied accessor (lygia, URP's accessor functions, three.js's define) works
where the library is source text the caller's preprocessor sees. Where the library is compiled or its storage layout is
fixed (URP's GLES path, Bevy's WebGL2 fallback, Filament's uniform blocks), the input is a fixed-capacity array plus a
count, and the capacity is a named library constant.
## 3. What the build route allows (probes)
| Candidate | HLSL (dxc) | Library SPIR-V and SPIRV-Cross GLSL | C# mirror (`TransformHlslToCSharp`) | Verdict |
|---|---|---|---|---|
| Fixed-size array parameter plus count | compiles, `lib_6_3` and `cs_6_0` [probe P1, P3] | see P2 | needs one transform (`T x[N]` parameter to `T[] x`) | see section 4 |
| Caller accessor macro (`CULTMATH_BODY(i)`) | the library build defines no accessor, so the function is absent [probe P4] | absent from the built GLSL: SPIRV-Cross output carries no consumer macro | the mirror would need the macro too | refused by the route |
| Bodies packed into library-owned constants | a library cbuffer or uniform | a uniform block in a function-only library; a binding the consumer does not own | a static field | refused (P5, by construction, not run): a library may not own a consumer's binding |
Probes (files in `shader-variable-inputs/`):
- [probe] P1, local, dxc 2026_07_29 (`.tools/dxc`): `lattice-array.hlsl` (CultMath.hlsl, `prototype.hlsl`, one export)
compiles with `-T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od`. The SPIR-V types the array as
`OpTypeArray %v4float %uint_16` behind a Function pointer, and `-Od` copies it at each call (`param_var_bodies`).
- [probe] P3, local: `entry-cbuffer.hlsl` passes a cbuffer array directly and a `StructuredBuffer` copied into a local
array; both compile to SPIR-V (`cs_6_0`) and to DXIL.
- [probe] P4, local: `macro-accessor.hlsl` with no `CULTMATH_BODY` defined fails, `use of undeclared identifier`: a
macro-parameterised function does not exist in a library compiled without the caller's macros, so the built GLSL
cannot carry it. Shipping it would need a hand-kept GLSL text beside the build, a second owner the ruling removed.
- [probe] P2, Yggdrasil job (`probe.sh` at `23ac4e89`, sdk:10.0, 2026-10-17): with the prototype appended to
`CultMath.hlsl` and two build-tool edits (`wrappers.py` strips `[N]` from an argument name; `partition.py` accepts
`static const int`), `build-glsl.sh` is green ("build shape ok", "suffixed ids: 0; inout: 0") and emits
`const int CULTMATH_MAX_SITES = 16;`, `vec4 cultmath_power_border(vec3 p, vec4 sites[16], int count)` and
`vec4 cultmath_weighted_lattice_cells(vec2 p, float time, vec4 bodies[16], int count, ...)`. A `#version 300 es`
fragment consumer passing `uniform vec4 zone[CULTMATH_MAX_SITES]` compiles with glslang.
- [probe] Same job, lavapipe (`llvmpipe (LLVM 20.1.2, 256 bits)`), 257 cases from `ref/Program.cs` (the ring-17 point
of `TheCapIsReachedAndMatchesTheFullBox` and 7 neighbours, four looks, five normals including (3e-30, -4e-30) and
(1e30, 2e30), a zero normal at stretch 1, counts 0, 1, 3, 6, 16), judged against C#'s bits at 2.2e-6 max(|v|, 1):
| Variant (HLSL route unless named) | bit-exact | over bound | worst |
|---|---|---|---|
| built GLSL | 237 | 0 | 1.19e-6 |
| HLSL (equal to built GLSL bit for bit) | 237 | 0 | 1.19e-6 |
| M1 exit at `0.5 k` | 236 | 1 | 4.28e-3 |
| M2 exit ignores the smooth band (`>= f2`) | 226 | 13 | 8.33e-2 |
| M3 normaliser drops the divide by the largest component | 148 | 96 | 1e30 |
| M4 normaliser drops the divide by length | 1 | 208 | 0.259 |
| M5 smooth step loses its `min` cap | 153 | 85 | 2.26e-3 |
| M6 box pass skips its first row | 228 | 9 | 0.807 |
| M7 exit one ring late (`0.5 (k - 2)`) | 237 | 0 | 1.19e-6 |
M7 is equivalent: the result does not depend on the box (the proof above `weighted_lattice_cells`), so a late exit
costs rings and changes nothing. M1 is killed by one case of 257, the ring-17 point: an early exit only matters where
a far ring wins, so the device family must carry several such points.
## 4. Decision
The fixed-size array plus count is the only candidate the route admits, and it is the shape the compiled-library
prior art uses (URP's GLES path, Bevy's WebGL2 fallback, Filament's uniform blocks). The capacity is a named library
constant, `CULTMATH_MAX_SITES`, shared by every span-taking shader function. Its value is
`cultmath-tapes:question:shader-site-capacity` (Aetheria zones: p50 17, p99 57, max 80 bodies, gravity map B28).
The shader recomputes each distance in the box pass instead of caching it: C#'s 75 x 75 stack cache is not a shader
option, and recomputing gives the same value, as the probe's bit-exact HLSL and built-GLSL agreement shows.
Reflection cannot pass a `ReadOnlySpan`, so the mirror and the fixture reach the C# span functions through one
explicit adapter table rather than `MethodInfo.Invoke`.
## 5. lattice-shader-pins
- The smooth-step family draws m in [f1, f1 + smoothing), so the cap never binds. The cap binds when m = f1 + smoothing,
d = f1 + x smoothing with x around 1.3 to 1.9, and smoothing above about 0.25: next - m = (d - m) h - h (1 - h)
smoothing / (1 + 3 smoothing) is positive there (for smoothing 1 and d - m = 0.5, h = 0.156 and next - m = +0.045).
M5 above is killed by the whole function already; the step family should kill it on its own.
- Platform values: `8b31e47e` rewrote 7 platform-family entries of `glsl-parity.json` from "Ubuntu 24.04.5 LTS" to
"Microsoft Windows 10.0.26200", while `7ddb1040` recorded the lavapipe bounds on Yggdrasil against that fixture.
The bounds, the lavapipe job and the build route all run in Yggdrasil's sdk:10.0 Ubuntu image, so that image
owns the fixture's platform values too. A write on another OS is refused rather than reviewed.

View file

@ -0,0 +1,15 @@
// Probe C: a compute entry passing a constant-buffer array, and one passing a StructuredBuffer copied to a local array.
#include "lattice-array.hlsl"
cbuffer Zone : register(b0) { float4 zoneBodies[CULTMATH_MAX_SITES]; int zoneCount; };
StructuredBuffer<float4> bufferBodies : register(t1);
RWStructuredBuffer<float4> result : register(u2);
[numthreads(64, 1, 1)]
void main(uint3 id : SV_DispatchThreadID)
{
float2 p = float2(id.x * 7.0, id.y * 11.0);
float4 local[CULTMATH_MAX_SITES];
for (int i = 0; i < CULTMATH_MAX_SITES; i++)
local[i] = i < zoneCount ? bufferBodies[i] : float4(0.0, 0.0, 0.0, 1.0);
result[id.x] = cultmath_weighted_lattice_cells(p, 1.0, zoneBodies, zoneCount, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5)
+ cultmath_weighted_lattice_cells(p, 1.0, local, zoneCount, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5);
}

View file

@ -0,0 +1,16 @@
layout(local_size_x = 64) in;
layout(std430, binding = 0) readonly buffer IB { uint a[]; } In;
layout(std430, binding = 1) buffer OB { uint a[]; } Out;
void main()
{
uint i = gl_GlobalInvocationID.x; if (i >= NCASES) return; uint b = i * 76u;
vec4 bodies[CULTMATH_MAX_SITES];
for (int k = 0; k < CULTMATH_MAX_SITES; k++)
bodies[k] = uintBitsToFloat(uvec4(In.a[b + 2u + 4u * uint(k)], In.a[b + 3u + 4u * uint(k)], In.a[b + 4u + 4u * uint(k)], In.a[b + 5u + 4u * uint(k)]));
uint c = b + 67u;
vec4 r = cultmath_weighted_lattice_cells(uintBitsToFloat(uvec2(In.a[b], In.a[b + 1u])), uintBitsToFloat(In.a[c]), bodies, int(In.a[b + 66u]),
uintBitsToFloat(In.a[c + 1u]), uintBitsToFloat(In.a[c + 2u]), uintBitsToFloat(In.a[c + 3u]), uintBitsToFloat(In.a[c + 4u]),
uintBitsToFloat(In.a[c + 5u]), uintBitsToFloat(uvec2(In.a[c + 6u], In.a[c + 7u])), uintBitsToFloat(In.a[c + 8u]));
uvec4 w = floatBitsToUint(r);
Out.a[i * 4u] = w.x; Out.a[i * 4u + 1u] = w.y; Out.a[i * 4u + 2u] = w.z; Out.a[i * 4u + 3u] = w.w;
}

View file

@ -0,0 +1,16 @@
// Probe compute entry (HLSL route): 76 words in per case (p, 16 bodies, count, 9 parameters), 4 words out.
#include "CultMath.hlsl"
[[vk::binding(0)]] StructuredBuffer<uint> In;
[[vk::binding(1)]] RWStructuredBuffer<uint> Out;
[numthreads(64, 1, 1)] void main(uint3 id : SV_DispatchThreadID)
{
uint i = id.x; if (i >= NCASES) return; uint b = i * 76u;
float4 bodies[CULTMATH_MAX_SITES];
for (int k = 0; k < CULTMATH_MAX_SITES; k++)
bodies[k] = float4(asfloat(In[b + 2 + 4 * k]), asfloat(In[b + 3 + 4 * k]), asfloat(In[b + 4 + 4 * k]), asfloat(In[b + 5 + 4 * k]));
uint c = b + 67u;
float4 r = cultmath_weighted_lattice_cells(float2(asfloat(In[b]), asfloat(In[b + 1])), asfloat(In[c]), bodies, asint(In[b + 66]),
asfloat(In[c + 1]), asfloat(In[c + 2]), asfloat(In[c + 3]), asfloat(In[c + 4]), asfloat(In[c + 5]),
float2(asfloat(In[c + 6]), asfloat(In[c + 7])), asfloat(In[c + 8]));
Out[i * 4] = asuint(r.x); Out[i * 4 + 1] = asuint(r.y); Out[i * 4 + 2] = asuint(r.z); Out[i * 4 + 3] = asuint(r.w);
}

View file

@ -0,0 +1,14 @@
#!/usr/bin/env python3
"""Compare a device output with C#'s reference bits: cases over the lavapipe bound 2.2e-6 max(|v|, 1), and the largest.
usage: judge.py <ref.bin> <out.bin> <label>"""
import math, struct, sys
ref, out = open(sys.argv[1], 'rb').read(), open(sys.argv[2], 'rb').read()
n = len(ref) // 16
r, o = struct.unpack('<%df' % (4 * n), ref), struct.unpack('<%df' % (4 * n), out)
over, exact, worst = 0, 0, 0.0
for i in range(n):
s = max((abs(o[4*i+j] - r[4*i+j]) / max(abs(r[4*i+j]), 1.0) if math.isfinite(o[4*i+j]) else math.inf) for j in range(4))
exact += ref[16*i:16*i+16] == out[16*i:16*i+16]
over += s > 2.2e-6
worst = max(worst, s)
print('%s: cases %d, bit-exact %d, over 2.2e-6 %d, worst %.3g' % (sys.argv[3], n, exact, over, worst))

View file

@ -0,0 +1,7 @@
// Probe A (local dxc): the prototype as a lib_6_3 SPIR-V library export, bodies as a fixed-size array parameter plus a count.
#include "CultMath.hlsl"
#include "prototype.hlsl"
export float4 probe_lattice(float2 p, float4 bodies[CULTMATH_MAX_SITES], int count)
{
return cultmath_weighted_lattice_cells(p, 1.0, bodies, count, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5);
}

View file

@ -0,0 +1,12 @@
// Probe B: bodies through a caller-supplied accessor macro. The library build defines no accessor.
#include "CultMath.hlsl"
#ifdef CULTMATH_BODY
float4 cultmath_power_border_m(float3 p)
{
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
for (int k = 1; k < CULTMATH_BODY_COUNT; k++)
border = cultmath_power_border_step(p, CULTMATH_BODY(0), CULTMATH_BODY(k), border);
return border;
}
#endif
export float4 probe_macro(float3 p) { return cultmath_power_border_m(p); }

Some files were not shown because too many files have changed in this diff Show more