`isLoopbackEndpoint` handed the raw endpoint to `URL` and read the host it got
back. `URL` is a repairing parser and `System.Uri` is not, so seven spellings
were loopback here and remote in C#: a trailing dot on the host, which `URL`
drops; `ws:127.0.0.1`, `ws:/127.0.0.1` and `ws:///127.0.0.1`, whose authority
`URL` infers; a backslash path separator, which `URL` reads as a separator;
fullwidth digits and letters in the host, which `URL` folds to ASCII; and a tab
in the host, which `URL` deletes. Each of those is a local-development bypass:
a route the C# consumer would have demanded a certificate for opened unsigned
here.
`refusedByCSharpUriShape` refuses exactly those, on the raw string, before
`URL` sees it. Leading and trailing C0 controls and spaces around the whole
endpoint are stripped first, because both sides accept them. The spellings
Hands probed earlier and C# accepts -- `127.1`, hex, decimal,
`[::FFFF:7F00:1]`, userinfo, `localhost:8443` -- are untouched.
The doc comment claimed equivalence with `System.Uri.IsLoopback`. It is not
equivalent and now says so: two divergences remain, both unreachable from a
browser CultMesh route, and both named. An IPv6 zone id throws in `URL` and is
a host-local interface name no browser socket can dial; `file:` and `mailto:`
have no `URL` host and are refused by `openSocket`, which takes `ws:` and
`wss:` only.
The test is Soul's endpoint list with the C# answer as the expectation: 82 of
the 88 it probed, the six unreachable ones named in the doc comment instead.
Mutations: the trailing-dot refusal dropped, the scheme-and-authority shape
inverted, the backslash refusal dropped, and the non-ASCII host refusal
dropped -- each killed by the table.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The C# reference signs a transcript over a *constructed* route, and the
constructors clean on the way in: `CultMeshAuthorityRoute.Clean` trims each
protocol id, drops the blank ones, keeps one of each ordinally and sorts
ordinally; `RequireNonEmpty` trims the runtime id and endpoint; `Require`
trims the provider key id and coordinates; `CultMeshRouteCertificate` trims
the Odin key id; a blank generation becomes the runtime id and endpoint
joined by "@". TypeScript cleaned none of it and transcribed the caller's
string, so a route C# signed and accepted was refused here the moment any
field carried padding or a duplicate.
Soul's tamper loop is the evidence: C# accepted a duplicated protocol id, a
padded generation and a padded Odin key id against the committed vector's own
signature, and TypeScript refused all three. The test now reruns those, plus a
padded protocol id, a blank one, a reversed list, a padded endpoint and a
padded provider key id, against the C#-written vector's signature. A padded
Odin key id must also find its root, so the trust map keys on the trimmed id
as the C# dictionary does.
`verseId` is left alone on purpose: it is not a route field, and
`CanonicalRoute` passes the caller's string into `Canonical` untrimmed. The
test pins that too.
`protocolId ?? "cultmesh.documents.v1"` is gone. A C# route with no protocol
ids is `Array.Empty<string>()` and transcribes as the empty string; reading
the handshake's `protocolId` as a one-element list wrote bytes the reference
never writes. `canonicalSession` matches its C# counterpart the other way:
it cleans nothing on the request, because the handshake message is what the
wire carried, and only the endpoint is cleaned, because the one C# passes is
`Route.Endpoint`.
Mutations: the dedupe removed, the blank-id filter removed, the generation
trim removed, the provider key id trim removed, and the Odin key id trim
removed from the lookup -- each killed by its test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two edge alignments that have to land together, because the test for the
first asserts the messages of the second.
`String.prototype.trim` is not `char.IsWhiteSpace`. JavaScript counts U+FEFF,
which .NET does not; .NET counts U+0085, which JavaScript does not. Every
`trim()` in the shared verifier stood in for a C# `IsNullOrWhiteSpace` or
`Trim()`, so both were wrong at those two code points: a U+0085 signature was
an unsigned route to C# and a bad signature to TypeScript, and a U+FEFF
signature the other way round. `isCSharpWhiteSpace`, `trimCSharp` and
`isNullOrWhiteSpaceCSharp` are that set, and `isUnsignedCertificate` and the
signature trim now use it. The test pins all 34 code points Soul probed on
both sides against the answers .NET 10 gave on 2026-09-16.
The C# `Verify` also has three signature refusals, not one: a signature that
is not base64, a decoded length that is not 64, and a signature that decodes
but does not verify. TypeScript collapsed the first two into the third and
lost the distinction the reference draws. Split, with each message pinned by
its exact C# string. `verifyP256` keeps its own checks: it is the silent
`VerifySession` path, which returns false for all three.
Mutations: the whitespace-set site reverted to `String.prototype.trim`,
U+0085 dropped from the set, and the two new messages merged back into
"invalid" -- each killed by its test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The cut closes on its invariants. What is left is the gap between the C#
rules as written and as executed: the route is cleaned before the
transcript on one side and transcribed raw on the other, whitespace and
loopback are decided by different alphabets at the edges, and the browser
guard is enforced by name only. All in a final Hands batch before Cut 4
makes a TypeScript signer real.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The browser's local-development short-circuit reads "unsigned" through the
shared predicate, so the two TypeScript sides cannot disagree with each
other or with C#. The negative grep now names the two functions the fix
batch added. The generated contracts file's line-ending churn is recorded
where unassigned findings live.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The browser's local-development session-proof short-circuit tested
`!route.certificate`, while the shared verifier already treated an empty or
whitespace signature as unsigned, matching C# `Verify` (`:120`). A loopback
route carrying a blank-signature certificate therefore passed the verifier
and was then refused for lacking a provider key, where C# returns
`IsLocalDevelopment` and skips the session proof.
`isUnsignedCertificate` in `cultnet-ts/authority` is now the one owner of
that reading; the verifier and the browser both call it. A browser test pins
the local-development loopback open without a provider proof, the signed
loopback certificate still demanding it, and the remote counterpart refused
in both trust modes. The mutation runner gains two predicate mutations
(whitespace treated as signed; only a missing certificate treated as
unsigned), both killed by the existing whitespace-signature test in
cultnet-ts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both parity directions are now pinned in the repo and run in CI, the
public verifier copies its bytes, and the four edge rules match the C#
reference, with the loopback set probed rather than assumed. One scar: the
runner's multi-line anchors had never matched on a CRLF checkout, so two
earlier kill claims rested on an LF-on-disk state. One browser-side
divergence remains and is in Hands.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every test run inside the runner rebuilds `packages/cultnet-ts/dist/`
from the mutant under test, so after the final restore of the source the
compiled module still carried the last mutant. The runner now rebuilds
the package after the restore and asserts `dist/cultmesh-authority.js`
contains a sentinel the last listed mutation removes, checked against the
list itself so a reordering cannot make the assertion vacuous.
The original bytes are also written to `<target>.mutation-original`
before the first write and removed after the verified restore. A run
killed mid-mutation is repaired from that sidecar at the next run's
start, instead of by whoever notices git status.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`CultMeshAuthorityProofTests` verifies both vector files with the C#
reference, but no workflow ran it. The portability workflow's
reactive-and-resource-lifetime job is the one that already builds and
runs `GameCult.Mesh.Tests` on every push to main and codex branches and
on every pull request, on Windows and Ubuntu, so it gains one step:
`dotnet test --filter FullyQualifiedName~AuthorityProof`. The interop
workflow was not chosen: it runs on Windows only, on main pushes only,
and runs no Mesh tests. It already covers the TypeScript direction
through `npm test --workspace packages/cultnet-ts`.
CULTMESH_WRITE_VECTORS stays unset in CI; the committed files are the
contract.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`contracts/cultmesh/authority-route-vectors.json` proves C# bytes verify
in TypeScript. Nothing in the repo proved the reverse; Soul's probe did it
once in a scratch directory. It is now a committed check in the same shape,
mirrored:
- `scripts/sign-cultmesh-authority-vectors.mjs` builds the route and
session transcripts with the shared module, signs them with fixed
TypeScript-owned test keys over awkward inputs (a quic scheme with a
query, three unsorted protocol ids, non-ASCII generation text, priority
13), self-verifies, and writes
`contracts/cultmesh/authority-route-vectors.ts-signed.json`.
- `CultMeshAuthorityProofTests.AuthorityRouteVectorsSignedByTypeScriptVerifyHere`
reads that file with keys taken from the file, asserts the protocol ids
are unsorted, that `Validate` and `VerifySessionProof` accept, and that
a priority-tampered route and a replayed nonce are refused.
- The TypeScript vector test runs over both files, so a transcript change
on the TypeScript side fails here before C# could trust a stale file.
Checked negatively: one base64 character changed in the committed
signature fails the C# test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
verifyP256 handed WebCrypto `view.slice().buffer`. For a Node Buffer,
`slice` is `subarray`, so `.buffer` is the shared pool: a pooled Buffer or
a subarray of a larger allocation verified false while the same bytes as a
plain Uint8Array verified true. Copy exactly the viewed bytes.
Trust rules that had drifted from `CultMeshAuthorityProof.cs`:
- A certificate whose signature is empty or whitespace is an unsigned
route (C# `Verify` line 120): local-development loopback accepts it,
everything else refuses with the unsigned message.
- Loopback is `System.Uri.IsLoopback`: any 127.0.0.0/8 host, `::1`, the
IPv4-mapped `::ffff:127.0.0.1` (only `.1`), and the host names
`localhost` and `loopback`. WHATWG URL normalises `127.1`, `0x7f000001`
and `2130706433` the way System.Uri does. Probed on the C# side through
the public policy before the rule was written; the test pins that set.
- The root lookup runs before the validity window, so an expired route
under an unknown root is refused as untrusted on both sides.
- A policy listing one Odin key id twice is refused at first use, the
closest a plain object gets to the C# constructor throwing.
One test per rule, one mutation per rule in the runner's list. A fixture
whose protocol ids are unsorted and whose expected transcript is the
sorted one means removing the sort is now killed.
The runner now matches anchors in the file's own line ending. On an
autocrlf checkout the two multi-line anchors it already had ("route
transcript field order", "session transcript binds the nonce") matched
nothing, so the run threw at those entries instead of testing them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The verifier is sound in both directions, the second one by probe. What
can go wrong silently is the verification story: a mutant left compiled
in an ignored directory, a C# vector check with no CI owner, a public
function that reads a pooled buffer wrong, and four edge rules that drift
from the reference. One map claim about error texts is withdrawn.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One route verifier in cultnet-ts, the browser copy deleted, a C#-written
vector file, and a mutation runner with a control. Soul is on it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One mutation per rule the cut map names, applied to the source bytes and
restored from the original bytes with a digest check; a no-op control
runs first through the same write path. A survivor is a rule no test
pins and fails the script.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cultmesh-browser now verifies routes and provider session proofs through
cultnet-ts/authority. Its own verifyAuthorityRoute, canonicalRoute,
canonicalSession, canonicalFields, verifyP256, isLoopbackEndpoint and the
two base64 helpers are gone; the six route and trust types are re-exported
under their old Browser names as aliases of the shared types. randomNonce
stays: it is client behaviour, not verification. All 17 browser tests pass
unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cut 1 of the TypeScript QUIC realtime plane: one TypeScript
implementation of Odin root lookup, the validity window, the canonical
route and session transcripts, P-256/P1363 verification, loopback
detection and the protected-scheme rule, published as
`cultnet-ts/authority`. Browser-safe by construction: WebCrypto,
TextEncoder, atob/btoa, URL, DataView; no node: import, and a test that
says so.
The protected-scheme rule is the C# rule (wss, https, any scheme
containing "quic") rather than wss-only, so the module is not wrong for
QUIC routes; the browser client still refuses non-ws(s) endpoints itself.
The transcript bytes are pinned by contracts/cultmesh/authority-route-
vectors.json, written by the C# reference under CULTMESH_WRITE_VECTORS=1
with fixed test keys and otherwise re-verified by both runtimes. The
vector carries a route certificate and a provider session proof.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The deploy tarball is a git archive, so committed files are the only ones
that reach Yggdrasil without a second mechanism, and the Windows pair is
already committed for Unity. Four binaries, about eight megabytes, with a
manifest and digests beside them. No operator question is open on this map.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The deploy tarball is a git archive of two package directories, so only
committed files reach Yggdrasil and cultmesh-ts does not ride at all
today. Under C the binding resolves binaries from an environment variable
or the package's own native directory, a workflow builds the Linux artifact
in a debian:13 container and attaches it to a release with a manifest and
digests, and the operator smoke ships its own archive. Registry publication
is parked with its seam named. One question remains: whether the four
binaries are committed or fetched at deploy time.
The corrected npm premise is recorded in the operator's words: reachable,
publication deferred by choice.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ruling A assumed a publish job whose token is minted by logging in to
npmjs.com, and the operator's subnet is blocked there. That is also why the
existing cultcache-ts publish job has never run. Consumers keep file: paths;
the binaries ship in the CultLib tree the deploy path carries; the Actions
build attaches the Linux artifact to a release. Cut 6 and the lines of Cuts
4 and 7 that assumed a registry are flagged for remapping.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
All three recommendations were taken: unscoped cultnet-ts and cultmesh-ts
with only the native platform packages scoped; the Linux artifact built by
GitHub Actions in a debian:13 container and smoked on Yggdrasil; the lookup
source port plus a static source now, with the Node Odin rendezvous parked.
The status header now says no operator question is open and cut 1 has not
started, so a reader of the header alone knows where the campaign stands.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Seven cuts against main at f2cd2eb4, with the largest liability priced first
rather than discovered later. The native binding was the open question: a probe
loaded the committed msquic.dll through koffi under Node 24, drove a handshake
on MsQuic's own threads and closed cleanly, with no addon build and no toolchain
at load time. It is one cut, not the campaign.
The consumer audit found no package at zero external consumers and every
external consumer on a file: path rather than the registry, which is why
registry naming is an operator question rather than an assumption.
Two corrections against the target are recorded in the map rather than absorbed
silently: a cited transport filename that does not exist under that name, and a
registry path the target assumes for cultmesh-ts that has never existed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eureka target for giving cultmesh-ts a QUIC realtime provider and consumer,
byte-compatible with GameCult.Mesh.Quic and the Unity native MsQuic connector.
First consumer: the StreamPixels service publishing to its Unity overlay.
Operator rulings 2026-09-16: overlay character, subathon window and scene
intents travel as QUIC state frames and the costume catalog as an HTTPS
content artifact; shared TypeScript route verification moves to a
browser-safe module; Windows x64 and Linux x64 only; datagrams out of scope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Corrects the 1.0.58/1.3.0 release. Rebuilds the tracked plugins with
scripts/build-unity-package.ps1 -UpdateTemplate so they carry "" as the
unset CultRecordRef wire form in every position (nil still reads as unset)
and the edit-start Record snapshot. The native QUIC connector changes once
to its /Brepro form; later rebuilds reproduce it byte for byte. The Studio
package moves to 1.3.1 and depends on cultlib 1.0.59.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b85a828 changed x86_64/gamecult_mesh_quic_native.dll with no native
source change. The two copies differ in six bytes: the PE header
TimeDateStamp and the debug directory timestamp. The MSVC link stamps the
build time, so every -UpdateTemplate run rewrote the committed DLL.
/Brepro on compile and link replaces the timestamps with content hashes.
Two clean builds from an emptied build root now produce identical bytes,
and the DLL embeds no build path. The committed DLL changes once to the
reproducible form; after that the release byte check covers it like the
managed plugins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BeginEdit cloned Document eagerly but Record lazily from record.Document,
so a cache change in place before Record's first read made the two copies
come from different states. BeginEdit now serializes the document once and
deserializes Document from those bytes; Record deserializes the same bytes
on first read. Record is the state the edit began from, and costs one
deserialize only when a drawer reads it, with no second serialize.
Each commit still spends the edit, so a slider drag begins an edit per
frame; that serialize was already paid for Document, and Record adds a
deserialize only in frames where a drawer reads it.
The new test mutates the cached document between BeginEdit and the first
Record read; it fails on the lazy clone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
452f928 made nil the unset wire form, including dictionary keys: a
Dictionary<CultRecordRef<H>, float> holding the empty key encoded it as C0.
Python msgpack refuses nil map keys under strict_map_key, its default, so
cultcache-py cannot decode such a record. Nil cannot be the one canonical
form; "" can, and every runtime reads it.
The writer always writes "". The reader still takes nil as unset, because
452f928/b85a828 stores and Aetheria's Cut 9 import wrote nil; such a record
converts to "" on its next save and is byte-stable after that. The
store-composition contract names "" for value and key position, requires
readers to accept nil, and tells other runtimes to write "".
The tests pin payload bytes, so each claim is consequential: an unset ref
writes "", reloads through a fresh cache and resaves byte-identical; a
nil-written legacy payload reads as unset and resaves as ""; a ref-keyed
dictionary with an empty key writes "", reads back unset and resaves
byte-identical. The nil writer fails every byte assertion, a reader taking
nil as a set key fails the legacy pair, and a reader taking "" as a set key
fails the read-back assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rebuilds the tracked plugins with scripts/build-unity-package.ps1
-UpdateTemplate so they carry the unset-ref wire fix and
CultInspectorEdit.Record. The Studio package moves to 1.3.0 for
CultInspector.Record and depends on cultlib 1.0.58.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A drawer saw only its value; Aetheria's schematic-shape drawer needs sibling
members of the owning document (texture path, aspect, hull tints).
The model owns the seam: CultInspectorEdit.Record is a second copy of the
stored document as the edit began, cloned on first read and kept for the
edit. The Studio lowers it as CultInspector.Record, so a runtime CultUI
panel gets the same seam from the same edit. The Draw signature is
unchanged, so existing drawers keep compiling.
Why not hand drawers the edit copy the Studio already draws: that copy is
what Commit upserts, so a write through it would be saved without passing
through the returned value, GUI.changed, or read-only metadata. The record
copy is read by nothing, so writing to it saves nothing and changes neither
the edit nor the cache. It does not show edits other drawers made earlier in
the same frame.
The Studio's separate document-type field collapses into the held edit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The formatter wrote default(CultRecordRef<T>) as nil but read nil back as
"", so re-upserting a loaded record changed its bytes (0xC0 became 0xA0).
One in-memory unset value: CultRecordKey.Value is never null, so
default(CultRecordRef<T>) equals a ref built from "". One wire form: an
empty key writes nil, and nil or a legacy "" reads back as default.
Load then save is byte-stable; a store holding "" converges to nil on its
next save.
Rust, TypeScript and Python have no reference type: a reference member is a
string in the consumer's own payload, so there is no sibling codec to align.
The store-composition contract now names nil as the one unset wire form.
The new test loads a record with an unset ref through a fresh cache,
re-upserts and flushes, and requires identical payload bytes; it fails on
the old formatter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and
returns float. Unity.Mathematics' hash returned a well-mixed uint, and
seeding from the float hash collapses: a 100x100 position grid gave 1625
distinct values in Aetheria's zone generator.
- math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4)
follow Jarzynski and Olano, "Hash Functions for GPU Rendering"
(JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32.
pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the
constants checked against pcg-c's pcg_variants.h.
- CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit
patterns in int3/int4. The float overloads hash IEEE-754 bits via the
HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at
0, the paper's route for 2 -> N inputs.
- The HLSL mirror gains the same functions, written over scalar uints
so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean)
and in C#. HlslSourceCompatibilityTests now compares int and uint
arguments and results by value; a wrong shift in the mirror's pcg4d
gives 208 mismatches.
- Known-answer vectors come from an independent Python transcription.
pcg3d separates a 100x100 half-unit grid (the full int3). One
component alone has one birthday collision there, as a 32-bit value
can. The same grid on the integer lattice has none.
- design.md states that the hashes are integer-exact in HLSL and on the
CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does.
- Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dxc 1.9.0.5402 lowers float2/3/4 normalize(x) to x * Rsqrt(Dot(x, x)),
and DXIL.rst defines Rsqrt as 1 / sqrt(src). A zero vector normalizes
to NaN and a NaN component poisons every component. CultMath divided
by max(length, 1e-20), which returned 0 for a zero vector and a huge
finite component beside a NaN one.
- math.normalize(float2/3/4) is now value * (1 / sqrt(dot(value, value))).
- The HLSL mirror drops cultmath_normalize and its epsilon constant: it
now equals the normalize intrinsic, and nothing consumed it.
- HlslSemanticsTests pins zero and NaN normalize, the rsqrt bit pattern,
and zero length/distance. design.md states the rule and notes that
DXIL.rst's Rsqrt special-value table is a copy of Round's.
- GameCult.Geometry's iso-surface gave degenerate triangles (an inside
vertex on the isovalue) a zero normal through the old epsilon. It now
guards them explicitly, with a test.
- quaternion normalize keeps its identity fallback: quaternion is not
an HLSL type.
- Bump to 0.2.2 and rebuild the tracked Unity DLL and pdb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bump the package and assembly version and rebuild the tracked Unity
DLL and pdb from a deterministic build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aetheria Cut 8a needs int2(float2), clamp(int, int, int), and
frac(double). Fill them in CultMath as dxc compiles them:
- generate-swizzles.ps1 emits same-size conversion constructors and
math functions between float, int, and bool vectors. dxc lowers
float to int as fptosi (toward zero; NaN and out-of-range undefined),
numeric to bool as != 0 (fcmp une, NaN is true), bool to numeric as
0 or 1. floatN(int) splat keeps floatN(0) from going ambiguous
against floatN(intN).
- Int and int-vector clamp are min(max(x, a), b), matching dxc's
IMin(IMax(x, a), b); inverted bounds return b.
- frac(double) keeps double precision. dxc demotes double frac to
float, as it does exp and lerp; CultMath's existing double overloads
already diverge that way, and design.md now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bumps org.gamecult.cultlib to 1.0.57 and rebuilds its managed plugins from
main, so the Unity package carries the store-routing CultCache (one home
store per type, AddBackingStore(CacheBackingStore, Type[]), atomic commit)
instead of the Sep 3 assemblies. Install docs point at the new tag.
The build script now publishes without Source Link or the source revision,
with CI path mapping and cleared Release intermediates. The previous DLLs
named the commit and the worktree they were built in, so committed plugins
could never match a fresh build of the commit that holds them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings Cuts 1-6 of the Aetheria CultCache migration (one home store per
type, fixed routes, atomic commit with compare-exchange, parked SoA) and
CultMath 0.2.0 onto main, which had moved on with the CultNet contract
consolidation. The two lines touch disjoint files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings codex/cultmath-hlsl-parity (d4c43ed) into the Cut 6 branch so Aetheria's
CultMath adoption and the CultCache Studio land on one line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IsInitOnly reads the IsExternalInit required modifier, which is unverified on
Unity's Mono. Where it is not reported, init members show editable; reflection
assigns them anyway, so edits still round-trip. No code change: no cheaper
robust check exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unity's Quaternion showed eulerAngles beside x/y/z/w, Rect about eleven rows
over four values, and Bounds center/extents/size/min/max: settable properties
alias the fields they wrap. A struct with public fields now shows only those; a
struct without shows its settable properties, so record structs and CultMath
matrices (properties over private rows) still get clean rows and vectors show
fields only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The window lost its disabled state when record creation moved into the model:
Add and Create were enabled for abstract types and threw on click, and
CreateElement(type, type) on an abstract [Union] type threw "X is not a choice
for X". CultInspectorModel.CanCreate is now the one rule CreateDefault and the
window both read, a slot's own type is always a choice, and an abstract one
returns the "abstract or no parameterless constructor" notice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An exception thrown inside a `when` filter is swallowed as false, so a manifest
re-read failing mid-File.Replace turned a race that should retry into a thrown
torn-page error. The re-read now runs in the handler, and a failed re-read
counts as moved. Parallel.For's AggregateException is unwrapped where the pages
are read, so the page's own failure propagates and the torn check no longer
walks wrappers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mirror compiles under using static CultMath.math, so it proves composition,
not intrinsic rules; HlslSemanticsTests alone pins those. design.md also states
where FMin/FMax/Saturate turn NaN or infinity into numbers and tells callers to
check isnan first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dxc lowers double min/max to dx.op.binary.f64 opcodes 36/35 (FMin/FMax),
the same operations as float, so Math.Min/Math.Max (NaN-propagating) was the
wrong rule. Double min/max now use the float formulas, with NaN and signed-zero
assertions beside the float ones. Tracked Unity DLL rebuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The window decided whether a record type could be added
(GetConstructor(Type.EmptyTypes)), made it with Activator.CreateInstance,
and matched records to a type with IsAssignableFrom twice, duplicating
RecordCandidates.
Add and Create now ask the model's CreateElement, and its notice is the
refusal. The record list and the per-type counts come from
RecordCandidates. The window only renders.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Key identity serialized a key with the options of the key type's own
assembly, but the store writes it inside the document with the document
assembly's options, [CultCacheFormatterResolver] resolvers included. A key
that failed to serialize fell back to comparison by ToString.
The inspector codec now takes the owning document type, ReplaceKey and
FreshKey take it, and a key serializes as its declared key type under that
document's options, the store's own path. A key the document cannot
serialize is refused with a notice; the text fallback is deleted.
The comment and README said "the dictionary's own comparer"; it is the
rebuilt dictionary type's default comparer, and now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The copy was dropped only when a claimed drawer threw. A built-in drawing
exception (BuildDictionary, SetValue, a throwing constructor, Clone) or an
ExitGUIException escaped OnGUI with the copy alive, and the next change
committed the half-made edit.
The window now takes the copy out of _edit before drawing and puts it back
only when the document drew to the end with no drawer failing. Every
abnormal exit, ExitGUIException included, leaves the next frame to clone
afresh from the cache.
ExitGUIException is dropped too, not exempted: Unity's pickers exit on the
click that opens them and deliver the pick in a later event, which lands
on the fresh copy and commits normally.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unkeyed struct rule found auto-properties by the compiler's
<Name>k__BackingField name and marked the whole struct Unsupported when
one was get-only or a field was readonly. Record structs fell out with a
generic reason, and settable or init properties beside a field were
silently hidden. Editing is in place, so the lock protected nothing.
An unkeyed struct now shows its public fields (readonly ones read-only)
and its public properties that have a setter (init-only ones read-only).
Get-only properties are computed or kept by the in-place edit, so they are
not rows; vector swizzles stay out of the inspector.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A missing or hash-failing page under an unchanged manifest is corruption,
not a writer mid-commit, but every InvalidDataException was retried five
times and reported as "did not settle" with the real cause buried. The
retry now also requires the manifest bytes to have changed; otherwise the
corruption error is thrown on the first attempt. A vanished page directory
is a race under the same condition.
The reload test's writer now commits under every unleased read, so it only
settles if the retry takes the lease that commit created.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SequenceEqual on floats treats -0 as 0. The mirror test now compares
SingleToInt32Bits, with any NaN equal to any NaN. Its summary and
design.md now say the test proves CultMath.hlsl's text computes the same
float32 results as C# math on the CPU, not that a GPU agrees: driver sin
precision alone makes cultmath_hash and value noise differ on hardware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>