C#, TypeScript, Python, Rust and CultMesh refuse an unknown
cultcache.store.* header and a fifth record slot, naming the version or the
key and schemaId. The silent slot-skipping and C#'s ignored header are
deleted, and CultMesh's second record decoder is gone. Shared vectors are in
tests/vectors/document-variants-c0/. v1 stores stay byte-identical. At base,
the single-file commit's read-modify-write rewrote a v2 store as v1, which
lost data; it now refuses.
Soul passed it. Cross-runtime interop was run and passes. Three C#
Caching tests fail on Linux at both base and tip: they are Windows
share-mode tests (a FileShare.Read handle blocking delete/replace), which
cannot pass on Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Rust runtime gains the reference's content chunk messages on the raw
path, CultMeshCdnArtifactManifest with wire-ordered metadata,
normalize_hash, pack_content, validate_manifest,
answer_content_chunk_request (every bad request is answered, byte
identical to the reference server) and fetch_content (one owner of
receiving-side verification; refuses zero-size floods and unnormalised
hashes before any request). Vectors run both ways between C# and Rust.
The operator ruled Q-BP1 A: chunks travel on the session already held.
This is the explicit Rust content path until an authenticated connector
exists.
Soul passed it after two fix batches. cultnet-rs: content 24, cultnet 53,
rudp 7, lib 110. cargo-mutants on content.rs misses 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A C# caller that leaves ChunkHash, MessageId or RecordKey null writes msgpack nil; the
reference reads it back as null, treats it as blank and answers found:false. Rust now
decodes nil as blank in content chunk requests, so those requests are answered with the
reference bytes instead of dropped. Three nil vectors from the reference server are
judged by decoding them in Rust. Record two content-plane limits in the parity scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The function took state and lease digest from the caller, so anyone holding the
authority could sign active without reportHealth or assertWriteLease. It is now
private to the signer. Test: the subpath and root do not expose it. Docs:
waitForWriteLease throws out of publish if health is already non-warming;
MAX_RECENT_WARMING_PROOFS=64 is a recorded limit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`cultnet-ts/idunn-runtime` exposes the signer and loader alone. Web apps import
it instead of the package root, which also carries the Odin publisher and
node:dgram. The root export is unchanged. Test loads both under a stubbed
dgram: the subpath loads, the root fails.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The loaded authority is deeply frozen (bind, health contract, Expected,
Activation, identities), so a caller cannot steer what the signer signs; the
typed Buffers that could not be frozen are dropped (Expected canonical bytes,
Activation public key kept as hex only for the load-time comparison). A second
createIdunnRuntimeSigner on the same authority throws, so two signers cannot
emit sequence 1 under one runtimeInstanceId.
Tests: stateful set pinned against Rust (warming, active, degraded), lease
accepted for a route-answered warming, refused for another warming and another
target, one signer per authority, frozen authority.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The signer holds the last reported health, starting at warming, and gains
reportHealth(state). sign(detail), answerRouteObservation(request) and the Odin
publisher's publish(detail) no longer take a state, so the route answer and the
Odin publish cannot disagree and a fresh signer cannot answer active. Every
warming presence the signer signs becomes a valid write-lease target; the
publisher's recordPublishedWarming is deleted. Tests now assert every committed
stateless vector, dead ones included.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Output of the Rust generator (3c9ce38) on Yggdrasil. Every set is re-enrolled, so
all bytes change; adds service-stateful with lease.cc, the two wrong leases and
the warming/active/degraded vectors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds `service-stateful` (streampixels-service shape, state lineage set,
write_lease_required) with a Rust-issued lease.cc, warming/active/degraded
vectors bound to it, and two wrong leases (other warming digest, other target).
Each vector still passes authenticate + correlate before it is written.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The peek left snapshot slots unread and rmp-serde refused the length
mismatch, so every store fell through to the legacy decode.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A hostile manifest of empty chunks no longer becomes a request flood: fetch_content
refuses a zero-size chunk beside other chunks, and settles the content hash and every
chunk hash (normalised, non-empty) before the first request.
Every bad chunk request is now answered found:false in the reference handler's
validation order, never refused at decode. validate_message no longer refuses content
chunk requests or responses, so a blank message id or hash, a prefix-only hash and a
negative size reach answer_content_chunk_request. The answer follows the reference's
double normalisation (handler, then CreateRecordKey), which fixes the prefix-only
hash spelling to ArgumentException on 'chunkHash'. The cs vectors gain nine
reference-server answers (blank and whitespace id, blank, whitespace and prefix-only
hash, negative size, record-key case, blank record key, double prefix) and a
metadata-order manifest; Rust answers all of them byte-identically.
Manifest metadata is Vec<(String, String)> in wire order and refuses a duplicate key,
replacing the BTreeMap that re-sorted keys.
Tests pin the surviving mutants: unusable chunk sizes, the payload-length and
claimed-size halves of the chunk check, stable sort at equal offsets, the normalised
hash and canonical key actually sent, and the kind/mime defaults never trimming.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
idunn-runtime-authority.ts now loads and signs only: no Odin endpoint, no Odin
dependency check, no RUDP. createIdunnRuntimeSigner signs presence and answers
Idunn's route challenge with answerRouteObservation(request, state), a
synchronous local signature in the state the app reports. The RUDP publisher
moves to idunn-odin-presence-publisher.ts for services that declare
odin.verse-rendezvous, and keeps the Expected-endpoint check.
Deleted: the TS capability-minimum refusal (the Rust correlation records the
typed disagreement), latestPresenceDocument, the forced "active", and the
publisher's redundant health-contract and bind checks. Route validation now
accepts http, tcp and rudp as Rust does; the machine-id falls back to
/var/lib/dbus/machine-id. Tests run against the Rust-written fixtures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expected, Activation, identities and signed presence vectors from the cultnet-rs
example; the README says how to regenerate them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PersistedRecord refuses a fifth slot naming its key and schema id, and an
unknown cultcache.store.* header no longer falls through to the legacy
envelope decode that hid the cause.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete the raw_record[:4] slice and the legacy-envelope fallthrough for
unknown cultcache.store.* headers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The store reader and the inspector share one header and record-slot rule;
an unknown cultcache.store.* header no longer falls through to the legacy
envelope decoder.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real enrolled identities, a validated Expected, an Activation issued by
IdunnRuntimeActivationLaunch, bundle files through the CultCache store, and
runtime-presence vectors signed by the Rust signers and run through
authenticate/correlate before they are written. cultnet-ts tests will commit
the output.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The UTF-8 detail bound calls String.prototype.toWellFormed, which
Node 20 introduced. Node 18 reached end of life in April 2025, and CI
runs Node 24. All four TypeScript packages move together so the family
keeps one floor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
signed-daemon-health sliced detail at 512 UTF-16 units while the Rust owner
counts bytes, so 300 x "é" failed shape validation. truncateUtf8Bytes now
serves runtime presence and both daemon-health sites, replacing
truncateRuntimePresenceDetail. It makes the string well formed first, so a
lone surrogate becomes U+FFFD instead of invalid UTF-8 in msgpack. The
unsigned payload is extracted as unsignedDaemonHealthPayload so it can be
tested without a socket.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Rust owner refuses detail over 512 UTF-8 bytes; TypeScript counted
UTF-16 units, so 300 x "é" was signed and then refused, and a slice inside a
surrogate pair produced invalid UTF-8. One helper,
truncateRuntimePresenceDetail, now cuts on a code point boundary at 512
bytes and serves both the publisher and validateRuntimePresence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
content-vectors.cs-written.json is written by CultMeshContentVectorTests on
Linux, and includes the answers CultMeshLegacyRudpContentServer gives, so the
failure spellings are the reference's. tests/content.rs judges it. The
rs-written mirror follows in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ContentChunkRequest/Response join CultNetMessage on the raw path, byte
identical to the reference's field order. content.rs owns normalize_hash,
pack_content, validate_manifest, answer_content_chunk_request and
fetch_content, mirroring CultMeshCdn and CultMeshLegacyRudpContentServer.
The C# vector test is added here; the vector files and Rust tests follow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul pass 3 confirmed two documentation defects: the F6 search-radius proof
misstated 3 - 2m + 3m^2 at m=1/2 as 2.5 (actual 2.75; true min on [0,1/2] is
8/3 at m=1/3), and both math.cs and design.md cited a fabricated 203.8
chi-square statistic for float-bit hashing that Soul could not reproduce
(6+ seeds scored 7.7-30.9). Corrected the arithmetic and replaced the
unreproducible measurement with what actually pins integer hashing: the
oracle and mirror tests, and the chi-square test's real scope (the hash as
cellular calls it, not the call site itself). No test or code changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EveryMirrorFunctionMatchesCSharpMath's generic random/special-value cases
never happen to hit a point where a radius-2 cell wins (about 3.5e-5 of
points) or an integer point at or past 2^23, so an HLSL mutant of cellular's
search radius, its lower-bound prune, or the removed F2=0 guard could change
cultmath_cellular's real output and still pass this test.
Adds three targeted point cases, reusing CellularAndSminGradTests' own
fixtures so the mirror test does not carry a second copy of the construction:
- the six points from AxisAlignedRadiusTwoOffsets/FindPointRealizingOffset,
each of which realizes a different axis-aligned radius-2 offset as F1 or
F2 in the independent 7x7x7 oracle;
- four integer points at 2^24 (both signs), where distinct cells' jittered
features can round onto the same float32 value, reaching the F2 = 0 guard;
- one exact F1 = F2 tie point, the midpoint of two adjacent cells' feature
points (bit-exact-equidistant from both by construction, no search
needed).
Verified against four HLSL mutants applied to shaders/CultMath.hlsl on
scratch copies (not committed) and reverted after each check:
- `dz < 2` instead of `dz <= 2`: killed by the axis-slice point at
(-56.671272, -67.01973, -79.001).
- `dx` starting at -1 instead of -2: killed by the axis-slice point at
(62, -77.91331, -79.42451).
- pruning against f1 instead of f2: killed by an axis-slice point and both
of the 2^24 integer points (3 mismatches).
- the F2 = 0 guard removed (`grad2 = (p - c2) / f2`, unconditional): killed
by the (2^24, 2^24, 2^24) integer point (NaN vs the guarded zero).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CellularAndSminGradTests.cs changes for cut 2a-i's pass-2 findings:
- FeaturePoint and the two other independent pcg3d/pcg4d call sites now hash
int3(cell)/int4(int3(cellId), 0) to match production's integer-hashing fix
(previous commit); the oracle would otherwise diverge from cellular() and
every comparison test would fail.
- New JitterAndIdPassAChiSquareUniformityTest: jitter.x/y/z and id each pass
a chi-square uniformity test (16 bins, 15 df, critical value 37.7 at
alpha=0.001) over 200,000 cells. This is the behavioural pin for the
hashing fix; reverting to pcg3d(cell)'s float-bit hash fails it.
- Two false comments corrected to the measured facts: the
NearestFeatureWins test's claim that |component|=2 offsets are "provably
never the true nearest" is false (their lower bound is 1, not sqrt(3); Soul
measured 3 F1 wins in 2,000,000 samples), and SearchReachesEveryAxis...'s
claim of "lower bound is exactly 2" and "zero hits in 3,000,000" is also
false (lower bound is 1; Soul measured about 1 hit in 28,000).
- PrunedSearchIsBitIdentical... renamed to
...BelowTwoToTheTwentyFifth and its comment states that domain explicitly
(design.md, previous commit).
- GradientTolerance split into SminGradientTolerance (2.5e-4f) and
CellularGradientTolerance (1.3e-3f): the two functions have different
curvature (smin_grad is exactly quadratic in a band; cellular's F1/F2 are
real distance fields), so a shared 3e-4 tolerance carried almost no
headroom over either's actual measured noise (re-measured here at 5.3e-4
for smin's old eps, 2.0e-4/2.6e-4 for cellular), not the "~6e-5" the old
comment claimed. smin_grad's eps moves from 0.001f to 0.01f (no truncation
cost there; dilutes float noise 8x) with a matching widened seam-exclusion
band so p +/- eps cannot step across the seam the check at p already
avoided. cellular's eps stays at 0.0005f (a small sweep found it already
near the truncation/noise sweet spot for a real distance field). Both new
tolerances keep >5x headroom over their own measured worst case and both
still die under a 1.01x-scale mutant on the returned gradient — smin down
to 1.0005x, cellular (M9/M16-style, either gradient) down to 1.002x.
- GradientMatchesCentralDifferencesAwayFromTheF1EqualsF2Seam also excludes a
band around F2 = F3 now, not just F1 = F2: the second-nearest feature's
identity swaps there too, a genuine kink the old test could silently hit
(an unfiltered large sample found a 0.78 edge error this way, swamping the
~2e-4 the filtered test actually sees).
- AxisAlignedRadiusTwoOffsets and FindPointRealizingOffset are extracted from
SearchReachesEveryAxisAlignedRadiusTwoSlice as internal fixtures so
HlslSourceCompatibilityTests (next commit) can reuse the same construction
instead of a second copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
design.md follow-ups matching the math.cs/HLSL fix in the previous commit:
- The F6 "5x5x5 is exact" argument is rewritten with the corrected near-side
neighbour bound (choose the axis closest to its own boundary, not a fixed
axis), the m <= 1/2 derivation, and the counterexample that showed the old
argument wrong. Same text as the math.cs comment, so the two do not drift.
- "Integer hashing" now states that cellular hashes the winning cell's
integer coordinate, not its float32 bit pattern, and records the measured
chi-square evidence (203.8 over 1,000,000 cells, 15 df, critical value
37.7) for why the old bit-pattern hash failed a uniformity check.
- "Precision domain" gets a new paragraph: the lower-bound prune's bit
identity to the unpruned 5x5x5 search holds for |p| < 2^25, not
unconditionally. Above that, cell + offset itself starts to round in
float32, so the pruned and unpruned loops can visit different actual
neighbours at the same nominal offset (Soul, cut 2a-i: 180 of 1,100,000
points differ at |p| ~= 3.4e7). This is a float32 domain limit on the
equivalence proof, not a bug in the prune; the committed test's [-50, 50]^3
sweep is comfortably inside it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:
- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
instead of the integer coordinate itself. Two adjacent cells' bit patterns
share more structure (same exponent, mantissa one increment apart) than
the integers do going into pcg3d's own mixing, and that structure survived
into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
cells against 15 degrees of freedom (critical value 37.7). Both call sites
now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
as before). This changes cellular's noise output — nothing has shipped, so
that is acceptable, but it is a real behavioural change, not a comment fix.
- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
picking the near-side neighbour on a FIXED axis is not always within
sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
exclusion). The comment is rewritten to the correct argument: choose the
axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
1/2 on that axis), and the near-side neighbour on THAT axis has squared
distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
strictly below 3 since the jitter never reaches the boundary. Margin
2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
sqrt(3), only in the unattainable corner limit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker (mtp, --since 29e50ad3) found 261 survivors / 24 timeouts across the
whole repo; triaging the ones in this fix batch's own new code (cellular's
loop, its prune, cellular_unit) surfaced one real gap disguised as
collateral timeout noise from a batched infinite-loop mutant: dz < 2 (and
the dy/dx equivalents), silently dropping the +2 layer on one axis alone,
passed every existing random-sampled test. That is not the sweeps being
weak: the exactness proof's own margin (F1, F2 always < sqrt(3), radius-2
cells' lower bound is exactly 2) makes a genuine win from any single
|offset component| = 2 cell astronomically rare over uniformly random
points (measured zero hits in 3,000,000 samples).
SearchReachesEveryAxisAlignedRadiusTwoSlice constructs a real example for
each of the six axis-aligned radius-2 offsets directly: for a candidate
integer cell, build the query point that is the closest point of that
cell's own unit cube to the target neighbour's actual jittered feature.
That construction finds a hit for every offset within a couple thousand
candidate cells, deterministically. This kills dz < 2 / dy < 2 / dx < 2 (all
manually confirmed).
Also manually confirmed via Stryker's own reported mutants: the dz--/dy--/
dx-- (increment-to-decrement) mutants are genuine infinite loops, correctly
caught as timeouts. The f2 >= 0.0f mutant (disabling the F2 = 0 guard) is
killed by the existing NoNaNAtIntegerPointsNear2To24 test. The remaining
survivors/timeouts in the new code (the lower-bound prune's >>= vs >
comparison, its arithmetic terms, and cellular_unit's >> vs >>> shift on an
already-unsigned operand) are equivalent mutants: since the per-axis lower
bound is always 0 or 1 by construction, and pruning is a proven-safe
optimization that never changes cellular's output, no mutation confined to
"when does the prune trigger" can be observed by any value-based test.
Verified by hand: disabling pruning entirely reproduces the unpruned
reference bit for bit, as it must.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- smin_grad is Quilez's quadratic-polynomial smin, not cubic.
- cellular's search radius is 5x5x5 (F6), not 3x3x3; adds the exactness
proof and the pruning claim.
- id source is now pcg4d over the winning cell, not the pcg3d hash that
produced its jitter (F4).
- F2 = 0 is documented as reachable (past |p| of about 2^23), not
unreachable; removes the false "unreachable" claim and adds a "Precision
domain" note stating the float32 feature-spacing/tie behaviour honestly
(F3, minor finding).
- The struct-mirror comparison note now says it also matches field name and
type in declaration order, not just position (F2).
No entry added to THIRD-PARTY-NOTICES for the Quilez/Worley formulas: per
Self's standing ruling, a published formula is not ported code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker (scoped to this cut's diff) survived two mutants on the grad2 ternary
in math.cellular: forcing the true branch unconditionally, and loosening
f2 > 0.0f to f2 >= 0.0f. Both are real findings about the code, not the
tests: F2 = 0 needs two distinct integer cells' pcg3d-jittered feature points
to land on the exact float32 value in every component, which the jitter's
~24-bit float precision makes unreachable for any p this function is
actually called with. The spec only asks for the degenerate guard at F1 = 0;
F2's copy of it was added for symmetry and had no reachable case to defend.
Deleted the guard (grad2 is now a bare (p - c2) / f2) in math.cs and both
HLSL mirrors, and corrected design.md's invariant-8 section, which had
overstated this as symmetric with F1's guard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dotnet test found two failures where a fixed-decimal-place Assert.Equal
snapped two numbers that already agreed to within about 2e-5 to different
rounded digits, because their true difference straddled a rounding boundary
(0.824987829 vs 0.825006127 -> 0.82 vs 0.83). Both were central-difference
checks (smin_grad's own invariant-8 property test, and cellular's gradient-
matches-central-differences test), not a correctness bug in the primitives.
AssertWithinTolerance replaces the precision-based assertion for both central-
difference comparisons with an explicit absolute-difference check, which
states the actual claim (agreement within the finite-difference step's own
truncation error) without the rounding-boundary artifact. Also dropped an
unused local function (F2At) the build flagged.
Found by the third ygg-verify.sh test run on 77c3580.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Inside namespace CultMath.Tests, unqualified `Random` resolves to the
enclosing CultMath namespace's own Random struct (design.md: "Random is
CultMath's own xorshift32") before `using System;` is even considered, so
`new Random(seed)` in the new test file was constructing CultMath.Random,
which has no NextSingle(). HlslSourceCompatibilityTests.cs already works
around this the same way (`new System.Random(...)`); CellularAndSminGradTests
now qualifies every use the same way.
Found by the second ygg-verify.sh build/test run on e613ed6.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Inside the static math class, the unqualified name `float3` resolves to
math's own `float3(float,float,float)` constructor-function first, not the
CultMath.float3 type, so `float3.zero` fails to compile there (CS0119). No
prior code in math.cs used the Type.zero static-field spelling from inside
math itself for this reason. Switched cellular's zero vectors to
`new float3(0.0f, 0.0f, 0.0f)`, which is unambiguous under `new`.
Found by the first ygg-verify.sh build/test run on 3eed434.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CellularAndSminGradTests pins the rules the cut map calls out, each against a
mutant it must kill:
- smin's value never exceeds the ordinary minimum, and matches min with that
input's own gradient once |a.w - b.w| exceeds k on either side.
- smin_grad's gradient matches central differences of its own .w at random
seeded points (the formula is exact, not an approximation, so no exclusion
band is needed away from the |a.w-b.w|=k seam itself).
- A direct case falsifies blending the gradient with a constant 0.5 or with
(1 - h) instead of h.
- cellular's F1 never exceeds F2, matched against an independent brute-force
Worley search that shares only the public pcg3d hash, not cellular's control
flow.
- The true nearest feature wins even when an independent search finds it in
the (1, 1, 1) neighbour offset, the last one cellular's own loop order
visits, ruling out a "first found" or early-exit mutant.
- The gradient at a feature point is finite zero, not NaN.
- Both nearest and edge gradients match central differences away from a band
around the F1 = F2 set, where the nearest and second-nearest identity swap
discontinuously.
- id is constant across small moves that stay in the same feature's region and
differs from a distant cell's id.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>