Commit graph CultLib/tests
Author SHA1 Message Date
Meta Vens
b8dbcfe40c GameCult.Networking: RUDP socket transports drop and count strays
A malformed frame, a foreign connection id or a packet the session refuses
threw out of TryReceiveOnce. They are now counted in Stats.PacketsDropped and
the read reports progress. The client-side transport checks the connection
id before it claims the peer endpoint; the server listener drops refused
packets instead of throwing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:51:32 +02:00
Meta Vens
b9cca38dbf Cover valid surrogate pairs and a second unpaired surrogate in node ids
Stryker on the escaping showed the pair-skip branch uncovered and the run
offset unpinned; the kinds now include a valid pair, a replacement character
before a lone low surrogate, a reversed pair, and two unpaired surrogates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:49:51 +02:00
Meta Vens
7defd82c20 Pin node-id escaping, case-distinct nodes and the flat ungrouped list
Ids that differ only by %2F versus /, keys that differ only by case, and
unpaired surrogates each get a test; the ungrouped list stays flat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:00:47 +02:00
Meta Vens
4f7d8821af Pin the create-in-group node check against a value that fits another grouping
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:29:09 +02:00
Meta Vens
439efd1550 Drop the inert inherit flag from the global check and its derived-type case
[CultGlobal] is not inherited, so the flag changed nothing and no test can tell.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:16:11 +02:00
Meta Vens
30e1f3fb95 Cover ungrouped types, null strings, derived globals and id separators
Stryker survivors on the grouping diff: the no-declaration path, the
null-string identity, the inherited [CultGlobal] check and the id separator.
GroupLevel takes text and tie as optional arguments so the scalar kinds
stop passing constant strings that could be swapped without any effect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:15:03 +02:00
x
84c9a79972 Pin string case ordering and node id spelling in the grouping tests
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:52:05 +02:00
x
3d07ba0d20 Give the overriding variant its own name in the grouping test
A variant must override its name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:21:28 +02:00
x
79e9318698 Add model-owned record grouping: declaration, tree, create-in-group, ref labels
[CultInspectorGroupBy(nameof(A), nameof(B))] on a document class names the
members its record list groups by, outermost first. CultInspectorModel owns
the rest: GroupingOf validates the declaration (a refused one gives a flat
list and a notice), GroupRecords partitions RecordCandidates by value
identity with occupied values only, CreateInGroup builds the record a node
holds, RecordRefLabel owns None and Missing <key>. A variant groups by its
resolved Document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:09:25 +02:00
x
8ad0691bdd Declare the label fixture's key on its base member
The registry requires an override to repeat or omit the base declaration's [Key].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:03:28 +02:00
Meta Vens
a6382475e4 Collapse the ref-candidate predicate and read inherited member attributes
RecordCandidates and the new IsRecordCandidate share one rule, so a
picker's candidates and a drop's acceptance cannot disagree. Member
attributes are read with Attribute.GetCustomAttributes(member, true):
PropertyInfo.GetCustomAttributes ignores inherit, so a base abstract
property's inspector attributes never reached the override.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:55:41 +02:00
Meta Vens
cbd3b4f3ce CultNet: a withdrawn hop subscription is refused with a typed error and its rows are withdrawn
Reconcile's withdrawal sent an untyped error with no subscription id and left the
peer holding the rows the subscription had delivered. It now sends removals for
those rows, then a variant_unsupported error whose details carry
field "subscriptionId" and the id (the existing details slots; no new wire key).
The subscribe and per-change paths use the same code. The error schema and the
Rust CultNetErrorCode accept variant_unsupported, so a Rust peer decodes it
instead of refusing an unknown code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:12:20 +02:00
Meta Vens
2eec436850 Variants C1: the index rule is linear, a merge judges only when a variant is involved, duplicates are deterministic
- RefuseVariantIndexSharing built its holder list per landing record by scanning
  every landing record, so opening a store with one variant was quadratic. It now
  groups what lands once and looks each (index, value) up once. Pinned by a count
  of index-accessor reads at two sizes, not a clock.
