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>
The fixture had zero duplicate record keys across its ten rows and no
ordinal tied across two different schemas - both shapes this fix batch's
rulings needed and neither runtime's parity vectors ever covered.
R-AF adds a "citer_narrow" schema (real SHA-256 id, computed the same way
CultDocumentRegistry does - a probe run against a type matching the
eventual C# type's exact namespace/nesting confirmed it) whose one
reference targets leaf_a only, plus rows for both shapes from Soul's
table: a lone out-of-target row at a key (refuses), and two rows sharing
a key where only one is in-target (accepts, resolving the in-target one
regardless of row order).
Also drops "targetSchema" from every reference entry: it was never
anything but documentation (SelectionParityVectorTests.BuildDocument and
cultnet-rs's build_row both already read only targetKey) and keeping a
field the wire does not actually have would contradict the rule R-AF just
established.
R-AJ adds a tied-ordinal pair across leaf_a and leaf_b, with keys chosen
opposite to the real schema ids (probed) on purpose, so a schemaId-first
order and a recordKey-first order are forced to disagree regardless of
which real id happens to sort first - see the row-tiebreak test this
enables.
selection-vectors.cs-written.json and selection-vectors.rs-written.json
are regenerated from the new fixture (CULTNET_WRITE_VECTORS=1 on both
sides) and cross-checked in both directions locally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran CULTNET_WRITE_VECTORS=1 dotnet test tests/GameCult.Networking.Tests -c
Release --filter FullyQualifiedName~WriteSelectionVectors on Yggdrasil
against b1932d9 and committed its output verbatim. 17 vectors total (up from
15): the 4 door-refusal vectors now include
refuses_unmatched_cites_target_schema and
refuses_cites_target_by_hash_shaped_alias.
Per the brief, this is the fix-batch-3 report's parity-vector table baseline.
The C# reader (SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyIn
TheReference) now passes all 16 of rs-written.json's vectors, up from 14/16 -
rs-written.json already carried both refusal vectors; only this file and the
door check that makes it agree were missing on the C# side. rs-written.json
itself was not touched (Rust-owned).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reconciles the two Hands' independently-regenerated parity vectors
(Soul's whole-cut pass, F3): the C# fixture used schema *names*
("leaf_a") as literal ids, the Rust fixture used invented fake
sha256-shaped ids - every vector carrying an id failed when read by
the other runtime.
Adds contracts/cultnet/interop/selection-vectors.fixture.json: the one
row set both writers now build from, keyed by each schema's real
SHA-256 content-hash id exactly as CultDocumentRegistry computes it
(dumped via a temporary DumpFixtureSchemaIds run). Carries the rows
either runtime needs - astral/BMP-private-use keys, a float tie,
cites/cited, a cites target reached by alias - plus two DIFFERENT
alias forms, because the two runtimes' alias matchers key off
different descriptor attributes and no single candidate resolves
through both (see the fixture's "//aliases" note and the report).
Rewrites SelectionParityVectorTests.cs to load the fixture instead of
hand-building rows (BuildFixture/BuildExtendedFixture deleted), assert
the fixture's declared schema ids still match what the registry
computes (fails loudly on drift instead of silently comparing wrong
ids), and use the real schema id (not SchemaName) as a row's
cross-runtime identity. Also fixes a real bug this file carried since
before the shared fixture: both the writer and the reader compared
`evaluation.Rows.Count` (the page size) against "matched" instead of
`evaluation.TotalMatched` (the total, per R-G) - selections with a
Limit were writing/checking the wrong number. The reader now collects
every vector's mismatch instead of stopping at the first one, so a
real parity defect is reported in full.
Regenerates selection-vectors.cs-written.json from the shared fixture.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json
under R-B/R-C/R-D/R-E/R-I.
SelectionParityVectorTests.cs: the six original cases' fixture
(BuildFixture) is left untouched, because SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
reads the already-committed selection-vectors.rs-written.json against
it, and a new row reachable by an unfiltered or loosely-filtered case
(descending_with_limit, gt_numeric_boundary) would silently change that
case's expected page. The fix-batch rows and cases live in a new
BuildExtendedFixture/Cases set, additive over BuildFixture, used only
by WriteSelectionVectors:
- float_tie_exact_ge: a row at 2233759.25f, on the round-to-even tie
R-D's exact BigInteger rendering fixes (its canonical decimal is
"2233759.25" regardless of which shortest form a runtime's ToString
would have picked).
- cites_target_by_alias: cites.target.schemaId given as "leaf_a"
(ParityLeafA's SchemaName) against a-eq's real content-hash SchemaId,
exercising R-E's alias-matched cites (the pre-fix code required an
exact SchemaId match and this always missed).
- astral_key_code_point_order: two rows sharing one ordinal, keyed by
a BMP private-use character (U+E000) and an astral character
(U+1F602, surrogate pair 😂), pinning R-C's code-point
tiebreak order.
- header_projection/document_projection: the same hop-bearing match set
under each projection.
Known-expected failure: SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
fails against the currently-committed selection-vectors.rs-written.json
(incoming_exists_true's edge count) because that file predates R-B and
was not touched here - the parallel Rust Hands owns it. Re-run once it
regenerates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two things landed on this branch while commit 3 was in flight, both addressed
here rather than left for the vectors to paper over:
**Self's ruling, 2026-09-22 (commit 2 fix batch, 568e8e3/021e3f1): empty
lists, blank entries, and v0's own cleaning.** Rust already refused an empty
schemas/keys list (inherited from before the ruling); it now also matches
the reference's three further decisions exactly, not just in spirit:
- The door's refusal field is the plain list name ("schemas"/"keys"), not an
indexed "schemas[i]" - selection::validate's SelectionInvalid.field now
matches CultNetSelectionInvalidException("schemas", schema, ...) literally,
because the parity vectors compare this string across runtimes.
- snapshot_query.rs's lower_v0_list now deduplicates a mixed list the same
way CultNetV0SelectionLowering.Lower does (Distinct(StringComparer.Ordinal)
- order-preserving, first occurrence wins), not just blank-filters it.
A new test (serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter,
extended) pins the dedup case directly.
- Two mutation entries (RS-EmptySchemas-Revert, RS-EmptyKeys-Revert) added
for the door's original empty-list refusal, which had tests but no
mutation coverage before this commit.
**S12: the two-way parity vectors**, contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json,
eight vectors each: the six evaluated selections from commit 3 (any_of+ge
conjunction, descending+limit, gt boundary, cited exists true/false, three
rows sharing one index value) plus two door-refusal vectors added for this
ruling - `refuses_empty_schemas_list` (schemas: []) and `refuses_blank_key_entry`
(keys: ["a-eq", " "]) - which decode their selection bytes and assert
`validate`/`Validate` refuses at exactly the named field, never evaluating.
The blank-key vector could not be generated before this commit: the door
rule it exercises did not exist in the reference until 568e8e3 landed.
`cites`-based hop vectors are deliberately absent (documented in both test
files): MatchesCitation compares Citation.Target.SchemaId against the
reference's real SHA-256 schema id exactly, with no SchemaName fallback,
and this fixture pair has no way to agree on that hash across runtimes;
`cited` (role/existence, matched by record key alone) is the hop direction
both sides can cross-check, so incoming_exists_true/incoming_negation carry
that coverage instead. Both reader tests pass:
SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference (C#)
and selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust
(Rust), each judging the other runtime's file.
**The remaining mutation matrix.** Of the coordinator's named set (S1, S2,
S6, S9-S15, S17, S20, S23): S1 was already covered (RS-EmptySchemas/Keys,
RS-BlankSchema/Key); S2 (any_of matches membership, RS-S2-Revert) and S6
(cites.role filters the hop, RS-S6-Revert) are newly covered - S6 needed a
new fixture (cites_role_excludes_a_second_reference_at_the_same_target)
because base_rows()'s citer-1 only ever carries one reference, so a mutant
dropping the role filter had nothing else to wrongly match and survived
against the original hops_one_edge_by_declared_role test; that finding is
recorded in-line as a comment, not silently swapped for an easier mutant.
S9-S15 (subscription reconcile, v0-lowering-of-subscribe) do not apply:
D7 - no Rust caller subscribes in this cut, so there is no subscription
client to mutate. S17 (inherited alias reaches every leaf) does not apply
either: Rust's toy Row/RowSet has no inheritance to begin with - both
leaves in the fixture declare "mass" independently, which is the Rust
spelling of the same guarantee, already exercised throughout the suite
without a dedicated mutant (there is no shared base-type discovery path to
revert). S20 (edges only for a hopping selection) was investigated and
found structurally vacuous in Rust as built: `edges` is only ever populated
inside the cites/cited branches, so a mutant on the has_hop() gate cannot
produce a different observable result for a non-hopping selection - no
entry added; a real gap would need the evaluator's structure to change
first. S23 (matches every row sharing an index value, never a cache's
unique-index winner) has no revertable bug surface: Rust's Row/RowSet
never had a unique-index map to begin with (D7), so this is pinned by
construction, not by a mutation-testable rule; matches_every_row_sharing_an_index_value
still exists as a positive test naming it.
**A pre-existing, unrelated flaky test, found and worked around, not
fixed.** tests/cultnet.rs's reactive_document_coalesces_direct_same_schema_alias_member_writes
fails consistently when the mutation harness runs the full tests/cultnet.rs
binary (both default-parallel and RUST_TEST_THREADS=1), but passes 5/5 in
isolation and passes in a plain `cargo test -p cultnet-rs` run - state
pollution from another test in that binary under the harness's specific
invocation path, not a race this campaign's code participates in (nothing
here touches reactive_document machinery). RS-V0Lowering-Revert's Command
is narrowed to `cargo test --test cultnet serve_read_only_raw_snapshot`
(filtering both M0 and the entry's own run to this campaign's test) so the
suite does not depend on that unrelated test's health; the flake itself is
reported here as a finding, not patched.
Tests: cultnet-rs 174 passed, 0 failed (default parallel run).
GameCult.Networking.Tests 196 passed, 1 skipped (WriteSelectionVectors,
env-gated), 0 failed.
Mutation harness: 24 entries (was 22 in commit 3; +RS-S2-Revert, +RS-S6-Revert;
RS-BlankSchema/BlankKey and RS-V0Lowering-Revert's anchors and field-name
assertions updated to match the landed fix batch's exact spelling), all
KILLED, M0 green.
Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty.
Structural delta, this commit only: 6 files changed, +108/-23 (selection.rs
+15/-9 field-name fix; snapshot_query.rs +25/-14 dedup; tests/cultnet.rs +21
dedup test; tests/selection.rs +35/-2 S6 fixture and field-name fixes;
scripts/mutate-cultnet-selection-rust.entries.ps1 +33/-4 two new entries
plus three corrected anchors/commands; SelectionParityVectorTests.cs +2/-1
field name), plus the two committed vector JSON files (~90 lines each, new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>