CultLib/tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs

639 lines
37 KiB
C#
Raw Normal View History

CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
#nullable enable
using System;
using System.Collections.Generic;
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
using System.Globalization;
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
using System.IO;
using System.Linq;
using System.Text.Json;
using GameCult.Caching;
using MessagePack;
using NUnit.Framework;
namespace GameCult.Networking.Tests
{
// CultNet typed selection, Cut 1 commit 3/4 (docs/cultnet-selection-cut.md section 6/10, S12):
// the C#<->Rust parity vectors. New file; no existing C# source is modified.
//
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// Self's ruling, 2026-09-22 (shared-fixture ruling, on top of the Cut 1 fix batch): both this
// writer and packages/cultnet-rs/tests/selection.rs's write_selection_vectors_for_the_reference
// build their rows from the one shared
// contracts/cultnet/interop/selection-vectors.fixture.json, keyed by each schema's real
// SHA-256 content-hash id, exactly as CultDocumentRegistry computes it for the fixture's own
// document types below - never by schema *name* as a stand-in id, which let every vector that
// carried an id decode on the wrong runtime's terms (Soul's whole-cut pass, F3: a name-as-id
// fixture masked the real alias-matching gap between CultNetSchemaAliasMatching and
// cultnet_rs::selection::schema_alias). AssertSchemaIdMatches below fails loudly, not silently,
// if this file's reflected shape ever drifts from the committed fixture.
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
public sealed class SelectionParityVectorTests
{
private static CultDocumentRegistry Registry() => CultDocumentRegistry.ForTypes(new[]
{
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
typeof(ParityLeafA), typeof(ParityLeafB), typeof(ParityCiter), typeof(ParityCiterNarrow)
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
});
private static CultNetSelectionEvaluator.Row Row(CultDocumentRegistry registry, object document, string key, long ordinal) =>
new(registry.GetRequired(document.GetType()), new CultRecordKey(key), document, ordinal);
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// ----------------------------------------------------------------------------------------
// The shared fixture: contracts/cultnet/interop/selection-vectors.fixture.json. Holds the
// real schema ids (and their unversioned aliases) and every row either runtime needs:
// astral/BMP-private-use keys (R-C), a float tie (R-D), a cites/cited pair, and a cites
// target reached by alias (R-E). Both writers build from it; neither hand-rolls its own row
// set any more.
// ----------------------------------------------------------------------------------------
// R-E: two alias forms, because the two runtimes' schema-alias matchers key off different
// attributes and no candidate resolves through both (see the fixture file's "//aliases"
// note and this file's report of the confirmed defect). NameAlias is what
// CultNetSchemaAliasMatching's descriptor overload (the only one any C# call site uses)
// can resolve; HashAlias is what cultnet_rs::selection::schema_alias can resolve.
private sealed record FixtureSchema(string SchemaId, string NameAlias, string HashAlias);
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
// R-AF: no TargetSchema - the wire a stored reference actually travels on carries a bare
// record key (GameCult.Caching.MessagePack.CultRecordRefFormatter<T> writes only
// value.Key.Value), so the fixture stopped pretending otherwise. See the fixture file's
// "//references" note.
private sealed record FixtureReference(string Role, string TargetKey);
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
private sealed record FixtureRow(
string Schema, string Key, long Ordinal,
IReadOnlyDictionary<string, string> Fields,
IReadOnlyList<FixtureReference> References);
private static string FixturePath() =>
Path.Combine(RepoRoot(), "contracts", "cultnet", "interop", "selection-vectors.fixture.json");
private static (IReadOnlyDictionary<string, FixtureSchema> Schemas, IReadOnlyList<FixtureRow> Rows) LoadFixture()
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
{
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
using var document = JsonDocument.Parse(File.ReadAllText(FixturePath()));
var root = document.RootElement;
var schemas = new Dictionary<string, FixtureSchema>(StringComparer.Ordinal);
foreach (var entry in root.GetProperty("schemas").EnumerateObject())
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
{
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
schemas[entry.Name] = new FixtureSchema(
entry.Value.GetProperty("schemaId").GetString()!,
entry.Value.GetProperty("nameAlias").GetString()!,
entry.Value.GetProperty("hashAlias").GetString()!);
}
var rows = new List<FixtureRow>();
foreach (var row in root.GetProperty("rows").EnumerateArray())
{
var fields = new Dictionary<string, string>(StringComparer.Ordinal);
if (row.TryGetProperty("fields", out var fieldsElement))
{
foreach (var field in fieldsElement.EnumerateObject())
fields[field.Name] = field.Value.GetString()!;
}
var references = new List<FixtureReference>();
if (row.TryGetProperty("references", out var referencesElement))
{
foreach (var reference in referencesElement.EnumerateArray())
{
references.Add(new FixtureReference(
reference.GetProperty("role").GetString()!,
reference.GetProperty("targetKey").GetString()!));
}
}
rows.Add(new FixtureRow(
row.GetProperty("schema").GetString()!,
row.GetProperty("key").GetString()!,
row.GetProperty("ordinal").GetInt64(),
fields,
references));
}
return (schemas, rows);
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
}
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
private static void AssertSchemaIdMatches(CultDocumentRegistry registry, Type type, string expected)
CultNet selection cut1 fix batch: regenerate the C#-written parity vectors 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>
2026-09-22 09:40:18 +02:00
{
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
var actual = registry.GetRequired(type).SchemaId;
Assert.That(actual, Is.EqualTo(expected),
$"{type.Name}'s real schema id drifted from contracts/cultnet/interop/selection-vectors.fixture.json - " +
"regenerate the fixture (its schemas.*.schemaId/alias fields) from CultDocumentRegistry before trusting these vectors.");
}
private static object BuildDocument(FixtureRow row) => row.Schema switch
{
"leaf_a" => new ParityLeafA
{
Name = row.Key, Kind = row.Fields["kind"], Mass = float.Parse(row.Fields["mass"], CultureInfo.InvariantCulture)
},
"leaf_b" => new ParityLeafB
CultNet selection cut1 fix batch: regenerate the C#-written parity vectors 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>
2026-09-22 09:40:18 +02:00
{
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
Name = row.Key, Kind = row.Fields["kind"], Mass = float.Parse(row.Fields["mass"], CultureInfo.InvariantCulture)
},
"citer" => new ParityCiter
{
Name = row.Key,
Design = new CultRecordRef<ParityMiddle>(new CultRecordKey(
row.References.Single(reference => reference.Role == "Design").TargetKey))
},
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
// R-AF: citer_narrow's reference is declared to ParityLeafA only (one leaf, not
// ParityMiddle) - the shared-key shape needs a role whose declared target excludes at
// least one of the schemas that can share a record key.
"citer_narrow" => new ParityCiterNarrow
{
Name = row.Key,
NarrowRef = new CultRecordRef<ParityLeafA>(new CultRecordKey(
row.References.Single(reference => reference.Role == "narrow_ref").TargetKey))
},
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
_ => throw new InvalidOperationException($"selection-vectors.fixture.json: unknown schema '{row.Schema}'")
};
// The one row set: both SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
// (reading rs-written) and WriteSelectionVectors (writing cs-written) build from it, so both
// committed vector files are regenerated together whenever the fixture changes.
private static (CultDocumentRegistry registry, CultNetSelectionEvaluator.Row[] rows, IReadOnlyDictionary<string, FixtureSchema> schemas) BuildFixture()
{
var registry = Registry();
var (schemas, fixtureRows) = LoadFixture();
AssertSchemaIdMatches(registry, typeof(ParityLeafA), schemas["leaf_a"].SchemaId);
AssertSchemaIdMatches(registry, typeof(ParityLeafB), schemas["leaf_b"].SchemaId);
AssertSchemaIdMatches(registry, typeof(ParityCiter), schemas["citer"].SchemaId);
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
AssertSchemaIdMatches(registry, typeof(ParityCiterNarrow), schemas["citer_narrow"].SchemaId);
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
var rows = fixtureRows.Select(row => Row(registry, BuildDocument(row), row.Key, row.Ordinal)).ToArray();
return (registry, rows, schemas);
CultNet selection cut1 fix batch: regenerate the C#-written parity vectors 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>
2026-09-22 09:40:18 +02:00
}
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
private static IEnumerable<(string Name, CultNetSelection Selection)> Cases(IReadOnlyDictionary<string, FixtureSchema> schemas)
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
{
yield return ("any_of_plus_ge_conjunction", new CultNetSelection
{
Fields = new[]
{
new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "weapon" } },
new CultNetFieldPredicate { Index = "mass", Op = "ge", Number = "5" }
}
});
yield return ("descending_with_limit", new CultNetSelection { Descending = true, Limit = 3 });
yield return ("gt_numeric_boundary", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "mass", Op = "gt", Number = "1" } }
});
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// R-E: the exact real schema id - matches on both runtimes' first (exact string)
// comparison branch, independent of the alias-matching gap below.
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
yield return ("incoming_exists_true", new CultNetSelection
{
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
Schemas = new[] { schemas["leaf_a"].SchemaId },
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
Cited = new CultNetIncoming { Role = "Design", Exists = true }
});
yield return ("incoming_negation", new CultNetSelection
{
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
Schemas = new[] { schemas["leaf_a"].SchemaId },
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
Cited = new CultNetIncoming { Role = "Design", Exists = false }
});
yield return ("shared_index_value_three_rows", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "shared" } } }
});
CultNet selection cut1 fix batch: regenerate the C#-written parity vectors 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>
2026-09-22 09:40:18 +02:00
// R-D: the row's canonical rendering of 2233759.25f is its exact decimal expansion, not a
// shortest round-trip form that could round the tie the other way - ge against the exact
// value must match.
yield return ("float_tie_exact_ge", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "mass", Op = "ge", Number = "2233759.25" } }
});
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// R-E: the exact real schema id, mirroring cultnet-rs's hop_by_role_cites_exact_id.
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
// R-AF: Keys=["citer-1"] (the fixture's one row that actually cites a-eq via Design)
// scopes this away from citer_narrow's rows, as hygiene rather than necessity -
// landing R-AF's vectors surfaced a real R-T/R-W parity bug this exposed: Rust's
// matches_citation used to resolve and R-W-check every declared reference a row
// carries before filtering by the citation's own role, where C#'s
// ReferenceMembers(descriptor, citation.Role) filters by role first
// (CultNetSelectionEvaluator.cs:451-458). Fixed in cultnet_rs::selection::matches_citation
// to filter first too, so narrow-citer-refused's out-of-target narrow_ref edge no longer
// reaches a Role="Design" citation in either runtime - this vector keeps the Keys scope
// anyway so it never depends on that.
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
yield return ("hop_by_role_cites_exact_id", new CultNetSelection
CultNet selection cut1 fix batch: regenerate the C#-written parity vectors 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>
2026-09-22 09:40:18 +02:00
{
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
Keys = new[] { "citer-1" },
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].SchemaId, RecordKey = "a-eq" }, Role = "Design" }
CultNet selection cut1 fix batch: regenerate the C#-written parity vectors 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>
2026-09-22 09:40:18 +02:00
});
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// R-E: the same citation, but the target is named by its C#-resolvable alias
// ("leaf_a.v9") rather than the exact id. CultNetSchemaAliasMatching's descriptor overload
// (CultNetDatabase.cs:74-87, the only overload any `schemas`/`cites.target.schemaId` call
// site in src/ uses) strips the trailing ".v9" and compares "leaf_a" to
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
// ParityLeafA's descriptor.SchemaName - a real match here, in the reference. S-12
// (2026-09-22) is not a leftover defect: cultnet_rs::selection::schema_alias now resolves
// the same way, through Row::schema_name/RowSet::schema_name standing in for
// CultDocumentDescriptor.SchemaName (packages/cultnet-rs/src/selection.rs:452-475), so
// this vector's expectedIds are real and non-empty in both runtimes - the alias port made
// the two DIFFERENT alias forms deliberate (see the fixture file's "//aliases" note)
// rather than leaving this one unresolved on the Rust side.
// R-AF: Role="Design" plus Keys=["citer-1"] for the same reason as
// hop_by_role_cites_exact_id above.
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
yield return ("cites_target_by_name_alias", new CultNetSelection
{
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
Keys = new[] { "citer-1" },
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].NameAlias, RecordKey = "a-eq" }, Role = "Design" }
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
});
// R-E: `schemas` reached by the same name alias. Same one-sided-match caveat as above.
yield return ("schemas_by_name_alias", new CultNetSelection { Schemas = new[] { schemas["leaf_a"].NameAlias } });
CultNet selection cut1 fix batch: regenerate the C#-written parity vectors 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>
2026-09-22 09:40:18 +02:00
// R-C: BmpPrivateUseKey (U+E000) sorts before AstralKey (U+1F602) in code-point order,
// despite AstralKey's leading UTF-16 code unit (0xD83D) being numerically smaller.
yield return ("astral_key_code_point_order", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "unicode" } } }
});
// R-I: page bytes under each projection, over the same match set.
yield return ("header_projection", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "unicode" } } },
Projection = CultNetSelectionProjections.Header
});
yield return ("document_projection", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "unicode" } } },
Projection = CultNetSelectionProjections.Document
});
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
// R-AJ: a tied ordinal across two DIFFERENT schemas (kind=tie-schema is leaf_b/"zz-tie-
// schema" and leaf_a/"aa-tie-schema") - the shape the row order's schemaId tiebreak
// needs and the astral/BMP pair above never covered (both those rows are leaf_a). The
// fixture's keys are anti-correlated with the real schema ids on purpose, so a
// schemaId-first order and a recordKey-first order disagree: only the correct
// composition puts leaf_b's row ("zz-tie-schema") first.
yield return ("row_tiebreak_schema_id_before_record_key_at_tied_ordinal", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "tie-schema" } } }
});
// R-AF: "dup-key" carries two rows (leaf_a and leaf_b); citer_narrow's narrow_ref
// targets leaf_a only. Keys scopes the citer candidate set to narrow-citer-accepted
// alone, so this vector never touches narrow-citer-refused's bad edge (R-W resolves
// every reference member unconditionally, so leaving both citer_narrow rows in one
// candidate set would always refuse regardless of which edge the query named). One
// rule, derived from the wire (docs/cultnet-selection-cut.md, R-AF): resolving "the
// row this edge names" is by record key plus the declared leaf set, not an exact
// (schema, key) pair, so this must accept in both runtimes and resolve to the leaf_a
// row even though a leaf_b row shares the same key.
yield return ("shared_key_narrow_ref_resolves_leaf_within_target", new CultNetSelection
{
Keys = new[] { "narrow-citer-accepted" },
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].SchemaId, RecordKey = "dup-key" }, Role = "narrow_ref" }
});
// R-AO: "dup-key" has two in-target candidates for citer's Design role (unlike
// citer_narrow's narrow_ref above, which only ever had one). citer-both-in-target is the
// first row in the fixture file (and so in both runtimes' row order) to name it through
// Design - resolution must deterministically pick the first in-target candidate in that
// order, the leaf_a row (ordinal 11), never the leaf_b row (ordinal 12) sharing the key.
yield return ("shared_key_design_resolves_the_first_in_target_candidate_in_row_order", new CultNetSelection
{
Keys = new[] { "citer-both-in-target" },
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].SchemaId, RecordKey = "dup-key" }, Role = "Design" }
});
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
}
// R-AF: the shared-key shape's other direction - the only row at the declared edge's key is
// outside the reference's declared target, so evaluating the selection itself must refuse
// (CultNetSelectionReferenceOutsideTargetException/SelectionRefusal::ReferenceOutsideTarget),
// not merely return an empty page. Keys scopes the candidate set to narrow-citer-refused
// alone (see the comment on shared_key_narrow_ref_resolves_leaf_within_target above).
private static IEnumerable<(string Name, CultNetSelection Selection)> EvaluationRefusalCases(
IReadOnlyDictionary<string, FixtureSchema> schemas)
{
yield return ("shared_key_narrow_ref_refuses_when_only_candidate_is_outside_target", new CultNetSelection
{
Keys = new[] { "narrow-citer-refused" },
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].SchemaId, RecordKey = "narrow-only" }, Role = "narrow_ref" }
});
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
}
// Self's ruling, 2026-09-22 (commit 2 fix batch): door-refusal vectors. Never evaluated on
// either side - only decoded and handed to Validate/validate, which must refuse at exactly
// the named field. Mirrors packages/cultnet-rs/tests/selection.rs's refusal_cases.
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
//
Add the two pending cites-refusal parity vectors, now that C# refuses them Item 1 of the fix-batch-3 brief: check whether CultNetSelection.Validate refuses a cites.target.schemaId that CultNetSchemaAliasMatching cannot resolve. It already does (CultNetSelection.cs:389-394), landed by an earlier Hands pass on this branch (the "C# matrix Hands", after the parity table in the spec was captured) - so no production code changed here. What was still missing: SelectionParityVectorTests.RefusalCases() had a comment explaining why refuses_unmatched_cites_target_schema stayed "Rust-only", dated before that door check landed. The comment was stale and the vector was never added. Added it, plus refuses_cites_target_by_hash_shaped_alias (Self's ruling 2026-09-22: "the C# reference's alias rule is the rule" - C# only resolves a name-form alias, so a hash-shaped cites.target.schemaId is exactly the same unresolved-target case and must refuse at the same field). RefusalCases() now takes the fixture's schemas dictionary to reach schemas["leaf_a"].HashAlias. Verified on Yggdrasil: dotnet test tests/GameCult.Networking.Tests -c Release --filter FullyQualifiedName~SelectionParityVectorTests (the existing cross-runtime reader against the committed rs-written.json still passes, so this change does not depend on anything the Rust Hands owns). Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json with CULTNET_WRITE_VECTORS=1: both new refusal vectors are present and both refuse at "cites.target.schemaId" as expected before being committed as vectors (WriteSelectionVectors's own Assert.Throws gate). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:22:18 +02:00
// refuses_unmatched_cites_target_schema and refuses_cites_target_by_hash_shaped_alias now
// land here too: the C# matrix Hands pass added the door check
// (CultNetSelection.cs:389-394) that CultNetSelection.Validate refuses a cites.target.schemaId
// that CultNetSchemaAliasMatching cannot resolve to any declared schema. Under Self's ruling
// 2026-09-22 ("the C# reference's alias rule is the rule"), C# only ever resolves a name-form
// alias, never a hash-shaped one, so a hash-shaped cites.target.schemaId is exactly such an
// unresolved target and is refused at the same field.
private static IEnumerable<(string Name, CultNetSelection Selection, string ExpectedField)> RefusalCases(
IReadOnlyDictionary<string, FixtureSchema> schemas)
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
{
yield return ("refuses_empty_schemas_list", new CultNetSelection { Schemas = Array.Empty<string>() }, "schemas");
CultNet selection cut1, commit 4: the parity vectors, and the ruling this branch landed under 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>
2026-09-22 06:55:52 +02:00
yield return ("refuses_blank_key_entry", new CultNetSelection { Keys = new[] { "a-eq", " " } }, "keys");
Add the two pending cites-refusal parity vectors, now that C# refuses them Item 1 of the fix-batch-3 brief: check whether CultNetSelection.Validate refuses a cites.target.schemaId that CultNetSchemaAliasMatching cannot resolve. It already does (CultNetSelection.cs:389-394), landed by an earlier Hands pass on this branch (the "C# matrix Hands", after the parity table in the spec was captured) - so no production code changed here. What was still missing: SelectionParityVectorTests.RefusalCases() had a comment explaining why refuses_unmatched_cites_target_schema stayed "Rust-only", dated before that door check landed. The comment was stale and the vector was never added. Added it, plus refuses_cites_target_by_hash_shaped_alias (Self's ruling 2026-09-22: "the C# reference's alias rule is the rule" - C# only resolves a name-form alias, so a hash-shaped cites.target.schemaId is exactly the same unresolved-target case and must refuse at the same field). RefusalCases() now takes the fixture's schemas dictionary to reach schemas["leaf_a"].HashAlias. Verified on Yggdrasil: dotnet test tests/GameCult.Networking.Tests -c Release --filter FullyQualifiedName~SelectionParityVectorTests (the existing cross-runtime reader against the committed rs-written.json still passes, so this change does not depend on anything the Rust Hands owns). Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json with CULTNET_WRITE_VECTORS=1: both new refusal vectors are present and both refuse at "cites.target.schemaId" as expected before being committed as vectors (WriteSelectionVectors's own Assert.Throws gate). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:22:18 +02:00
yield return ("refuses_unmatched_cites_target_schema", new CultNetSelection
{
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = "sha256:not-a-declared-schema", RecordKey = "a-eq" } }
}, "cites.target.schemaId");
yield return ("refuses_cites_target_by_hash_shaped_alias", new CultNetSelection
{
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = schemas["leaf_a"].HashAlias, RecordKey = "a-eq" } }
}, "cites.target.schemaId");
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
}
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// R-E: id, not name - a row's cross-runtime identity is its real schema id plus its key, the
// same shape cultnet-rs's row_id() produces.
private static string RowId(CultNetSelectionEvaluator.Row row) => $"{row.Descriptor.SchemaId}/{row.Key.Value}";
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
// Vectors written by Rust (packages/cultnet-rs/tests/selection.rs,
// write_selection_vectors_for_the_reference) decode and evaluate identically here.
[Test]
public void SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference()
{
var path = Path.Combine(RepoRoot(), "contracts", "cultnet", "interop", "selection-vectors.rs-written.json");
if (!File.Exists(path))
Assert.Inconclusive($"{path} is missing - run cargo test -p cultnet-rs --test selection with CULTNET_WRITE_VECTORS=1 first.");
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
var (registry, rows, _) = BuildFixture();
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
using var document = JsonDocument.Parse(File.ReadAllText(path));
var vectors = document.RootElement.GetProperty("vectors");
Assert.That(vectors.GetArrayLength(), Is.GreaterThan(0), "the vector file must carry at least one vector");
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// Self's ruling, 2026-09-22 (shared-fixture ruling): a real parity defect is reported in
// full, not truncated by whichever vector happened to come first in the file - every
// vector is judged and every mismatch collected before the test fails once, mirroring
// packages/cultnet-rs/tests/selection.rs's own reader.
var failures = new List<string>();
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
foreach (var vector in vectors.EnumerateArray())
{
var name = vector.GetProperty("name").GetString();
var selectionBytes = Convert.FromBase64String(vector.GetProperty("selectionMessagePackBase64").GetString()!);
var selection = MessagePackSerializer.Deserialize<CultNetSelection>(selectionBytes, CultNetSchemaMessageSerialization.Options);
var asOf = vector.GetProperty("asOf").GetUInt64();
if (vector.TryGetProperty("expectedRefusalField", out var refusalField) && refusalField.ValueKind != JsonValueKind.Null)
{
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
try
{
selection.Validate(registry.AllDescriptors.ToArray());
failures.Add($"{name}: expected refusal at '{refusalField.GetString()}' but the reference accepted it");
}
catch (CultNetSelectionInvalidException error)
{
if (error.Field != refusalField.GetString())
failures.Add($"{name}: refusal field - expected '{refusalField.GetString()}', got '{error.Field}'");
}
continue;
}
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
// R-AF: a small, separate signal from expectedRefusalField above - that one is the
// door (Validate, never touches Select); this one names an evaluation-time typed
// refusal Select itself must throw (currently only reference_outside_target, S18).
// Deliberately narrower than R-Z/R-AH's page-bytes-and-code parity harness (not
// this cut's job): it checks which typed refusal fires, nothing about wire bytes.
if (vector.TryGetProperty("expectedEvaluationRefusal", out var evalRefusal) && evalRefusal.ValueKind != JsonValueKind.Null)
{
var expectedKind = evalRefusal.GetString();
try
{
CultNetSelectionEvaluator.Select(registry, rows, selection, asOf);
failures.Add($"{name}: expected Select to refuse ({expectedKind}) but it accepted the selection");
}
catch (CultNetSelectionReferenceOutsideTargetException) when (expectedKind == "reference_outside_target")
{
// expected
}
catch (Exception error)
{
failures.Add($"{name}: expected Select to refuse with {expectedKind}, got {error.GetType().Name}: {error.Message}");
}
continue;
}
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
CultNetSelectionEvaluator.Evaluation evaluation;
try
{
evaluation = CultNetSelectionEvaluator.Select(registry, rows, selection, asOf);
}
catch (Exception error)
{
var expectedIds = vector.GetProperty("expectedIds").EnumerateArray().Select(e => e.GetString()).ToArray();
failures.Add($"{name}: expected real evaluation (expectedIds=[{string.Join(", ", expectedIds)}]) but Select threw {error.GetType().Name}: {error.Message}");
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
continue;
}
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
var expectedIdsList = vector.GetProperty("expectedIds").EnumerateArray().Select(e => e.GetString()).ToArray();
var actualIds = evaluation.Rows.Select(RowId).ToArray();
if (!actualIds.SequenceEqual(expectedIdsList))
failures.Add($"{name}: row ids - expected [{string.Join(", ", expectedIdsList)}], got [{string.Join(", ", actualIds)}]");
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
var expectedMatched = vector.GetProperty("matched").GetUInt32();
if ((uint)evaluation.TotalMatched != expectedMatched)
failures.Add($"{name}: matched - expected {expectedMatched}, got {evaluation.TotalMatched}");
var expectedHasNext = vector.GetProperty("hasNext").GetBoolean();
if ((evaluation.NextCursor != null) != expectedHasNext)
failures.Add($"{name}: hasNext - expected {expectedHasNext}, got {evaluation.NextCursor != null}");
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
var expectedEdges = vector.GetProperty("expectedEdges").EnumerateArray().ToArray();
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
if (evaluation.Edges.Count != expectedEdges.Length)
{
failures.Add($"{name}: edge count - expected {expectedEdges.Length}, got {evaluation.Edges.Count}");
continue;
}
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
for (var i = 0; i < expectedEdges.Length; i++)
{
var edge = evaluation.Edges[i];
var expectedEdge = expectedEdges[i];
var expectedPayload = expectedEdge.GetProperty("payloadBase64");
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
var actualPayload = edge.Payload == null ? null : Convert.ToBase64String((byte[])edge.Payload);
var payloadMatches = expectedPayload.ValueKind == JsonValueKind.Null
? actualPayload == null
: actualPayload == expectedPayload.GetString();
if (RowId(edge.From) != expectedEdge.GetProperty("fromId").GetString() ||
edge.Role != expectedEdge.GetProperty("role").GetString() ||
RowId(edge.To) != expectedEdge.GetProperty("toId").GetString() ||
!payloadMatches)
{
failures.Add($"{name}: edge[{i}] mismatch");
}
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
}
}
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
Assert.That(failures, Is.Empty,
$"{failures.Count} of {vectors.GetArrayLength()} vectors disagreed with the reference:\n" + string.Join("\n", failures));
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
}
// Writes contracts/cultnet/interop/selection-vectors.cs-written.json for Rust's
// selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust to
// judge. Gated the same way the QUIC campaign's C#-written vectors are
// (docs/typescript-quic-realtime-cut.md:15-21): committed, not regenerated on every run.
[Test]
public void WriteSelectionVectors()
{
if (Environment.GetEnvironmentVariable("CULTNET_WRITE_VECTORS") != "1")
Assert.Ignore("Set CULTNET_WRITE_VECTORS=1 to (re)write the committed vector file.");
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
var (registry, rows, schemas) = BuildFixture();
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
var vectors = new List<object>();
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
foreach (var (name, selection) in Cases(schemas))
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
{
var evaluation = CultNetSelectionEvaluator.Select(registry, rows, selection, asOf: 1);
var selectionBytes = MessagePackSerializer.Serialize(selection, CultNetSchemaMessageSerialization.Options);
vectors.Add(new
{
name,
selectionMessagePackBase64 = Convert.ToBase64String(selectionBytes),
asOf = 1UL,
expectedRefusalField = (string?)null,
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
expectedEvaluationRefusal = (string?)null,
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
expectedIds = evaluation.Rows.Select(RowId).ToArray(),
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
matched = (uint)evaluation.TotalMatched,
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
hasNext = evaluation.NextCursor != null,
expectedEdges = evaluation.Edges.Select(edge => new
{
fromId = RowId(edge.From),
role = edge.Role,
toId = RowId(edge.To),
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// None of this file's cases hop through a many-dictionary reference (the only
// shape D11 gives a non-null payload), so this is always null here;
// packages/cultnet-rs/tests/selection.rs's
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
// dictionary_reference_entries_keep_distinct_payload_bytes pins the
// payload-carrying case directly against Row::references() instead.
payloadBase64 = edge.Payload == null ? null : Convert.ToBase64String((byte[])edge.Payload)
}).ToArray()
});
}
// Self's ruling, 2026-09-22 (commit 2 fix batch): door-refusal vectors. Never
// evaluated on either side - only decoded and handed to Validate, which must refuse
// at exactly the named field.
Add the two pending cites-refusal parity vectors, now that C# refuses them Item 1 of the fix-batch-3 brief: check whether CultNetSelection.Validate refuses a cites.target.schemaId that CultNetSchemaAliasMatching cannot resolve. It already does (CultNetSelection.cs:389-394), landed by an earlier Hands pass on this branch (the "C# matrix Hands", after the parity table in the spec was captured) - so no production code changed here. What was still missing: SelectionParityVectorTests.RefusalCases() had a comment explaining why refuses_unmatched_cites_target_schema stayed "Rust-only", dated before that door check landed. The comment was stale and the vector was never added. Added it, plus refuses_cites_target_by_hash_shaped_alias (Self's ruling 2026-09-22: "the C# reference's alias rule is the rule" - C# only resolves a name-form alias, so a hash-shaped cites.target.schemaId is exactly the same unresolved-target case and must refuse at the same field). RefusalCases() now takes the fixture's schemas dictionary to reach schemas["leaf_a"].HashAlias. Verified on Yggdrasil: dotnet test tests/GameCult.Networking.Tests -c Release --filter FullyQualifiedName~SelectionParityVectorTests (the existing cross-runtime reader against the committed rs-written.json still passes, so this change does not depend on anything the Rust Hands owns). Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json with CULTNET_WRITE_VECTORS=1: both new refusal vectors are present and both refuse at "cites.target.schemaId" as expected before being committed as vectors (WriteSelectionVectors's own Assert.Throws gate). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:22:18 +02:00
foreach (var (name, selection, expectedField) in RefusalCases(schemas))
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
{
var error = Assert.Throws<CultNetSelectionInvalidException>(
() => selection.Validate(registry.AllDescriptors.ToArray()),
$"{name} must be refused by the reference before it can be committed as a refusal vector");
Assert.That(error!.Field, Is.EqualTo(expectedField), $"{name}: refusal field");
var selectionBytes = MessagePackSerializer.Serialize(selection, CultNetSchemaMessageSerialization.Options);
vectors.Add(new
{
name,
selectionMessagePackBase64 = Convert.ToBase64String(selectionBytes),
asOf = 1UL,
expectedRefusalField = (string?)expectedField,
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
expectedEvaluationRefusal = (string?)null,
expectedIds = Array.Empty<string>(),
matched = 0U,
hasNext = false,
expectedEdges = Array.Empty<object>()
});
}
// R-AF: evaluation-time refusal vectors. Never handed to Validate - the selection
// decodes and passes the door; only Select itself refuses, and only once the shared-key
// resolution rule actually runs. Asserted here before being committed, mirroring how
// RefusalCases above asserts Validate refuses before its vectors are written.
foreach (var (name, selection) in EvaluationRefusalCases(schemas))
{
Assert.Throws<CultNetSelectionReferenceOutsideTargetException>(
() => CultNetSelectionEvaluator.Select(registry, rows, selection, asOf: 1),
$"{name} must be refused by Select before it can be committed as an evaluation-refusal vector");
var selectionBytes = MessagePackSerializer.Serialize(selection, CultNetSchemaMessageSerialization.Options);
vectors.Add(new
{
name,
selectionMessagePackBase64 = Convert.ToBase64String(selectionBytes),
asOf = 1UL,
expectedRefusalField = (string?)null,
expectedEvaluationRefusal = (string?)"reference_outside_target",
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
expectedIds = Array.Empty<string>(),
matched = 0U,
hasNext = false,
expectedEdges = Array.Empty<object>()
});
}
var directory = Path.Combine(RepoRoot(), "contracts", "cultnet", "interop");
Directory.CreateDirectory(directory);
var json = JsonSerializer.Serialize(new { vectors }, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(Path.Combine(directory, "selection-vectors.cs-written.json"), json);
}
CultNet selection cut1: the shared C#/Rust parity fixture 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>
2026-09-22 11:33:29 +02:00
// Dumps each fixture schema's real, reference-computed id (and lets a maintainer derive the
// unversioned alias by stripping the trailing ".v1") so
// contracts/cultnet/interop/selection-vectors.fixture.json can be regenerated after a shape
// change. Not part of the parity contract itself.
[Test]
public void DumpFixtureSchemaIds()
{
if (Environment.GetEnvironmentVariable("CULTNET_DUMP_SCHEMA_IDS") != "1")
Assert.Ignore("Set CULTNET_DUMP_SCHEMA_IDS=1 to print the fixture's real schema ids.");
var registry = Registry();
Console.WriteLine("SCHEMA_ID leaf_a=" + registry.GetRequired(typeof(ParityLeafA)).SchemaId);
Console.WriteLine("SCHEMA_ID leaf_b=" + registry.GetRequired(typeof(ParityLeafB)).SchemaId);
Console.WriteLine("SCHEMA_ID citer=" + registry.GetRequired(typeof(ParityCiter)).SchemaId);
}
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
private static string RepoRoot()
{
var directory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);
while (directory != null && !File.Exists(Path.Combine(directory.FullName, "CultLib.sln")))
directory = directory.Parent;
return directory?.FullName ?? throw new InvalidOperationException("CultLib.sln was not found above the test directory.");
}
// A conceptual abstract middle (no [CultDocument]), matching Design's declared target so
// D9's leaf-set resolution reaches both ParityLeafA and ParityLeafB. SchemaName is the
// literal fixture-identity string ("leaf_a", "leaf_b", "citer") the vectors compare by -
// see this file's header comment.
public abstract class ParityMiddle
{
[Key(0)]
[CultName]
public string Name = string.Empty;
[Key(1)]
[CultIndex("kind")]
public string Kind = string.Empty;
[Key(2)]
[CultIndex("mass")]
public float Mass;
}
[CultDocument("leaf_a", "cultnet.selection-parity.leaf_a.v1")]
[MessagePackObject(AllowPrivate = true)]
public sealed class ParityLeafA : ParityMiddle
{
}
[CultDocument("leaf_b", "cultnet.selection-parity.leaf_b.v1")]
[MessagePackObject(AllowPrivate = true)]
public sealed class ParityLeafB : ParityMiddle
{
}
[CultDocument("citer", "cultnet.selection-parity.citer.v1")]
[MessagePackObject(AllowPrivate = true)]
public sealed class ParityCiter
{
[Key(0)]
[CultName]
public string Name = string.Empty;
[Key(1)]
[CultReference(typeof(ParityMiddle))]
public CultRecordRef<ParityMiddle> Design;
}
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
// R-AF: a citer whose declared reference target is one leaf only, so a record key shared
// by a ParityLeafA row and a ParityLeafB row has exactly one row inside the declared
// target and exactly one outside it - see the fixture file's "citer_narrow" schema.
[CultDocument("citer_narrow", "cultnet.selection-parity.citer_narrow.v1")]
[MessagePackObject(AllowPrivate = true)]
public sealed class ParityCiterNarrow
{
[Key(0)]
[CultName]
public string Name = string.Empty;
[Key(1)]
[CultIndex("narrow_ref")]
[CultReference(typeof(ParityLeafA))]
public CultRecordRef<ParityLeafA> NarrowRef;
}
CultNet selection cut1, commit 3: the Rust runtime docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs (new) - the Rust spelling of the vocabulary at wire parity with the corrected C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner supplies declared values and edges through two traits. - Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge, RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/ schemaContentHash - a pre-existing v0 gap this cut does not touch). SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics; FieldPredicate.op stays a raw wire string, never a serde-tagged union. - canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a hand-rolled digit-grammar parser, no regex dependency added), canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then integer length, then digits, then padded fraction - no f64 anywhere), and render_i64/render_u64/render_f64/render_f32 for a Row implementer's own rendering. probe_f64_display_never_uses_exponent_notation empirically verifies the map's claim about Rust's f64 Display (confirmed true) rather than trusting it. - Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number (canonical string, never f64), references for Row; all_schema_ids, declared_indexes, declared_roles, is_numeric, target_leaves for RowSet. A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of references satisfy generic bounds without an extra copy. - validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing selections assert via debug_assert since Rust has no exception type to mirror C#'s InvalidOperationException), select (order/hop/cursor/paging, D9's reference_outside_target refusal, S23's per-row match - never through a unique-index map, because there is none here to begin with). - Cursor: mint/parse/digest is this runtime's own internal algorithm (a cursor is opaque and never crosses runtimes - each server answers its own pages), not a byte-for-byte port of the reference's; what must match is the *rule* (asOf + last position + a digest bound to the selection), which it does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert. - contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1, DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages' field-for-field shape. SnapshotResponseRawV1 carries binary payloads (documents[].payload, edges[].payload), so it is routed through the existing raw JSON-bypass path (parse_raw_cultnet_schema_message/ encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/ SnapshotResponseRaw, with its own require_*/encode_* helpers for SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef. SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the ordinary serde_json-intermediate path. No message-layer selection semantics validation here, matching the reference (CultNetSchemaMessageSerialization.cs only deserializes; a server calls selection::validate against its own RowSet before evaluating). - snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled allowlist filters collapse into one call to selection::matches over a lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose ordinal/values/number/references are unreachable (the lowered selection never sets fields/cites/cited, so matches_schema_keys_fields never calls them) - documented, not hidden. Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch, mirrored here per the coordinator's mid-task instruction rather than C#'s pre-fix spelling as it stood at e5348d3): - validate refuses an empty-but-present schemas/keys list (already had this) and now also refuses any individual entry that is empty or whitespace (fields[i]-style names: schemas[i]/keys[i]). - The evaluator never sees []; matches_schema_keys_fields does not special-case an empty list as "no filter" (it never did here - that C# fallback, CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust). - snapshot_query.rs's v0 lowering gets its own cleaning, owned by the lowering and not the evaluator: lower_v0_list turns an empty or all-blank v0 schemaIds/recordKeys list into None before it ever reaches validate or select. Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut; +62 across lib unit tests, tests/selection.rs and tests/cultnet.rs). cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit - confirmed unaffected). GameCult.Networking.Tests (the new SelectionParityVectorTests.cs file, compiled but not yet fully exercised - see below) builds clean. Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet- selection-rust.entries.ps1): 22 entries against src/selection.rs and src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and new blank-entry refusals (schemas and keys, revert x4), order/descending (revert), cursor staleness (revert + loosening) and digest (revert), S7's incoming negation (revert + loosening), S16's le/ge boundaries (revert x2), S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion (revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's integer-through-float64 renderer, decimal trailing-zero stripping and exponent expansion (revert x3, the last two pinned via direct canonical_number unit tests since render_f64 itself cannot reach exponent notation given Rust's Display contract - verified, not assumed), and the new v0-lowering rule (revert). Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty. Not included here, landing as commit 4 once ready: the committed parity vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json). Both directions already work for six evaluated selections and the [] door-refusal case; the blank-key-entry refusal vector cannot be generated from the C# side yet because the parallel C# fix batch implementing that door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit 2 fix batch") has not landed on this branch as of this commit. Rust's own tests (validation_refuses_a_blank_entry_in_schemas_or_keys, serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter) already pin both new rules independently of C#. Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new), contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new), tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new). Against section 14's ~430-line estimate for selection.rs+contracts.rs: the two together land at ~2000 lines added, well over estimate - the map's own §14 correction already flagged commits 0/1 running roughly 2x estimate for the same reason (the hop's incoming index, the cursor, and per-field door messages cost more than the prose assumed), and the v1 raw-message handling (SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers, ~400 lines) was not separately budgeted at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:31:19 +02:00
}
}