- JudgeMerge decoded every unseen disk record before Resolve could exit early,
  so a plain-only store failed a conditional commit on another writer's schema it
  does not register. It now judges only when the batch, the disk (by the record's
  variant header, never its payload) or this cache holds a variant.
- Plain duplicates find the holder with the smallest key (ordinal), so the choice
  is the same live, after a flush and after a reopen. The comment claiming "most
  recently indexed" was wrong across reload.
- A dropped slot is reported once per record, not once by the schema diff and
  again by the variant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:12:20 +02:00
Meta Vens
a0baa542d0 CultNet: refuse hop edges whose citer is a variant; withdraw a refused hop subscription
An edge carries the reference payload of its citer's resolved document, and the
citer is never a selected row under `cited`, so a variant citer slipped past the
selected-row refusal. Edges are now checked by the same rule (typed
NotSupportedException naming the variant), on the paged and the whole-set paths.

Reconcile refused a hop subscriber whose selection selected a variant without
withdrawing it, so it was told again on every store change. It withdraws as the
subscribe path does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:20:48 +02:00
Meta Vens
282e337c2c Variants C1: the held index tells the truth; a merge is judged on what the file will hold
The held index kept one key per value, last writer winning, and RemoveKey dropped
a value outright. Two consequences: removing or changing one of two plain
holders left the other unfindable, and the variant index rule judged a variant
against that lossy view, so a variant could land on a value a plain record still
held and the store then refused to reopen.

- _indexes maps a value to the keys holding it, oldest first. Index, Unindex and
  the removal maintain the set. GetByIndex finds the most recently indexed
  holder of a value, so removing the winner finds the other (behaviour change for
  plain duplicates: it used to find nothing).
- One rule, RefuseVariantIndexSharing, serves both write orders. It reads the
  truthful index minus what the commit replaces or removes, plus what it lands.
  ResolveOne no longer judges indexes; ClaimIncomingIndexValues, HeldByVariants
  and RefusePlainSharingWithVariant are gone.
- A conditional commit merged onto the file is judged by the cache's own
  resolution (CacheBackingStore.Judging) on the merged set: records other writers
  added, records they removed, and the batch. Another writer's variant sharing a
  value with the batch, or a base removal that strands another writer's variant,
  is refused with the named refusal instead of writing an unopenable store.
- Shedding is one decision at load. ToStoredDocument resolves the schema once and
  returns the report with it; a variant's override of a slot the type lacks is
  dropped from the delta there, so flush needs no predicate and every store
  reports through the same surface (ReportVariantSheds and OverridesTheTypeHas
  are gone). The directory store refuses variants and loads through the same call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 09:20:47 +02:00
Meta Vens
4f632fbf46 Drive the hop-subscription test through the database write path
Batch commits on the cache do not reach the database's change stream, so the subscriber's citers were written with database.PutAsync; the variant goes in through UpsertVariantAsync. On the previous source the second subscriber stops receiving after the variant exists (red); it now keeps receiving.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:42:32 +02:00
Meta Vens
b572823d0d Refuse variants after the selection is applied, and never tell a subscriber about the removal of one it was only refused
The selection paths (SelectPage, SelectAll) refused every stored variant before the selection ran, so once any variant existed a hop-bearing subscription threw out of its change handler and went silent. They now refuse only a variant the selection selects; the subscription server delivers that as a typed error and keeps its projection. The database server no longer sends removed for a key the subscriber only ever received as an error. A flatten racing the delivery check is recorded in a comment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:28:24 +02:00
Meta Vens
354f9743e6 Refuse a plain write that shares a variant's indexed value; a flush sheds overrides the type lacks
The index rule is about the commit's result, so it now runs for every commit that changes an indexed value: a plain record landing on a value a variant of the post-commit set holds is refused naming both keys and the index (the held index is never trusted; a plain write had already overwritten it). Plain-vs-plain is untouched. A variant sheds overrides of slots its type lacks when it is flushed, as a plain record sheds the member, and the load's report names the slot when the schema itself never had it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:28:24 +02:00
Meta Vens
5d6847e220 Align the C1 tests with the unique-index rule and fix the live-subscription fixture
Hand-written and directory-store variants now override their indexed value or
name as the rules require; the subscription test writes its variant through a
committed batch so the file holds it, subscribes by key, and reports what it
saw when it times out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 07:04:01 +02:00
Meta Vens
2845d3591b Regenerate the C1 variant vector: the variant overrides its indexed code
The unique-index rule refuses a variant that inherits its base's [CultIndex]
value, so the vector's laser-big now carries a code override. Written by the
code under test through the production path; the other runtimes still refuse
it by version and by the laser-big key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 06:59:29 +02:00
Meta Vens
c66d79e248 Variant overrides follow the plain-record outcome; bad writes and shared indexes are refused
F3: an override drifts exactly as the same member would on a plain record. A
slot the type dropped is ignored and reported by the store's schema resolution
(LastSchemaMigrationReports), the one surface a plain record uses; a value that
no longer decodes refuses the load, naming the variant and member. The second
report surface (VariantMigrationReports, the per-variant report map, the
catalog slot-name plumbing) is deleted.

