Commit graph CultLib/tests/GameCult.Networking.Tests
Author SHA1 Message Date
Meta Vens
79f144d9ec CultNetDatabase.Dispose publishes what a writer logged and had not yet published
The dispose test showed journal-first alone still lost the change: Dispose won the race against the writer's publication and cleared the stash. Dispose now drains the stash through the publisher after the journal is disposed (so nothing is stashed afterwards) and before the subject completes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:16:57 +02:00
Meta Vens
557e70fd03 Cut 4 Soul 4c: committed-unlogged wire writes, database serves only what it owns, typed unowned refusal
Q1: HandlePutAsync/HandleDeleteAsync catch a CultNetShardLogException (or an
AggregateException holding one) before the generic catch and answer nothing: the
write committed and was published, so the peer is not told it was refused. It is
logged at warning level. The wire has no committed-with-degraded-log message.
Q2: GetAsync/GetAll/GetByName/GetByIndex, the shard-less snapshot, the selection
and the subscription snapshot serve only rows a shard owns, so read and watch
agree; the CultMesh collections follow through GetAll, and the Mesh untyped
variant fallback checks ownership. The raw cache still holds everything.
Q3: a replica log apply skips a row its shard does not own, as the snapshot path
does, and still records the entry so a chained replica sees no gap.
API: rowFilter is internal (public CreateSelectionResponse and
CreateRawSnapshotResponse signatures restored, internal overloads added);
exception tags on the write methods; Commit's unwrap rule is XML doc; the
unowned refusal answers on the wire with code unowned_schema (schema enum in both
copies, Rust CultNetErrorCode, CultNetErrorMessage.ForUnownedSchema) and put and
delete log it at warning level.
Dispose keeps journal-first; a test now fails if the observer goes first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:14:49 +02:00
Meta Vens
396cccafd4 Soul probes: commit the pass 4c probes (Q1-Q5) as recovered from 9c30f67f
The Finding-category probes fail on this commit; the fixes that follow are their tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:09:14 +02:00
Meta Vens
dac712b09c CultNetDatabaseServer: a selection skips cached rows no shard owns instead of failing
ResolveShard now throws for an unowned schema, so one such cached row would
have failed the whole selection. The server passes a row filter
(CultNetDatabase.Owns) to CreateSelectionResponse, which gains an optional
rowFilter that SelectPage already took, so those rows are not part of what the
database serves and the rest is answered.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 05:29:35 +02:00
Meta Vens
e79882a8ec CultNetDatabase: no shard owns it, no write, on every path; the fallback shard is deleted
ResolveShardInternal throws CultNetUnownedSchemaException instead of falling
back to the first shard. PutAsync, DeleteAsync, PutPredictedAsync,
ApplyPutAsync and ApplyDeleteAsync refuse an unowned schema before the cache is
touched; a remote put reaches the server's generic catch and is answered as an
application rejection like any other refused remote put. ResolveShard throws
(documented), CanWriteAuthoritatively returns false, and a bare cache write no
shard owns is neither logged nor published. The unused string overload of
EnsureClientAuthority is deleted. No GameCult consumer configures shards
outside vendored CultLib copies; the default shard owns everything.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:31:03 +02:00
Meta Vens
e0075217b7 Networking test: the outside-the-shard fixture is written to the cache, since the database refuses it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:21:48 +02:00
Meta Vens
9f6b2d4081 CultNetDatabase: a write no shard owns is refused, not cached and never logged
PutAsync and DeleteAsync carry no door, so the journal cannot place an unowned
schema and now skips it; accepting the write anyway was a silent loss of
replication. They resolve their shard strictly and throw
CultNetUnownedSchemaException (an InvalidOperationException naming the schema)
before the cache is touched. Doors keep the fallback shard they were resolved
to. No GameCult consumer configures shards outside vendored CultLib copies:
AetheriaEve's node uses the default PrimaryAll shard, which owns everything.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 04:08:45 +02:00
Meta Vens
f3076264fc Journal tests: a plain put of an unowned schema is unlogged, a door put keeps the fallback shard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:51:14 +02:00
Meta Vens
2eb0e8d6a6 Journal fixes: hole floor on restart, one log exception per call, no shape guard, no stray shard-0 logging
F1: InitializeLogSequencesFromStore raises the in-memory floor to the highest
gap above the store's compaction point, so a restart after a double refusal or
an interrupted compaction answers compacted_history instead of a gapped log.
F2: the journal raises one CultNetShardLogException per call carrying every
burned (shard, sequence); the unwrap rule for a cache-aggregated hold is
documented on the exception and on the cache commit.
F3: TakeDoor no longer refuses a door whose instance is the cached one, so
mutate-then-predict publishes Predicted and is not logged.
F6: a door-less change no shard matches is not logged; doors keep the
fallback shard they were resolved to.
Dispose disposes the journal before the observer. The log store doc says a
slow store blocks cache readers. Soul F4 is ignored as an accepted follow-up,
M1 is explicit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:48:49 +02:00
Meta Vens
49582cf43e Soul probes: commit the pass-3 P-tests and the Cut 4 probes as recovered
SoulPub3Tests.cs is from 1ccc4927 (recovered pass-3 P-tests); SoulP4ProbeTests.cs
is from 8ae8b3dc. Committed as Soul wrote them; the Finding-category probes fail
on this commit and are the tests for the fixes that follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 02:38:50 +02:00
Meta Vens
e79f3ee370 Journal tests: pin the survivors Stryker found in the changed journal code
Restart over a retained log resumes above its highest sequence and reads its
latest from the durable log; put and delete doors log their own wire messages
and change kinds; a reconciled put is logged as an update; a replica's
watermark follows the primary's sequences; an admission racing disposal logs
nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:52:02 +02:00
Meta Vens
af7751e263 Journal tests: the nested shard-log apply must finish, and the store-writes-cache probe fires once
The reentrant apply test now asserts the nested call completes without
throwing: a re-recorded sequence used to be swallowed by the subscriber's
handler and hide a non-replacing in-memory log. The store-writes-cache probe
writes once, so a cache that let the journal re-enter it fails the test instead
of recursing until the host crashes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:24:40 +02:00
Meta Vens
3fb2613abf Networking tests: order is data, so pin each change's own publication, not its delivery order
The salvaged S1 and in-place re-upsert tests also asserted the delivery order
of the rejected drainer. Under Q-P1 A delivery order across threads and a
nested write's depth-first delivery are not guaranteed; each change is
published once with its own context, and wire peers drop stale changes by
Sequence (Cut 6). Both assertions now say exactly that. The journal tests'
reentrant-write recorder subscribes before the handler that writes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 23:52:14 +02:00
Meta Vens
fe7bca0cb5 CultNetDatabase: log on the cache journal and publish from Watch, no queue
Cut 4b-4d of the publication map. The reorder buffer, drainer, prediction gate
and the logging inside the Watch handler are gone. The database registers the
Watch observer, then one cache journal. The journal runs under the gate in
Sequence order: it takes the door, decides the change kind and prediction
bookkeeping, logs (a primary mints, a replica records the primary's entry) and
stashes the change; the Watch observer publishes the stash entry by Sequence.

A primary that cannot log a committed change burns that sequence, raises its
own compaction floor, tries CompactThrough, and throws CultNetShardLogException
(not an InvalidOperationException) after publication. A replica's refused
append reaches the pull loop and the cursor does not advance.

Fixes four defects the map named: the log is appended to disk before memory
(and memory is a SortedList keyed by sequence); the snapshot reads its log
sequence before its documents; a restart resumes above the compaction point;
GetCompactedMutationLogSequence and GetLatestMutationLogSequence honour the
in-memory floor. The snapshot removal door sets Removal.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 23:48:50 +02:00
Meta Vens
7d945b002d Merge the salvaged CultNet publication branch (queue removed in the next commit)
Cut 4a: brings the publication tests and the door table from
hands/cultnet-publish-commits at 28248ebe. The reorder buffer, drainer and
prediction gate die in 4b.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 23:36:55 +02:00
Meta Vens
3bf1c0cea5 Merge RUDP stray-packet hardening across all four runtimes
A stray, malformed, foreign-id or refused datagram is dropped and counted,
never fatal; a refused reliable frame ends the peer's session instead of
being acked and lost; flush fails once its session ends; one reliable
Accept per session; the TS operation service expires idle sessions and
survives a throwing handler. Four Soul passes; session-generation and
reconnect semantics move to the ack cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:05:30 +02:00
Meta Vens
29f8bf575e Tests: a repeated Connect acknowledges what it carries (TS, Python, C#); fix the TS replaced-peer test
The repeated-Connect answer applies its acknowledgement field in every runtime;
no test failed when a runtime stopped doing so. The TS replaced-peer test now
lets client A receive its Accept before A sends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:35:44 +02:00
Meta Vens
ca18dfac55 GameCult.Networking: an ended session fails its flush; a repeated Connect owes one Accept
FlushReliable throws for writes an ended session forgot. A server-mode
transport and the listener answer a Connect from the peer they already hold
with AnswerRepeatedConnect: the Accept still owed, or an Ack, queueing
nothing. The listener replaced the peer session on every Connect, reusing
sequences and never reporting PeerDisconnected; it now replaces only on a
different connect payload and reports the old peer, as the Rust hub does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:41:23 +02:00
Meta Vens
306412e822 GameCult.Networking.Tests: the malformed-length datagram has the 36-byte fixed header and no channel id
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:43:55 +02:00
Meta Vens
b579075dd5 GameCult.Networking: a declared length past int.MaxValue is malformed; server mode is claimed only by a Connect; a refusing session ends
CultNetRudpPacketCodec.Decode did checked((int)length), so a datagram declaring
0x80000000 threw OverflowException, which no receive loop catches; the length
is now compared as a long and takes the ordinary malformed-packet drop.
Server mode claimed its endpoint on the first datagram of any type; only a
Connect claims it now. A session that refuses a packet has already recorded
its reliable sequence, so the socket transport and the listener end it: the
session forgets the peer (so the goodbye does not acknowledge the refused
frame), the peer gets a Disconnect, the listener drops the peer and raises
PeerDisconnected, and a server-mode transport forgets its endpoint.
CultNetRudpSession gains ResetPeerState, and the session and the listener
reject an InitialSequence of uint.MaxValue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:41:56 +02:00
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
41b964c6f1 Narrow the escaped-observer probe to what this base can show
A database write is published after its cache write returns, so a write whose
observer threw never reaches the database observers on this base. Keep the
probe to the exception reaching the writer and later changes still publishing
and logging; the ordering question belongs to the database cut.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:57:04 +02:00
Meta Vens
b8ceab84ac Drive the escaped-observer probe through a database write
A direct cache write is not a database change on this base, so the database
observer could not show it. PutAsync is, and the observer after the thrower must
see it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:54:40 +02:00
Meta Vens
ecee41e8ff CultCache: own the observer list and isolate every observer
The cache no longer fans changes out through an R3 Subject. It keeps a
copy-on-write observer array and Publish catches per observer: every observer
receives every change, an exception that escapes (a fail-fast R3 handler) joins
the OnUpdate failures rethrown to the writer after delivery, and the observer
stays subscribed. Dispose completes observers, and a disposed cache completes
new ones at once. Deletes the false "OnNext does not throw" comment.

The composition contract now states the observer policy and that a throwing
name or index getter refuses a change set, loads included.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:51:33 +02:00
Meta Vens
7362257f08 CultCache: read names and indexes once before minting; a throwing getter refuses the whole change set
Resolve now ends in Project, the only place an admission runs a [CultName] or
[CultIndex] getter on a landing record. It runs before land and before any
Sequence is minted, so a throwing getter refuses the admission (loads
included) with nothing written, minted or left in memory. The variant name and
index rules and the indexer read the projection; Apply runs no user code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:48:19 +02:00
Meta Vens
28248ebede Adopt Soul pass 2 probes and pin log-before-publish and in-place re-upsert
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:06:05 +02:00
Meta Vens
d8d973c418 Pin the single-owner publication cases Soul falsified
Observer rewrite of the door key, variant log order, replica log sequences with
and without a local write, 4x5000 concurrent writers, a write after a database
delete, shard-snapshot resync, and a disposed database.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:21:39 +02:00
x
f3180f0194 Pin CultNet publication of cache commits over RUDP
Commit and upsert, database put/delete, replicated put, load removal and a base
edit re-resolving a variant dependent, each observed by a real subscriber.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:21:14 +02:00
x
574dba0a64 CultNet publishes every committed cache change once
CultNetDatabase watched OnUpdate, which fires for loads alone, so cache.Commit,
cache.UpsertAsync, variant dependents re-resolved by a base edit and Flatten never
reached subscribers or the mutation log, and a load removal carried an empty key.
The database now observes cache.Watch, the one owner of admitted changes. A write
door of the database (PutAsync, DeleteAsync, ApplyPut/Delete, replica apply, sync)
marks the key it is committing and publishes that change itself, since it carries
the shard-log entry; every other change publishes from the cache, keyed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:21:14 +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
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
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
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
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
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
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
Meta Vens
492dd0e754 R-BA: delete the unreachable to.schemaId tiebreak from both edge comparators
Soul could not construct a selection where To.SchemaId decides the
EdgesFor/page_edges order once From, Role and To.RecordKey already
tie, but flagged that as a claim, not a proof - the same shape of
argument that was wrong twice before on this comparator (R-AT).

Traced instead of asserted: TryResolveReferenceTarget (C#) and
resolve_reference_target (Rust) resolve a reference purely from the
citer's schema, the role, and the record key, against the evaluation's
own fixed candidate index (byKey/by_key) - no edge- or caller-specific
input steers which candidate wins. Once From and Role tie (pinning the
citer's schema and the declared target type/leaves) and To.RecordKey
also ties, the resolved row - schema included - is forced identical by
construction, in both runtimes, every time. No mutation can distinguish
it because no input can, so it was deleted rather than recorded as
"equivalent": a component nothing can distinguish is a line every
future reader must reason about for no behavioural gain.

Deletes the To.Descriptor.SchemaId/to.schema_id() ThenBy/then_with from
CultNetSelectionEvaluator.EdgesFor and selection.rs's page_edges sort,
and updates the comments (in-code and the Survivor 5 test block) that
described the old five-key chain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 09:08:55 +02:00
Meta Vens
cda28fa2ae R-AZ: mirror the all-absent Edge vector into the C# parity test
The 246-byte all-absent Edge vector (SM-10) existed only on the Rust
side (selection_wire_parity.rs's CSHARP_EDGE_HEX). The all-absent
header gets incidental cover because it is nested inside the 376-byte
envelope test; the edge has no such cover, so a C# edge encoder that
resumed omitting a null payload/payloadEncoding would still pass every
other test here - exactly the asymmetry R-AP existed to kill.

Adds EdgeAllAbsentMatchesTheConstantCommittedIntoTheRustTest, asserting
the same constant against MessagePackSerializer.Serialize. Flipping one
hex digit of RustEdgeAllAbsentHex fails the new test only, confirming
it is not vacuous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 09:08:42 +02:00
Meta Vens
35a450e32f R-AX: land the whole-message parity vector, and extend it past all-absent
Soul had this green in the rig at 376 bytes and called it "blocked on
unbuilt page-assembly ownership" - the wrong thing: only the two
*evaluators* producing the same page from the same rows needed that move
(a separate claim, R-AR); the two *encoders* producing identical bytes for
the same page value never did. Land the whole
cultnet.snapshot_response_raw.v1 envelope, byte-identical between Rust's
encode_cultnet_message_to_vec and C#'s CultNetSnapshotResponseRawV1Message,
on both runtimes.

The existing header/edge vectors also proved nothing about tags
(string[] vs Vec<String>) or payload (byte[] vs serde_bytes): both were
absent at every value pinned. Add a header with tags populated and an
edge with a payload populated, captured the same way against a real
MessagePack C# 3.1.7 run on Yggdrasil. Neither diverged - the two
mappings agree byte-for-byte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:40:52 +02:00
Meta Vens
22d0722d5a R-AT: fix the C# self-citer fixture's naming so it actually defeats push-order luck
The zebra/alpha pairing that kills Rust's drop-both-to-components mutant
does not kill it in C#: EvaluateAll concatenates citesEdges before
citedEdges (the opposite of cultnet_rs's single cited-then-cites Vec), so
that pairing's push order already matched the correct sort order here too
- just for the opposite reason. Swap to alpha (self) / zebra (target),
the pairing that actually disagrees with this runtime's own push order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:38:27 +02:00
Meta Vens
0bdcf6fd55 R-AT: add missing MessagePack using the C# self-citer fixture needs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:35:37 +02:00
Meta Vens
c9ba53cfe1 R-AT: pin the edge comparator's tied-from-and-role survivor in C#
The same live mutant Soul found in Rust (dropping to.schemaId/to.recordKey
from the edge comparator) survives in C# too - both runtimes share the
comparator's shape, not a rule proving it. Land the C# mirror of Soul's
self-citing-peer counterexample from the rig, renamed so the self edge's
push order and the correct sort order actually disagree (the rig's own
"hub"/"other" naming left the drop-both mutation surviving by coincidence
of push order matching alphabetical order - see the Rust commit on this
branch for the construction).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:34:44 +02:00
Meta Vens
62ebba4ab5 R-AO: shared vector for two-in-target shared-key resolution
Adds citer-both-in-target to the shared fixture, the first row in file (and
row-order) to cite "dup-key" through Design - both leaf_a and leaf_b at that
key are in Design's declared target, unlike citer_narrow's narrow_ref above
it (only ever one in-target candidate). Adds the matching vector case to
both runtimes' case lists and regenerates both written vector files
(CULTNET_WRITE_VECTORS=1), so cross-runtime agreement on this shape is
pinned by the parity check, not just asserted per-runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:18:11 +02:00
Meta Vens
f01c0b6112 R-AL/R-AM/R-AO: C# regression tests for the fix batch
- R-AL: DataMemberNameCollidingWithAReferenceMembersAliasIsRefusedAtRegistration
  pins D10b's registration refusal. ReferenceMembersExcludesADataMember...
  pins the restored evaluator guard directly (hand-built descriptor via
  reflection, since D10b now closes the registry door this shape used to
  slip through).
- R-AM: CultNetDatabaseServer_SnapshotV1Handler_CatchesAnUntypedFault... and
  RudpCultNetSchemaServer_DispatchBackstop_LogsEveryUntypedHandlerFault pin
  the handler's catch and the backstop's logging, each against a real
  untyped fault.
- R-AO: Evaluator_CitedWithTwoInTargetCandidates_ResolvesToWhicheverThe-
  CallerListedFirst pins "first row in the caller's order wins" when two
  in-target rows share a key, in both row orders.

Every test in this commit was checked against a hand-reverted mutation of
its own production fix and confirmed to fail before being confirmed to pass
against the real fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:17:21 +02:00
Meta Vens
3c4f83b038 R-AG/R-AK: checked C# cursor-length arithmetic, a dispatch backstop, and the redundant guard
R-AG. ReadLengthPrefixedFields tested pos + length > bytes.Length in int
math; a length prefix near int.MaxValue overflowed the unchecked add,
slipped past the guard, and Encoding.UTF8.GetString threw
ArgumentOutOfRangeException instead of this method returning null the
way every other malformed shape already does. Promotes to long before
the add, which cannot itself overflow (both operands stay far under
long's range), so the guard sees the true sum.

CultNetRudpSchemaServer.DispatchAsync gains a catch around the handler
invocation: HandleSnapshotRequestV1Async already answers
cursor_invalid/selection_invalid/reference_outside_target for its three
typed exceptions, but nothing stopped an untyped one from an
unanticipated bug reaching DynamicInvoke's caller and stalling the poll
loop for every other peer. Answers nothing on the wire (an exception
this generic carries no typed refusal to report) and drops the one
malformed dispatch, the same as an unregistered handler already does.

Hostile-cursor tests (CultNetSelectionFixBatchTests.cs) feed
CultNetSelectionCursor.Parse a length prefix near int.MaxValue (C#'s own
proven overflow), near usize::MAX (Rust's proven overflow, fed here too
per the ruling), wider than any integer type, and negative-looking -
each asserted to throw the typed CultNetSelectionCursorException.

R-AK. Deletes ReferenceMembers's `!member.IsReference` guard.
CultCache.ReferencesOf (CultCache.cs:262-278) already searches only its
own reference members for a role match and returns empty for anything
else; index aliases are unique per descriptor, so a data member's own
role string can never coincide with a reference member's. Verified
before deleting: traced ReferencesOf's role lookup directly rather than
trusting the ruling's premise. A second copy of the same exclusion here
was duplicate authority, not a second decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:44:09 +02:00
Meta Vens
b9db8702d2 R-AF/R-AJ: C# parity vectors and the ordering-composition tests they needed
R-AF: registers ParityCiterNarrow (citer_narrow's C# type, [CultIndex
"narrow_ref"] so its role matches the fixture/Rust spelling), adds
Keys-scoped vectors pinning both shapes from Soul's table - a shared key
where the only candidate is out of target refuses evaluation-time
(expectedEvaluationRefusal, a narrow addition to the vector schema
distinct from the door-only expectedRefusalField - not R-Z/R-AH's
page-bytes harness), and one where an in-target row shares a key with an
out-of-target row and must still resolve.

Landing these exposed a real, pre-existing cross-runtime scope
difference in R-W unrelated to R-AF: Rust checked every declared
reference on a row regardless of the citation's requested role;
C#'s ReferenceMembers(descriptor, citation.Role) narrows by role first.
Fixed on the Rust side (previous commit); hop_by_role_cites_exact_id and
cites_target_by_name_alias keep an explicit Keys scope here anyway as
belt-and-suspenders.

Also corrects S-12's last stale comment: cites_target_by_name_alias's
"Confirmed cross-runtime defect" note was already false - the alias port
fixed it - and still called it confirmed.

R-AJ: CultNetSelectionSurvivorTests.cs's row-tiebreak fixture
(Evaluator_BreaksAnOrdinalTieBySchemaIdThenRecordKeyInBothDirections) was
degenerate - probed by hand-reverting the source and swapping the
tiebreak's two ThenBy calls, it still passed, because whether the old
3-row fixture discriminated the swap depended on which of SelLeafA's/
SelLeafB's real schema ids happened to sort first, and on this repo's
ids it didn't. Rewritten to assign row keys relative to the schema ids
discovered at runtime (low-sorting schema gets the high-sorting key and
vice versa) so the two candidate orderings are always the exact reverse
of each other; reprobed the same mutant afterward and it now fails.

Adds a direct EdgesFor test (constructing FullEvaluation/EdgeMatch by
hand) pinning "From.Key before Role" in the five-key edge tiebreak - the
two existing EdgesFor tests each tie one of those two components, so
neither could tell a Role-first mutant from the rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:25 +02:00
Meta Vens
99e366a36d R-AA follow-up: kill ComputeDigest's flag-swap survivors, pin EdgesFor's primary key
The first Stryker pass (72.18%, up from 66%) showed the named dozen mostly
dead but surfaced two related gaps the "differs" style of assertion cannot
reach on its own:

- ComputeDigest's field-number and cites.role presence flags each had an
  equality-mutation survivor (field.Number != null / selection.Cites.Role
  != null) that a same-vs-different digest comparison cannot catch: the
  mutant swaps which of two cases gets which flag value, so both cases
  still digest differently, just under swapped labels. A handful of plain
  AppendString/sb.Append deletions survived alongside it for the same
  reason - removing content that a same-vs-different pair didn't happen
  to isolate. One independently reconstructed golden HMAC value, built
  from the method's own doc comments rather than its private helpers,
  pins the whole wire shape at once: any single append or flag polarity
  wrong breaks the exact match.
- EdgesFor's OrderBy(pageRowIndex) - the primary "page-row order" key R-B
  names before "(from, role, to)" - had no test forcing page position and
  from/role/to order to disagree, so it survived alongside (and separate
  from) the five ThenBy mutants already named.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:48:10 +02:00
Meta Vens
8ee1ebaf67 R-AA: behavioural tests for the dozen real Stryker survivors
Soul's triage on 52082ec named ~12 live survivors across
CultNetSelection.cs/CultNetSelectionEvaluator.cs, each sitting on a rule
this cut exists to enforce, plus 75 uncovered mutants behind them. Each
new test drives the public selection/evaluator surface with a fixture
built to make the rule's failure observable in the answer, not in the
code's spelling - several needed new fixtures (a mixed numeric/string
alias, an index alias differing from its member name, two reference
members resolving to one target) because their absence is why the
mutant survived in the first place.

Two of the five EdgesFor order-flip mutants (To.SchemaId/To.Key) are not
addressed: within one hop direction an edge's (From, Role) already
determines its To deterministically, so no legal selection can tie
anchor+From+Role while To differs. Recorded as a likely equivalence,
left for Stryker's own verdict rather than forcing a contrived test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:37:38 +02:00
Meta Vens
b7d77b708d Fix batch 4 (R-X, S-4/S-10): the error schema matches the wire, and the validator refuses a keyword it does not implement
CultNetErrorMessage is a MessagePack-CSharp map-mode object, so every
declared key is written on the wire, nil included - the same fact R-S
already established for the raw document header. cultnet.error.schema.json
was missing routingHint entirely and typed code/details as always-present
non-null values; Soul's P7 found real bytes failing it on exactly this.
routingHint, code and details are now `required`, with nullable types
matching CultNetErrorMessage/CultNetErrorDetails. A new test decodes real
bytes for all four refusal codes (selection_invalid, cursor_stale,
cursor_invalid, reference_outside_target) against the schema.

MiniJsonSchemaValidator:
- implements minimum, maximum, maxLength and maxItems, which were declared
  in committed schemas (fieldPredicate's number pattern, selection.limit,
  ...) but silently never checked;
- anchors `pattern` to a full match (Index==0 && Length==value.Length)
  instead of Regex.IsMatch's "search anywhere", so .NET's `$` (which,
  unlike ECMA-262's, matches immediately before one trailing newline even
  without RegexOptions.Multiline) can no longer let e.g. "5\n" pass a
  schema author wrote as "exactly one digit";
- refuses a schema node that uses a keyword this validator gives no
  constraint semantics to, instead of silently ignoring it. Running every
  committed contracts/cultnet/*.schema.json schema through this found 12
  such uses, none reached by any existing test: `allOf`/`if`/`then`/`else`
  in cultnet.database-change-raw.schema.json, and `uniqueItems` (six
  places) plus one `anyOf` in the cultmesh.verse-catalog-*.schema.json
  pair. contentEncoding, $schema/$id/$defs/title/description/default and
  the x-cult-runtime-type vendor extension are treated as metadata with no
  constraint semantics to implement, not refused.

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