F4: an override for a slot the type does not have, or a value that does not
decode as the member, is refused at write. Drift is for type changes after the
fact, not for bad writes. F5: two overrides of one slot are refused.

F2: a variant that shares an indexed value with another record is refused,
naming both keys and the index (the R6 rule extended to [CultIndex]); plain
record duplicate handling is unchanged.

F7: a flatten now resolves against the same batch, so a base edited in that
batch is in the flattened record; and the directory store refuses to load a
variant page as it refuses to write one.

The C1 vector's big variant now overrides its indexed code, as the unique-index
rule requires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 06:58:51 +02:00
Meta Vens
cdb8ee5dcc CultNet refuses a variant on both live-change paths, loudly, by key
A load or write that landed a variant reached CultNet subscribers as a plain
"added" record carrying the full resolved payload; a later base edit published
only the base, so the subscriber's copy of the variant went stale. Under Q6
CultNet carries no variants until its own cut. Both live paths now refuse a
change whose key holds a variant, after selection and authorization match, and
send the matching subscriber a CultNetErrorMessage naming the variant and its
base. Refused loudly rather than omitted: an omission is a silent hole in the
subscriber's view, and the snapshot path already refuses by throwing.

Tests: a real load through a real RUDP subscription never delivers the variant
as a record and delivers the typed error; the database server's change builder
refuses the same change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 06:58:51 +02:00
Meta Vens
de65c82768 Merge remote-tracking branch 'origin/main' into hands/variants-c1 2026-09-29 06:48:32 +02:00
Meta Vens
b93934f6b4 Pin the variant rules a mutant survived or that lost their only test
Reader refusals (v1 header over a variant, payload on a variant, unknown op), a
load that finds a variant flattened by another writer, a re-resolution reaching a
read-only cache, a base removed in the same batch as its new variant, and
plain-record-to-variant refused without the name rule masking it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 04:21:59 +02:00
Meta Vens
1ddf6e0d56 Mark the three share-mode tests Windows-only
They hold a FileShare.Read handle and expect a delete or replace to fail;
Linux unlink and rename ignore open handles, so they can never pass there and
showed as three standing failures. [Platform("Win")] reports them skipped with
a reason on Linux and keeps them running on Windows. No Linux failure injection
exists: the store names its temp file with a fresh GUID and the suite is
likely root in the verify container, so read-only directories do not fail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 04:17:31 +02:00
Meta Vens
fc78b9ad2b Test the C# variant path and add the C#-written variant store as a shared vector
VariantTests drive the cache batch API and the real single-file store. The C1
vector was written by the code under test through that path; C# reads it and
rewrites it byte for byte, and the other runtimes' tests read the same bytes next.
The C0 tests that expected C# to refuse a v2 store now expect what C1 does with
it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:52:35 +02:00
Meta Vens
e382bb4e20 Merge variants C0: every reader refuses what C1 will write, by name
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>
2026-09-29 03:52:07 +02:00
Meta Vens
5b538aaf07 Answer chunk requests whose string fields are nil
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>
2026-09-29 03:29:41 +02:00
Meta Vens
66bd07b941 Pin the legacy-layout and missing-header refusals
The legacy catalog layout reaches CultMesh's fallback reader, and a nil header
was the only defaulted header; each gets one vector or test. The C# store
tests catch the refusal where the store attaches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:35:57 +02:00
Meta Vens
16432d4b1e Fix the BP-1 findings in the cultnet-rs content plane
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>
2026-09-29 02:12:43 +02:00
Meta Vens
98ed042fcc C#: refuse unknown store headers and extra record slots by name
Delete the trailing Skip loop in the record reader, the ?? v1 header default,
and the mesh reader's copy of the record decoder. The single-file store and
CultMesh single-file reads accept cultcache.store.v1 only. Persistence-format
and parity docs state the rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:04:33 +02:00
Meta Vens
2cb694d612 Pin the C0 refusal vectors shared by every runtime
An unknown store header, a record slot beyond four with a full payload, the
v2 variant record C1 will write, and a v1 store written by the base-commit
Python writer. Every runtime's tests read these bytes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 02:04:33 +02:00
Meta Vens
29c3e4c2b5 Add the content plane to cultnet-rs: chunk messages, pack, answer, verified fetch
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>
2026-09-29 00:48:13 +02:00
Meta Vens
d64de1b69d fix: reject non-finite isoValue and origin in Extract
samples and cellSize were validated but isoValue and origin were not:
iso = NaN or +inf silently returned an empty mesh, and origin = NaN gave
NaN positions. Extend Extract's existing top-of-method validation authority
to reject non-finite isoValue and non-finite origin components with
ArgumentException, matching the existing style, with tests for each
component.

Overflow at extreme magnitudes (cellSize ~3e38, or finite samples whose
interpolation overflows to inf) is a separate, float-domain limit rather
than a validated-input error; documented in the XML doc and provenance
notes, deliberately not range-checked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:31:44 +02:00
Meta Vens
b4db5cb6ed test: cover CultGeometryGridEdge.Equals(object) behaviourally
Mutating obj is CultGeometryGridEdge other && Equals(other) to && true
passed the suite: nothing exercised object.Equals directly with a mismatched
X/Y/Z/Axis, null, or an unrelated type. Add a behavioural test that checks
each component independently, rejects null and other types, and agrees with
the typed Equals and GetHashCode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:29:00 +02:00
Meta Vens
6a4b0ed21f Add tests for surface-nets F1/F3/F4/F6/F7 mutation findings
- Fixed An_extremely_small_edge_delta_falls_back_to_the_edge_midpoint (renamed
  A_tiny_but_normal_edge_delta_...): it pinned the wrong value (1/6) for the
  fallback that F3's production commit deleted; now asserts the exact
  interpolated value, hand-derived in double precision independently of
  Interpolate. Added a genuinely subnormal-delta sibling test.

- Added two more vertex-placement fixtures (crossing counts 4 and 6) to kill
  the sum/count -> sum/3 mutant, each with a hand-derived expected mean.
  Counts of 1 and 2 from the finding are not used: a single cell's 12 edges
  form the cube graph Q3 (3-regular, 8 vertices), whose edge-cut size for any
  corner sign pattern is never 1 or 2 for any nonzero cut (minimum nonzero
  cut is 3, already covered by the existing symmetric/unequal fixtures) -
  see the fork note in this cut's report.

- Added three corner-order pinning tests (one per axis), each asserting the
  exact welded vertex at each of Quads[0..3] for a chosen edge against a
  hand-computed position, including the axis-1 v1/v3 swap that F4's fixed
  OrientationSign now performs. These kill the axis-0-mirrored, c0/c2-swapped
  and equivalent per-axis corner-order mutants.

- Added four degenerate-quad orientation tests (sample exactly at iso,
  large-origin/tiny-cellSize sphere, octahedron at iso, tiny-tolerance
  sphere) asserting every shared undirected edge is traversed in opposite
  directions (net zero), via a new AssertEdgeDirectionsBalance helper that
  also covers the 4-use case. Restricted the existing
  Every_quad_winds_outward_from_inside_to_outside test's geometric check to
  non-degenerate (non-zero cross product) quads, per spec.

- Added Grid_edge_inequality_is_the_negation_of_equality (F6: != is compiler-
  mandated alongside ==, and needed its own direct test) and
  A_non_finite_sample_is_rejected (F7: NaN and +-Infinity samples).

All 38 SurfaceNetsTests pass; the 7 pre-existing GeometryDocumentTests
failures from CultLib commit e420410 are untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:30:14 +02:00
Meta Vens
af39e05c21 Kill mutation survivors: degenerate fixtures, missing axis and edge tests
A --since:29e50ad Stryker pass found real gaps, all inside this cut's own
files:

- vertex-placement and normals fixtures anchored a vertex at the world
  origin, so a mutated (+ instead of -) or dropped cellSize multiply was
  invisible against a zero operand; moved them off the origin;
- the cube normals test only checked length, and three orthogonal
  +-2-length face contributions summed to the same length no matter which
  one a mutant negated; now checks the exact expected direction per vertex;
- the open-boundary rule only had an X-axis test, leaving TryGetQuadCells's
  Y- and Z-axis guards (and TryGetOtherEndpoint's Y/Z cases) uncovered;
  added the Y- and Z-axis twins of the existing plane test;
- the 1e-20 near-zero-delta fallback in Interpolate was never exercised;
  added a fixture with an edge delta of ~3e-21;
- CultGeometryGridEdge's Equals and ToString had no direct test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:38:15 +02:00
Meta Vens
dcfb267ac7 Fix the checkerboard fixture's open-boundary contamination
The 4x4x4 pure-checkerboard field had no fully-interior cell: every active
cell bordered the array edge, so some crossing edges lost a quad to the open-
boundary rule and edge usage came out odd (observed: all-3) for reasons
unrelated to the ambiguity the test means to exercise. Wrapped the
checkerboard in a one-sample uniform-outside margin (5x5x5 grid, checkerboard
only on samples 1..3) so every crossing edge under test keeps all four
surrounding cells in range, isolating the even-but-not-2 claim from
open-boundary effects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:15:28 +02:00
Meta Vens
7a76b89a94 Fix expression-tree-incompatible test lambdas for FluentAssertions OnlyContain
Two spots failed to build: a method-group reference to float.IsFinite and an
`is 1 or 2` pattern inside a lambda that FluentAssertions compiles as an
Expression<Func<T, bool>>; expression trees can't contain pattern-matching
`is`, and a method group isn't implicitly convertible to an expression tree
either. Rewritten as explicit lambdas with `==`/`||`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:10:19 +02:00
Meta Vens
09a85edc47 Test surface nets naming, orientation, welding, and normals; scope Stryker to it
Adds SurfaceNetsTests.cs and QuadNormalsTests.cs covering the sign
convention, edge naming, outward winding, vertex placement (midpoint and
unequal-crossing means, origin/cellSize), closed-sphere welding and
Euler characteristic, an ambiguous checkerboard cell's even-but-not-2 edge
usage, open-boundary quad suppression, determinism/ordering, and
face-weighted normals (sliver-vs-large-quad area weighting, unit cube
normals, zero-area/untouched-vertex degenerate case). Adds
stryker-config.json (vstest runner; this project is NUnit 4, not xunit.v3)
and records GameCult.Geometry as scoped, diff-only, in
docs/mutation-testing.md's follow-ups.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 18:06:54 +02:00
Meta Vens
d110d9d6ff Merge the TypeScript QUIC realtime line (Cuts 3-6 and retention)
What lands:
- Cut 3: the native MsQuic bridge, with the Windows release leg and F5
  closed on both platforms, and the closerace fixture fix.
- Cut 4: the koffi Node binding, consumer, session manager and trust.
- Cut 5: the TS provider. It reaches the C# native connector, which is
  the Unity overlay's path, on Windows.
- Fix batches 1-4: use-after-free, handler isolation, the
  handshake-timer eviction, the koffi allocation leak, one C#-shaped
  inbox, encode-once, advertisedHost, publish-only
  (acceptClientFrames: false), and maxConnections.
- Retention: the provider seeds late joiners with the newest
  latest-only frame per (channel, body), in TS and C#.
- Cut 6, narrowed: native binaries committed under
  packages/cultmesh-ts/native, built by Actions. A second from-scratch
  build reproduced them byte for byte.

Gates: four Opus Soul passes on the runtime and one Sonnet pass on the
packaging. The last runtime pass found that the connection death under
load was the old handshake eviction, which fix 3a had removed, and it
confirmed StreamPixels' load (60 Hz latest-only for 10 minutes) with
flat memory.

Also carried, per the operator's ruling: CultMath erf/erfinv (Cut 12.0
and its fix batch) from the branch base.

Map conflicts were resolved in favour of main, which owns the maps. The
only branch-side lines main lacked were a Cut 3 "still outstanding"
block that main's closure supersedes.

Open follow-ups, recorded on the map: IPv6-literal endpoints cannot be
dialled; consumer-side native queues are unbounded while Unity is not
polling; a client can see connect() resolve past maxConnections before
the refusal arrives; subtraction (about -130, mostly comments) is
deferred.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 23:51:19 +02:00
Meta Vens
8cf89a7e56 Extend the Cut 5 interop lane: a late C# native connector gets the TS retained frame
Cut C's headline cross-process proof. The TS `serve` peer already tore its
listener down the instant it finished broadcasting, so nothing could dial it
late; it gains `--linger-ms` to hold the listener open afterward instead. The
interop harness starts that provider, runs a throwaway `dial` peer that
receives every frame and disconnects (the provider requires a first peer
before it broadcasts at all, so this is also what proves broadcasting
finished), then only after that dials with the C# native connector and
asserts the frame it gets is the retained one, by payload, not just
non-empty. New `NativeConnectorReceivesTheRetainedFrameWhenJoiningLate` in
GameCult.Mesh.Quic.Native.Tests carries the assertion; the existing
`NativeConnectorReceivesFromExternalProvider` is untouched. Windows-only,
matching the native connector under test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:32:39 +02:00
Meta Vens
7b7de5b071 Fix a flaky disconnect wait in the reliable-ordered retention test
AReliableOrderedFrameIsNeverRetainedAndIsNotDeliveredToALateJoiner disposed
the first session and then waited on server.ConnectionCount == 0 before
attaching a second one, which timed out under Yggdrasil's ygg-verify
container (dotnet test, cultlib-quic-native-dev image): the retention rule
under test does not depend on the first peer disconnecting, so the fix keeps
both sessions connected concurrently and drops the flaky wait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:20:36 +02:00
Meta Vens
680af452c6 C# QUIC server: retain the newest latest-only frame per (channel, body)
CultMeshQuicRealtimeServer mirrors the TS provider: a `_retained` dictionary
holds the newest latest-only frame per (channel, body) key, updated in
BroadcastAsync with the same CultMeshGeneration comparison the receive-side
filter already uses. AcceptLoopAsync seeds a newly accepted client with the
retained frames. Both the retained update+fan-out and the client
registration+seed run under one lock (`_retainedGate`), so a seed and a
concurrent broadcast are totally ordered and a client can never end up seeded
with a frame older than one it already received via broadcast. Reliable-
ordered frames are events and are never retained.

CultMeshGeneration is now internal so the server can reuse the transport's
own generation comparison instead of a second implementation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 18:17:28 +02:00
Meta Vens
92f2566d2c Fix the golden-bytes lane to match by sequence, not assume frame 1
Found by a real failure on the Linux verification container: latest-only
coalesces on both the provider's outbox and the managed connector's own
receive-side inbox, so a single ReceiveAsync() call is not guaranteed to
land on the first frame broadcast — on a slower host it landed on sequence 3.
The C# test now also prints CULTMESH_GOLDEN_SEQUENCE, and the harness looks
up the TypeScript peer's logged encoding of that same sequence instead of
hardcoding sent === 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:21:03 +02:00
Meta Vens
962162e7c6 Rename and add the C# sides of Cut 5's external-provider lane
NativeConnectorReceivesFromExternalManagedProvider -> NativeConnectorReceivesFromExternalProvider:
CULTMESH_NATIVE_EXTERNAL_ENDPOINT's peer is no longer necessarily the C#
managed provider now that Cut 5 adds a TypeScript one on the same lane; the
env var name stays, since it names the endpoint, not the peer.

Add ManagedConnectorReceivesFromExternalProvider, gated by the same env var:
dials an external provider with the plain managed connector and no
ValidateProviderCertificate (so acceptance runs on the endpoint's own
cert-sha256 pin alone, like an ordinary consumer), and prints the received
frame's CultMeshRealtimeWireProtocol re-encoding as hex for the harness's
golden-bytes comparison against the TypeScript peer's own encoding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 15:06:15 +02:00
Meta Vens
fbbc6dcfc1 Wait for a client before quic-realtime-serve broadcasts
Finding 8(b): the serve mode broadcast immediately after printing its ready
line, before the harness's dial could reach it, so frames went to an empty
peer set and were silently dropped. WaitForFirstConnectionAsync polls
ConnectionCount, bounded at 30s so a peer that never gets dialed still exits
instead of hanging the interop lane forever.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 14:38:51 +02:00
Meta Vens
c57b613582 Move QuicRealtimeServeConfig to the file's type-declaration tail
Program.cs is a top-level-statements file: every type declaration (the
existing ServeConfig, DialConfig and friends) lives after every top-level
statement and local function, all clustered at the end. The new record landed
between two local functions instead, which is CS8803: "top-level statements
must precede namespace and type declarations." Moved beside DialConfig, where
its neighbors already live.

Found by a direct `dotnet build` run on Yggdrasil after the interop test's own
error message was truncated by Node's console printing before reaching the
actual compiler diagnostic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 13:18:20 +02:00
Meta Vens
7e08c5cfd2 Interop: C# managed QUIC provider serving a TypeScript native consumer
tests/GameCult.Networking.InteropPeer gains mode quic-realtime-serve
(--port, --frames, --delivery, --interval-ms): creates a self-signed
certificate the same way CultMeshQuicRealtimeTransportTests.cs does, starts
CultMeshQuicRealtimeServer, and prints one ready line carrying the advertised
cultmesh-state+quic:// endpoint with its cert-sha256 pin, then broadcasts N
frames with increasing sequence, exiting after the loop or on stdin close.
GameCult.Networking.InteropPeer.csproj references GameCult.Mesh.Quic.

Discrepancy, kept: the cut map's worked line for the ready signal is
`{ endpoint: "..." }` alone; this repo's spawnServeProcess harness
(cultnet-interop.test.ts:2491) waits for a line whose parsed `status` is
"ready", the same convention every other serve mode here already uses. The
printed line is `{ status: "ready", endpoint: "..." }` — the map's shape plus
the field its own named mechanism requires — rather than inventing a second
readiness convention for one peer.

packages/cultmesh-ts/test/interop/cultmesh-quic-peer.ts: the TypeScript half,
`dial --endpoint --expect N`, connecting with CultMeshQuicRealtimeConnector
and printing the received frames as one JSON line (fields plus payload hex).
It reaches the connector through this package's own src/, never importing
cultmesh-ts itself, keeping the dependency direction the map states
(cultmesh-ts depends on cultnet-ts, never the reverse).

packages/cultnet-ts/test/interop/cultnet-interop.test.ts gains
"CultMesh QUIC realtime: C# managed provider and TypeScript consumer",
spawning the C# peer with spawnServeProcess and the TypeScript peer with
runJsonCommand, both established patterns in this file. Skips when
CULTMESH_QUIC_NATIVE_DIR is unset, the same convention the unit tests use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 12:33:02 +02:00
Meta Vens
63a677bea9 Correct one comment still naming the deleted To.SchemaId tiebreak
Soul found this while confirming batch 8. The Survivor-5 header in the same
file was updated when the component was deleted and this one was not, so it
described a comparator that no longer exists.

Self edited this directly rather than dispatching Hands for a single stale
sentence. Naming that because Self editing code is a boundary worth saying
out loud when it is crossed, even for a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:21:52 +02:00