CultLib/packages/cultnet-rs/tests/selection.rs

2406 lines
108 KiB
Rust
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
//! CultNet typed selection, Cut 1 commit 3 (docs/cultnet-selection-cut.md section 7/10). A toy row
//! set implementing `Row`/`RowSet` directly - D7: no `cultcache-rs` knowledge, matching S14's brief
//! ("select_over_a_toy_row_set_matches_orders_hops_pages_and_refuses") rather than reproducing every
//! one of C#'s S1-S23 fixtures 1:1. Section 10's negative greps and the parity vectors (S12, S13) are
//! here too.
use std::fs;
use std::path::Path;
use cultnet_rs::{
Citation, Cursor, CursorKey, CultNetMessage, CultNetWireContract, Edge, FieldPredicate, Incoming,
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
RawDocumentHeader, RecordRef, Row, RowSet, Selection, SelectionDocumentRecord, SelectionOperator,
SelectionPage, SelectionRefusal, canonical_number, decode_cultnet_message_from_slice,
encode_cultnet_message_to_vec, schema_alias, select, select_page, validate,
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
};
/// R-O: one process-lifetime key shared by every test in this binary, so a cursor minted in one
/// `select`/`select_page` call verifies in a later call of the same test.
fn test_cursor_key() -> &'static CursorKey {
static KEY: std::sync::OnceLock<CursorKey> = std::sync::OnceLock::new();
KEY.get_or_init(CursorKey::random)
}
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
// ------------------------------------------------------------------------------------------
// The fixture: two leaves under a conceptual abstract middle (mass declared on both,
// independently - Rust has no inheritance to share it through), a citer with four reference
// shapes (a single "Design" reference into the abstract middle's leaf set, a "related" many
// reference, a "components" many-dictionary reference with float payloads, and a "parent"
// single reference), and a narrow citer whose "narrow_ref" targets only leaf_a - the edge S18
// needs to reach outside its declared target.
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
//
// Soul's finding (soul-ss4-notes.md, F3): the earlier fixture used the schema *names* ("leaf_a",
// "leaf_b", ...) as their own schema ids, so every `schemas`/`cites.target` selection matched by
// plain string equality and never exercised the alias matcher at all - the real gap (C# resolves
// `["leaf_a"]` against a schema's declared name; Rust's exact-id compare gave 0 rows) was
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
// invisible. leaf_a()/leaf_b()/citer() below are real SHA-256-shaped ids
// (`sha256:<hex>`, no version suffix of their own - see the fixture-loading block's comment) - the
// same ids `CultCache.cs`'s `Sha256(semanticFingerprint)` produces for the shared fixture's C#
// document types. NARROW_CITER (just below) is the one id this file still invents locally; it is
// never compared across runtimes.
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 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
// R-E/shared-fixture ruling (docs/cultnet-selection-cut.md, Self's rulings for the Cut 1 fix batch,
// 2026-09-22): leaf_a/leaf_b/citer's real ids come from the one shared fixture,
// contracts/cultnet/interop/selection-vectors.fixture.json, loaded lazily below - real SHA-256
// content-hash ids, exactly as GameCult.Caching.CultDocumentRegistry computes them for the
// fixture's C# document types (tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs). They
// carry no ".vN" suffix of their own - CultCache.cs's Sha256(semanticFingerprint) folds the version
// into the hash rather than appending it as text. leaf_a_hash_alias() is the same hash with a
// synthetic ".v1" appended, the one alias form this crate's own schema_alias module can resolve
// (see its module doc); leaf_a_name_alias() is the one alias form the C# reference can resolve
// instead ("leaf_a.v9") - the two runtimes' alias matchers key off different attributes of a
// descriptor and neither candidate resolves through both. NARROW_CITER stays a local, unshared id:
// it only ever appears in this file's own S18 test, never in a cross-runtime vector.
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
const NARROW_CITER: &str = "sha256:356840f80f14bda0186a33eb7aed41c1e644fd4ffc3b925ad0d7f10fe77b52cb.v1";
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
#[derive(serde::Deserialize)]
struct FixtureSchemaDef {
#[serde(rename = "schemaId")]
schema_id: String,
#[serde(rename = "nameAlias")]
name_alias: String,
#[serde(rename = "hashAlias")]
hash_alias: String,
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF: no target_schema - 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.
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
#[derive(serde::Deserialize)]
struct FixtureReferenceDef {
role: String,
#[serde(rename = "targetKey")]
target_key: String,
}
#[derive(serde::Deserialize)]
struct FixtureRowDef {
schema: String,
key: String,
ordinal: i64,
#[serde(default)]
fields: std::collections::BTreeMap<String, String>,
#[serde(default)]
references: Vec<FixtureReferenceDef>,
}
#[derive(serde::Deserialize)]
struct FixtureFile {
schemas: std::collections::BTreeMap<String, FixtureSchemaDef>,
rows: Vec<FixtureRowDef>,
}
static FIXTURE: std::sync::OnceLock<FixtureFile> = std::sync::OnceLock::new();
Rust R-AB: vendor the parity fixture too, so cargo-mutants' baseline actually runs The schema_discovery.rs include_str! escape was not the only one: tests/selection.rs's fixture() read ../../contracts/cultnet/interop/selection-vectors.fixture.json, the same three-levels-up path shape, and 37 of 45 tests panicked on it under cargo-mutants' isolated copy - confirmed on Yggdrasil after the schema_discovery.rs fix alone still landed "cargo test failed in an unmutated tree, so no mutants were tested". selection-vectors.fixture.json is a checked-in input (not a generated artifact), so it gets the same treatment as the 22 schemas: a byte-identical vendored copy under packages/cultnet-rs/contracts/cultnet/interop/, a drift test (fixture_contract_matches_the_canonical_source, same skip-when-absent shape as schema_discovery_contracts.rs), and the loader reads the vendored copy unconditionally. selection-vectors.cs-written.json is different in kind: it is the C# reference's own generated output, produced only by a real cross-runtime checkout running SelectionParityVectorTests with CULTNET_WRITE_VECTORS=1. There is nothing to vendor - an isolated Rust-only copy can never satisfy that test's premise regardless of what this crate does with its own files. That one test now skips (with an explanation) when the canonical contracts/ tree is entirely absent, rather than panicking and failing the whole suite over a cross-runtime artifact the isolated copy cannot produce by construction. Verified locally by hiding contracts/ entirely and rerunning the full suite (217 tests across every binary): unchanged pass count, same as with it present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:11:35 +02:00
// R-AB: `fixture()` used to read only `contracts_dir()` (the canonical, repo-root
// `contracts/cultnet/interop/`), which is outside `packages/cultnet-rs` and escapes any tool that
// copies just this package's directory - the same failure shape `schema_discovery.rs`'s
// `include_str!`s had. `selection-vectors.fixture.json` is a checked-in input, not a generated
// artifact (unlike `*-written.json` below, which each runtime produces from the *other* runtime's
// output and cannot be vendored the same way), so it gets the same vendored-copy treatment: a
// byte-identical copy lives under `packages/cultnet-rs/contracts/cultnet/interop/`, checked by the
// drift test right below, and the loader reads the vendored copy unconditionally so it works the
// same way whether or not the wider repo is checked out beside the package.
fn vendored_fixture_dir() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("contracts").join("cultnet").join("interop")
}
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
fn fixture() -> &'static FixtureFile {
FIXTURE.get_or_init(|| {
Rust R-AB: vendor the parity fixture too, so cargo-mutants' baseline actually runs The schema_discovery.rs include_str! escape was not the only one: tests/selection.rs's fixture() read ../../contracts/cultnet/interop/selection-vectors.fixture.json, the same three-levels-up path shape, and 37 of 45 tests panicked on it under cargo-mutants' isolated copy - confirmed on Yggdrasil after the schema_discovery.rs fix alone still landed "cargo test failed in an unmutated tree, so no mutants were tested". selection-vectors.fixture.json is a checked-in input (not a generated artifact), so it gets the same treatment as the 22 schemas: a byte-identical vendored copy under packages/cultnet-rs/contracts/cultnet/interop/, a drift test (fixture_contract_matches_the_canonical_source, same skip-when-absent shape as schema_discovery_contracts.rs), and the loader reads the vendored copy unconditionally. selection-vectors.cs-written.json is different in kind: it is the C# reference's own generated output, produced only by a real cross-runtime checkout running SelectionParityVectorTests with CULTNET_WRITE_VECTORS=1. There is nothing to vendor - an isolated Rust-only copy can never satisfy that test's premise regardless of what this crate does with its own files. That one test now skips (with an explanation) when the canonical contracts/ tree is entirely absent, rather than panicking and failing the whole suite over a cross-runtime artifact the isolated copy cannot produce by construction. Verified locally by hiding contracts/ entirely and rerunning the full suite (217 tests across every binary): unchanged pass count, same as with it present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:11:35 +02:00
let path = vendored_fixture_dir().join("selection-vectors.fixture.json");
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let text = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("{} is missing or unreadable: {error}", path.display()));
serde_json::from_str(&text)
.unwrap_or_else(|error| panic!("{} is not valid fixture JSON: {error}", path.display()))
})
}
Rust R-AB: vendor the parity fixture too, so cargo-mutants' baseline actually runs The schema_discovery.rs include_str! escape was not the only one: tests/selection.rs's fixture() read ../../contracts/cultnet/interop/selection-vectors.fixture.json, the same three-levels-up path shape, and 37 of 45 tests panicked on it under cargo-mutants' isolated copy - confirmed on Yggdrasil after the schema_discovery.rs fix alone still landed "cargo test failed in an unmutated tree, so no mutants were tested". selection-vectors.fixture.json is a checked-in input (not a generated artifact), so it gets the same treatment as the 22 schemas: a byte-identical vendored copy under packages/cultnet-rs/contracts/cultnet/interop/, a drift test (fixture_contract_matches_the_canonical_source, same skip-when-absent shape as schema_discovery_contracts.rs), and the loader reads the vendored copy unconditionally. selection-vectors.cs-written.json is different in kind: it is the C# reference's own generated output, produced only by a real cross-runtime checkout running SelectionParityVectorTests with CULTNET_WRITE_VECTORS=1. There is nothing to vendor - an isolated Rust-only copy can never satisfy that test's premise regardless of what this crate does with its own files. That one test now skips (with an explanation) when the canonical contracts/ tree is entirely absent, rather than panicking and failing the whole suite over a cross-runtime artifact the isolated copy cannot produce by construction. Verified locally by hiding contracts/ entirely and rerunning the full suite (217 tests across every binary): unchanged pass count, same as with it present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:11:35 +02:00
// The drift guard for the vendored fixture, mirroring
// `schema_discovery_contracts.rs`'s vendored-schema check: skips (not fails) when the canonical
// repo-root tree is not present beside the package, which is exactly the isolated-copy case the
// vendored copy exists to survive.
#[test]
fn fixture_contract_matches_the_canonical_source() {
let canonical = contracts_dir().join("selection-vectors.fixture.json");
if !canonical.is_file() {
eprintln!(
"skipping: {canonical:?} not present beside the package (isolated build, the case \
the vendored fixture copy is for)"
);
return;
}
let canonical_text = fs::read_to_string(&canonical).expect("reading canonical fixture");
let vendored_text = fs::read_to_string(vendored_fixture_dir().join("selection-vectors.fixture.json"))
.expect("reading vendored fixture");
assert_eq!(
canonical_text, vendored_text,
"packages/cultnet-rs/contracts/cultnet/interop/selection-vectors.fixture.json has \
drifted from the canonical contracts/cultnet/interop/selection-vectors.fixture.json - \
re-copy it"
);
}
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
fn fixture_schema(name: &str) -> &'static FixtureSchemaDef {
fixture()
.schemas
.get(name)
.unwrap_or_else(|| panic!("selection-vectors.fixture.json: no schema named '{name}'"))
}
fn leaf_a() -> &'static str {
&fixture_schema("leaf_a").schema_id
}
fn leaf_b() -> &'static str {
&fixture_schema("leaf_b").schema_id
}
fn citer() -> &'static str {
&fixture_schema("citer").schema_id
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF: the shared fixture's narrow-target citer - real SHA-256 id, same treatment as
// leaf_a()/leaf_b()/citer() above, so the shared-key resolution rule is pinned in the actual
// cross-runtime parity vectors rather than only in this file's own local NARROW_CITER tests.
fn citer_narrow() -> &'static str {
&fixture_schema("citer_narrow").schema_id
}
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// R-E/shared-fixture ruling: the fixture's map key ("leaf_a", "leaf_b", "citer") *is* each
// schema's declared name (C#'s CultDocumentDescriptor.SchemaName) - what RowSet::schema_name/
// Row::schema_name now expose, and what nameAlias's ".v9" suffix resolves against.
fn leaf_a_name() -> &'static str {
"leaf_a"
}
fn leaf_b_name() -> &'static str {
"leaf_b"
}
fn citer_name() -> &'static str {
"citer"
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
fn citer_narrow_name() -> &'static str {
"citer_narrow"
}
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
const NARROW_CITER_NAME: &str = "narrow_citer";
// R-E, Self's ruling 2026-09-22 ("the C# reference's alias rule is the rule"): a hash id with a
// synthetic ".v1" appended is not a wire form either runtime's production rule resolves -
// CultNetSchemaAliasMatching's descriptor overload strips the ".v1" and compares the bare hash
// text against the declared *name* ("leaf_a"), never equal. This is now a shared negative check
// (see does_not_match_a_hash_shaped_alias below and the parity vectors this file writes).
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
fn leaf_a_hash_alias() -> &'static str {
&fixture_schema("leaf_a").hash_alias
}
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// The alias form the C# reference's descriptor overload resolves ("leaf_a.v9") - and, as of this
// cut's fix, the alias form this crate's schema_alias resolves too, through Row::schema_name/
// RowSet::schema_name now standing in for CultDocumentDescriptor.SchemaName.
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
fn leaf_a_name_alias() -> &'static str {
&fixture_schema("leaf_a").name_alias
}
fn leak(value: String) -> &'static str {
Box::leak(value.into_boxed_str())
}
fn build_row(row: &FixtureRowDef) -> FixtureRow {
match row.schema.as_str() {
"leaf_a" | "leaf_b" => {
let schema_id = if row.schema == "leaf_a" { leaf_a() } else { leaf_b() };
FixtureRow::leaf(
schema_id,
leak(row.key.clone()),
row.ordinal,
leak(row.fields["kind"].clone()),
leak(row.fields["mass"].clone()),
)
}
"citer" => {
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF: the stored edge is a bare target key - the wire carries no schema for it.
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let references = row
.references
.iter()
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
.map(|reference| (reference.role.clone(), reference.target_key.clone(), None))
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
.collect();
FixtureRow::citer(leak(row.key.clone()), row.ordinal, references)
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
"citer_narrow" => {
let references = row
.references
.iter()
.map(|reference| (reference.role.clone(), reference.target_key.clone(), None))
.collect();
FixtureRow::citer_narrow(leak(row.key.clone()), row.ordinal, references)
}
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
other => panic!("selection-vectors.fixture.json: unknown schema '{other}'"),
}
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
#[derive(Clone, Debug)]
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
struct FixtureRow {
schema_id: &'static str,
record_key: &'static str,
ordinal: i64,
kind: Option<&'static str>,
mass: Option<&'static str>,
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF: the stored target is a bare record key, not a RecordRef - see Row::references's doc
// comment in src/selection.rs.
references: Vec<(String, String, Option<Vec<u8>>)>,
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
}
impl FixtureRow {
fn leaf(schema_id: &'static str, record_key: &'static str, ordinal: i64, kind: &'static str, mass: &'static str) -> Self {
Self {
schema_id,
record_key,
ordinal,
kind: Some(kind),
mass: Some(mass),
references: Vec::new(),
}
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
fn citer(record_key: &'static str, ordinal: i64, references: Vec<(String, String, Option<Vec<u8>>)>) -> Self {
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 {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schema_id: citer(),
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
record_key,
ordinal,
kind: None,
mass: None,
references,
}
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF: the shared fixture's narrow-target citer (real schema id, from
// selection-vectors.fixture.json) - distinct from this file's own local NARROW_CITER, which
// stays a Rust-only id never compared across runtimes.
fn citer_narrow(record_key: &'static str, ordinal: i64, references: Vec<(String, String, Option<Vec<u8>>)>) -> Self {
Self {
schema_id: citer_narrow(),
record_key,
ordinal,
kind: None,
mass: None,
references,
}
}
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: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// Shared by Row::schema_name and RowSet::schema_name below - one place that knows which fixture
// schema id carries which declared name.
fn schema_name_for(schema_id: &str) -> Option<&'static str> {
if schema_id == leaf_a() {
Some(leaf_a_name())
} else if schema_id == leaf_b() {
Some(leaf_b_name())
} else if schema_id == citer() {
Some(citer_name())
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
} else if schema_id == citer_narrow() {
Some(citer_narrow_name())
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
} else if schema_id == NARROW_CITER {
Some(NARROW_CITER_NAME)
} else {
None
}
}
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
impl Row for FixtureRow {
fn schema_id(&self) -> &str {
self.schema_id
}
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
fn schema_name(&self) -> &str {
schema_name_for(self.schema_id).unwrap_or(self.schema_id)
}
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
fn record_key(&self) -> &str {
self.record_key
}
fn ordinal(&self) -> i64 {
self.ordinal
}
fn values(&self, index: &str) -> Vec<String> {
match index {
"kind" => self.kind.map(|v| vec![v.to_string()]).unwrap_or_default(),
_ => Vec::new(),
}
}
fn number(&self, index: &str) -> Option<String> {
if index == "mass" {
self.mass.map(str::to_string)
} else {
None
}
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
fn references(&self) -> Vec<(String, String, Option<Vec<u8>>)> {
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.references.clone()
}
}
struct FixtureRowSet;
impl RowSet for FixtureRowSet {
fn all_schema_ids(&self) -> Vec<String> {
vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
leaf_a().into(),
leaf_b().into(),
citer().into(),
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
citer_narrow().into(),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
NARROW_CITER.into(),
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: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
fn schema_name(&self, schema_id: &str) -> Option<String> {
schema_name_for(schema_id).map(str::to_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
fn declared_indexes(&self, schema_id: &str) -> Vec<String> {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
if schema_id == leaf_a() || schema_id == leaf_b() {
vec!["kind".into(), "mass".into()]
} else {
Vec::new()
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
}
}
fn declared_roles(&self, schema_id: &str) -> Vec<String> {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
if schema_id == citer() {
vec![
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
"parent".into(),
"related".into(),
"components".into(),
"Design".into(),
// R-W: a citer-to-citer role, so a both-hops selection can page a citer row that
// is itself cited by another citer (rather than only ever by a leaf).
"peer".into(),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
]
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
} else if schema_id == NARROW_CITER || schema_id == citer_narrow() {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
vec!["narrow_ref".into()]
} else {
Vec::new()
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
}
}
fn is_numeric(&self, schema_id: &str, index: &str) -> bool {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
(schema_id == leaf_a() || schema_id == leaf_b()) && index == "mass"
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
}
fn target_leaves(&self, role: &str) -> Vec<String> {
match role {
"parent" | "Design" | "related" | "components" => {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
vec![leaf_a().into(), leaf_b().into()]
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 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
"narrow_ref" => vec![leaf_a().into()],
"peer" => vec![citer().into()],
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
_ => Vec::new(),
}
}
}
fn rr(schema_id: &str, record_key: &str) -> RecordRef {
RecordRef::new(schema_id, record_key)
}
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
// S2-S23's original 7-row set: the shared fixture's first 7 rows (a-lo..citer-1), unchanged in
// value or order from before the shared fixture - every test below that enumerates base_rows()'s
// exact key order or count (S3, S4/S5, matched_is_the_total_count_not_the_page_page, ...) still
// holds. The parity-vector tests use all_fixture_rows() instead, which adds the fix-batch rows
// (float tie, astral/BMP keys) the shared fixture also carries.
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
fn base_rows() -> Vec<FixtureRow> {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
fixture().rows.iter().take(7).map(build_row).collect()
}
fn all_fixture_rows() -> Vec<FixtureRow> {
fixture().rows.iter().map(build_row).collect()
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
}
// S2: fields conjoin over declared indexes.
#[test]
fn conjoins_any_of_and_comparison_predicates() {
let rows = base_rows();
let selection = Selection {
fields: Some(vec![
FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["weapon".into()]),
number: None,
},
FieldPredicate {
index: "mass".into(),
op: "ge".into(),
values: None,
number: Some("5".into()),
},
]),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).expect("selection is valid");
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
let ids: Vec<&str> = evaluation.rows.iter().map(Row::record_key).collect();
assert_eq!(ids, vec!["a-eq"]);
}
// S3: order is (ordinal, schemaId, recordKey) ascending, reversed under descending.
#[test]
fn orders_by_ordinal_then_identity_and_reverses() {
let rows = base_rows();
let ascending = select(&FixtureRowSet, &rows, &Selection::default(), 1, test_cursor_key()).unwrap();
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
assert_eq!(
ascending.rows.iter().map(Row::record_key).collect::<Vec<_>>(),
vec!["a-lo", "a-eq", "b-hi", "shared-1", "shared-2", "shared-3", "citer-1"]
);
let descending = select(
&FixtureRowSet,
&rows,
&Selection {
descending: true,
..Selection::default()
},
1,
test_cursor_key(),
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
)
.unwrap();
assert_eq!(
descending.rows.iter().map(Row::record_key).collect::<Vec<_>>(),
vec!["citer-1", "shared-3", "shared-2", "shared-1", "b-hi", "a-eq", "a-lo"]
);
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AJ (Soul's SM-4): `base_rows()`'s one tied ordinal (a-lo/a-eq/b-hi... no - actually every
// base row has a distinct ordinal) never exercises the schemaId component of `order_rows`'s
// `(ordinal, schemaId, recordKey)` tiebreak at all. This test builds two rows, one on each
// schema, sharing one ordinal, with keys chosen *relative to the schema ids discovered at
// runtime* - the low-sorting schema gets the high-sorting key ("z") and the high-sorting schema
// gets the low-sorting key ("a") - so a schemaId-first order and a key-first order are the exact
// reverse of each other no matter which real schema id happens to be smaller (a fixture that
// merely ties one ordinal across two same-key-ordered rows can pass under either rule by
// coincidence - see the C# mirror's fix, CultNetSelectionSurvivorTests.cs, for the probe that
// found this).
#[test]
fn row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal() {
let (low_schema, high_schema) = if leaf_a() < leaf_b() { (leaf_a(), leaf_b()) } else { (leaf_b(), leaf_a()) };
let low_schema_high_key = FixtureRow::leaf(low_schema, "z", 1, "k", "1");
let high_schema_low_key = FixtureRow::leaf(high_schema, "a", 1, "k", "1");
let rows = vec![low_schema_high_key, high_schema_low_key];
let evaluation = select(&FixtureRowSet, &rows, &Selection::default(), 1, test_cursor_key()).unwrap();
// schemaId-first (the rule): low_schema < high_schema regardless of key, so the "z"-keyed row
// (on low_schema) comes first. record_key-first (the mutant) would instead put the "a"-keyed
// row first - the exact reverse.
let order: Vec<(&str, &str)> = evaluation.rows.iter().map(|r| (r.schema_id(), r.record_key())).collect();
assert_eq!(order, vec![(low_schema, "z"), (high_schema, "a")]);
}
// R-AT (Soul's SM-7 table, row comparator): `order_rows`'s tiebreak is
// (ordinal, schema_id, record_key) - the test above pins schema_id ahead of record_key, but every
// row in it has a distinct record_key too, so record_key never has to be the sole discriminator.
// This test ties both ordinal and schema_id across two rows on the same schema and varies only
// record_key, inserted out of order, so dropping record_key from the comparator entirely (SM-7's
// named Rust row-comparator survivor - killed in C# a pass ago, never pinned in Rust) leaves them
// in insertion order instead of "a" before "z".
#[test]
fn row_tiebreak_orders_by_record_key_when_ordinal_and_schema_id_are_both_tied() {
let high_key = FixtureRow::leaf(leaf_a(), "z", 1, "k", "1");
let low_key = FixtureRow::leaf(leaf_a(), "a", 1, "k", "1");
// Inserted high-key-first, the reverse of the required output.
let rows = vec![high_key, low_key];
let evaluation = select(&FixtureRowSet, &rows, &Selection::default(), 1, test_cursor_key()).unwrap();
let order: Vec<&str> = evaluation.rows.iter().map(Row::record_key).collect();
assert_eq!(order, vec!["a", "z"]);
}
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
// S4/S5: a page walk visits every row exactly once, the last page carries no cursor, and a
// cursor is refused when it does not decode, its digest does not match, or asOf has moved.
#[test]
fn pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor() {
let rows = base_rows();
let mut selection = Selection {
limit: Some(2),
..Selection::default()
};
let mut seen = Vec::new();
for _ in 0..10 {
let page = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
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
seen.extend(page.rows.iter().map(|r| r.record_key.to_string()));
match page.next_cursor {
Some(cursor) => selection.cursor = Some(cursor),
None => break,
}
}
assert_eq!(
seen,
vec!["a-lo", "a-eq", "b-hi", "shared-1", "shared-2", "shared-3", "citer-1"]
);
let first = select(&FixtureRowSet, &rows, &Selection { limit: Some(1), ..Selection::default() }, 1, test_cursor_key()).unwrap();
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
let cursor = first.next_cursor.expect("more than one row");
let stale = select(
&FixtureRowSet,
&rows,
&Selection { limit: Some(1), cursor: Some(cursor.clone()), ..Selection::default() },
2,
test_cursor_key(),
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
);
assert!(matches!(stale, Err(SelectionRefusal::CursorStale { .. })));
let mismatched = select(
&FixtureRowSet,
&rows,
&Selection { limit: Some(1), cursor: Some(cursor), descending: true, ..Selection::default() },
1,
test_cursor_key(),
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
);
assert!(matches!(mismatched, Err(SelectionRefusal::CursorInvalid { .. })));
let garbage = select(
&FixtureRowSet,
&rows,
&Selection { cursor: Some("not-base64!!".into()), ..Selection::default() },
1,
test_cursor_key(),
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
);
assert!(matches!(garbage, Err(SelectionRefusal::CursorInvalid { .. })));
}
// S6: the hop follows one declared reference by role.
#[test]
fn hops_one_edge_by_declared_role() {
let rows = base_rows();
let selection = Selection {
cites: Some(Citation {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
target: rr(leaf_a(), "a-eq"),
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
role: Some("Design".into()),
}),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
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
assert_eq!(evaluation.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["citer-1"]);
assert_eq!(evaluation.edges.len(), 1);
assert_eq!(evaluation.edges[0].role, "Design");
}
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
// S6, isolated: a citer with two references at the *same* target under different roles - the
// role filter must exclude the one the caller did not ask for. base_rows()'s citer-1 only ever
// carries one reference, so a mutant that drops the role filter entirely is invisible there (it
// has nothing else to wrongly match); this fixture gives it something to wrongly match.
#[test]
fn cites_role_excludes_a_second_reference_at_the_same_target() {
let rows = vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
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
FixtureRow::citer(
"double-citer",
2,
vec![
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
("Design".to_string(), "a-eq".to_string(), None),
("OtherRole".to_string(), "a-eq".to_string(), None),
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
],
),
];
let selection = Selection {
cites: Some(Citation {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
target: rr(leaf_a(), "a-eq"),
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
role: Some("Design".into()),
}),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
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
assert_eq!(evaluation.edges.len(), 1, "OtherRole's reference must not also match");
assert_eq!(evaluation.edges[0].role, "Design");
}
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
// S7: cited { exists } is the one negation, and the two directions answer opposite sets.
#[test]
fn cited_exists_is_the_one_negation() {
let rows = base_rows();
let cited = select(
&FixtureRowSet,
&rows,
&Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schemas: Some(vec![leaf_a().into()]),
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: Some(Incoming { role: "Design".into(), exists: true }),
..Selection::default()
},
1,
test_cursor_key(),
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
)
.unwrap();
assert_eq!(cited.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["a-eq"]);
let uncited = select(
&FixtureRowSet,
&rows,
&Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schemas: Some(vec![leaf_a().into()]),
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: Some(Incoming { role: "Design".into(), exists: false }),
..Selection::default()
},
1,
test_cursor_key(),
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
)
.unwrap();
let mut ids: Vec<&str> = uncited.rows.iter().map(Row::record_key).collect();
ids.sort_unstable();
assert_eq!(ids, vec!["a-lo", "shared-1", "shared-3"]);
}
// R-V (S-1): a row's identity is `(schemaId, recordKey)`, never the key alone. CultCache keys are
// unique per schema, so two schemas can share one record key. Before the fix, `byKey` kept only
// the last row seen at a key, so which schema's row a `cites` edge resolved to - and whether a
// `cited` selection counted a row as cited at all - depended on row order.
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
//
// R-AF: uses `narrow_ref` (declared target leaf_a only), not `Design` (both leaves) - a stored
// edge carries no schema on the wire (`Row::references`'s doc comment), so a *broad*-target role
// has nothing of its own to break a shared-key tie with and genuinely does depend on row order
// (Soul's map, SM-1/R-AF: "both row orders give the same answer" is R-V's narrower promise, true
// only when the declared target excludes all-but-one of the duplicate-key candidates). A
// narrow-target role still resolves the one candidate inside its target regardless of order,
// because the other schema's row at the same key is never a valid candidate to begin with - that
// is the guarantee this test pins.
#[test]
fn row_identity_is_schema_and_key_not_key_alone_in_either_row_order() {
let leaf_a_row = FixtureRow::leaf(leaf_a(), "shared-key", 1, "weapon", "1");
let leaf_b_row = FixtureRow::leaf(leaf_b(), "shared-key", 2, "shield", "2");
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
let citer_row = FixtureRow {
schema_id: NARROW_CITER,
record_key: "citer-1",
ordinal: 3,
kind: None,
mass: None,
references: vec![("narrow_ref".to_string(), "shared-key".to_string(), None)],
};
let cites_selection = Selection {
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
cites: Some(Citation { target: rr(leaf_a(), "shared-key"), role: Some("narrow_ref".into()) }),
..Selection::default()
};
let cited_selection = Selection {
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
cited: Some(Incoming { role: "narrow_ref".into(), exists: true }),
..Selection::default()
};
for rows in [
vec![leaf_a_row.clone(), leaf_b_row.clone(), citer_row.clone()],
vec![leaf_b_row.clone(), leaf_a_row.clone(), citer_row.clone()],
] {
let cites_eval = select(&FixtureRowSet, &rows, &cites_selection, 1, test_cursor_key()).unwrap();
assert_eq!(
cites_eval.rows.iter().map(Row::record_key).collect::<Vec<_>>(),
vec!["citer-1"],
"row order must not change whether the citation resolves"
);
assert_eq!(cites_eval.edges.len(), 1);
assert_eq!(
cites_eval.edges[0].to.schema_id(),
leaf_a(),
"the edge must resolve to leaf_a's row, never leaf_b's, regardless of row order"
);
let cited_eval = select(&FixtureRowSet, &rows, &cited_selection, 1, test_cursor_key()).unwrap();
let cited_pairs: Vec<(&str, &str)> =
cited_eval.rows.iter().map(|r| (r.schema_id(), r.record_key())).collect();
assert_eq!(
cited_pairs,
vec![(leaf_a(), "shared-key")],
"cited membership is (schemaId, recordKey) - leaf_b's row at the same key must not \
count as cited just because leaf_a's row at that key is"
);
}
}
// R-W (S-3), Rust half: a selection carrying both `cites` and `cited` anchors each edge by its
// own hop, so it returns both sets of edges rather than one flag deciding the direction for the
// whole batch. Shape from Soul's probe (soul-sel-final-notes.md): a page row "ca" that cites a
// leaf via "Design" and is itself cited by another citer "cb" via "peer" - the answer must carry
// both `cb --peer--> ca` (anchored on the citee) and `ca --Design--> leaf` (anchored on the
// citer).
#[test]
fn a_selection_with_both_hops_returns_both_directions_edges() {
let leaf = FixtureRow::leaf(leaf_a(), "leaf-row", 1, "weapon", "5");
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
let ca = FixtureRow::citer("ca", 2, vec![("Design".to_string(), "leaf-row".to_string(), None)]);
let cb = FixtureRow::citer("cb", 3, vec![("peer".to_string(), "ca".to_string(), None)]);
let rows = vec![leaf, ca, cb];
let selection = Selection {
cites: Some(Citation { target: rr(leaf_a(), "leaf-row"), role: Some("Design".into()) }),
cited: Some(Incoming { role: "peer".into(), exists: true }),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
assert_eq!(evaluation.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["ca"]);
assert_eq!(
evaluation.edges.len(),
2,
"both the cites edge (ca->leaf) and the cited edge (cb->ca) must survive - not just one"
);
let mut seen: Vec<(&str, &str, &str)> = evaluation
.edges
.iter()
.map(|edge| (edge.from.record_key(), edge.role.as_str(), edge.to.record_key()))
.collect();
seen.sort_unstable();
assert_eq!(seen, vec![("ca", "Design", "leaf-row"), ("cb", "peer", "ca")]);
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AJ (Soul's SM-4): the test above (and its C# mirror) never actually checks the evaluator's
// edge *order* - it sorts the observed edges before comparing, so it pins which edges appear, not
// where. And even a version that did compare order would be blind here: "ca" < "cb" and
// "Design" < "peer" agree, so a From-key-first rule and a Role-first rule produce the same
// sequence by coincidence. This test picks a hub citer key and role name that *disagree*: the hub
// ("z_hub") sorts after the other citer ("a_other") by key, but the hub's own cites-edge role
// ("Design") sorts before the other's cited-edge role ("peer") - so a From-key-first order and a
// Role-first order are provably the reverse of each other, and only one of them is the rule (R-B:
// "page-row order, then (from, role, to)").
#[test]
fn edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor() {
let leaf = FixtureRow::leaf(leaf_a(), "target", 1, "weapon", "5");
let hub = FixtureRow::citer("z_hub", 2, vec![("Design".to_string(), "target".to_string(), None)]);
let other = FixtureRow::citer("a_other", 3, vec![("peer".to_string(), "z_hub".to_string(), None)]);
let rows = vec![leaf, hub, other];
let selection = Selection {
cites: Some(Citation { target: rr(leaf_a(), "target"), role: Some("Design".into()) }),
cited: Some(Incoming { role: "peer".into(), exists: true }),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
assert_eq!(evaluation.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["z_hub"]);
assert_eq!(evaluation.edges.len(), 2);
// From-key-first (the rule): "a_other" < "z_hub", so the cited edge (From=a_other) comes
// first. Role-first (the mutant) would instead put the cites edge (Role="Design") first,
// since "Design" < "peer" - the exact reverse.
let order: Vec<(&str, &str)> =
evaluation.edges.iter().map(|edge| (edge.from.record_key(), edge.role.as_str())).collect();
assert_eq!(order, vec![("a_other", "peer"), ("z_hub", "Design")]);
}
R-AN/R-AO: Rust edge-order and shared-key regression tests R-AN: the existing edge-order test ties From and To across its two edges, so it only pins from.record_key as sole discriminator - dropping role entirely still survives (Soul's SM-4). Two new tests isolate the other two components of EdgesFor's comparator: role (same citer, same target, two roles - Citation.role: None matches any reference) and from.schema_id (two different-schema citers sharing one record key and role). Both confirmed to kill their own dropped-clause mutation by hand (reverted after). R-AO: shared_key_with_two_in_target_candidates_resolves_to_whichever_the_- caller_listed_first pins "first row in the caller's order wins" - R-AF's existing vector only covered one-in/one-out. Confirmed to kill a last-wins mutation by hand (reverted after). Also extends the shared vector case list with shared_key_design_resolves_the_first_in_target_candidate_- in_row_order (paired with the fixture/vector commit). To.SchemaId/To.Key were re-checked per Self's ruling: within any one page_position bucket, `cites` always ties every edge's To to the query's own single target, and `cited` always ties it to the anchor row's own identity - the only way for To to vary at a tied page_position is a mixed cites+cited hop on one row, which always ties or untangles via From first. No construction was found where To is the sole discriminator; the earlier "equivalent within one hop direction" claim holds. No code change, no artificial test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:17:59 +02:00
// R-AN: the test above pins from.record_key before role, but ties From and To across the two
// edges - dropping role from the comparator entirely still leaves from.record_key as the sole,
// sufficient discriminator, so that mutation survives (Soul's SM-4). This test ties From (one
// citer) and To (one target) identically across both edges - only role can differ - by giving the
// same citer two references through different roles at the same target key. Citation.role: None
// matches any declared reference (matches_citation), so both edges surface from one `cites` query.
// Declared out of code-point order ("parent" before "Design") on purpose, so a stable sort with
// role dropped from the comparator would leave them in that wrong order.
#[test]
fn edge_order_pins_role_when_from_and_to_are_both_tied() {
let leaf = FixtureRow::leaf(leaf_a(), "target", 1, "weapon", "5");
let hub = FixtureRow::citer(
"hub",
2,
vec![
("parent".to_string(), "target".to_string(), None),
("Design".to_string(), "target".to_string(), None),
],
);
let rows = vec![leaf, hub];
let selection =
Selection { cites: Some(Citation { target: rr(leaf_a(), "target"), role: None }), ..Selection::default() };
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
assert_eq!(evaluation.edges.len(), 2);
let order: Vec<&str> = evaluation.edges.iter().map(|edge| edge.role.as_str()).collect();
// "Design" < "parent" in code-point order (uppercase 'D' 0x44 sorts before lowercase 'p' 0x70).
assert_eq!(order, vec!["Design", "parent"]);
}
// R-AN: ties From.Key and Role across the two edges (both citers share one record key and cite
// the same target through the same role, "peer" - already declared by the `citer()` schema and
// resolvable regardless of which schema's row carries it, since target_leaves takes no schema -
// Soul's separate PLAUSIBLE finding) - only From.SchemaId can differ. Two different-schema citer
// rows (`citer()` and `citer_narrow()`), same record key, inserted in the opposite of the correct
// order so a stable sort with from.schema_id dropped from the comparator would leave them wrong.
#[test]
fn edge_order_pins_from_schema_id_when_from_key_and_role_are_both_tied() {
let target = FixtureRow::citer("hub-target", 1, Vec::new());
let from_wide = FixtureRow::citer("same-key", 2, vec![("peer".to_string(), "hub-target".to_string(), None)]);
let from_narrow =
FixtureRow::citer_narrow("same-key", 3, vec![("peer".to_string(), "hub-target".to_string(), None)]);
// Insertion order is whichever schema id sorts *second* first, then the one that sorts first -
// deliberately the reverse of the comparator's required output.
let (first_inserted, second_inserted) = if citer() < citer_narrow() {
(from_narrow, from_wide)
} else {
(from_wide, from_narrow)
};
let expected_first_schema = if citer() < citer_narrow() { citer() } else { citer_narrow() };
let rows = vec![target, first_inserted, second_inserted];
let selection =
Selection { cited: Some(Incoming { role: "peer".into(), exists: true }), ..Selection::default() };
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
assert_eq!(evaluation.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["hub-target"]);
assert_eq!(evaluation.edges.len(), 2);
assert_eq!(evaluation.edges[0].from.record_key(), "same-key");
assert_eq!(evaluation.edges[1].from.record_key(), "same-key");
assert_eq!(evaluation.edges[0].role, "peer");
assert_eq!(evaluation.edges[1].role, "peer");
assert_eq!(evaluation.edges[0].from.schema_id(), expected_first_schema);
}
// R-AT (Soul's SM-7, merge gate third pass): the two tests above tie From+To or From+Role but
// never both edges' `to` while `from` and `role` are also tied - so dropping `to.schema_id` and
// `to.record_key` from the edge comparator entirely (or reversing `to.record_key`) still sorted
// correctly by luck and survived under mutation. Hands' argument that no construction produces
// this was false: a selection carrying BOTH hops (`cites` and `cited`) mixes their edges into one
// `Vec`, and a row that self-references through the same role it also uses to cite a peer yields
// a `cites` edge (`to` = the queried peer) and a `cited` edge (`to` = the anchor row itself) tied
// on `from` and `role`, differing only on `to`. Soul built this counterexample and it is taken
// from the rig, not rebuilt - but the rig's own "hub"/"other" naming leaves `to.record_key`
// dropped-entirely still SURVIVING: `select`'s `cited` phase always pushes the self-edge before
// the `cites` phase pushes the peer edge, and "hub" < "other" happens to match that push order, so
// a stable sort with the tiebreak gone reproduces the right answer by coincidence. Naming the
// self-citing row so its key sorts AFTER the cited target's key ("zebra" self-cites and cites
// "alpha") makes the push order and the correct sorted order disagree, so only the comparator's
// own `to` component - not accidental insertion order - can produce the right answer.
#[test]
fn edge_order_pins_to_when_from_and_role_are_both_tied_by_a_self_citing_peer() {
let zebra = FixtureRow::citer(
"zebra",
1,
vec![
("peer".to_string(), "zebra".to_string(), None),
("peer".to_string(), "alpha".to_string(), None),
],
);
let alpha = FixtureRow::citer("alpha", 2, Vec::new());
let rows = vec![zebra, alpha];
let selection = Selection {
cites: Some(Citation { target: rr(citer(), "alpha"), role: Some("peer".into()) }),
cited: Some(Incoming { role: "peer".into(), exists: true }),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
assert_eq!(evaluation.edges.len(), 2, "expected two edges in one page-position bucket");
assert_eq!(evaluation.edges[0].from.record_key(), evaluation.edges[1].from.record_key());
assert_eq!(evaluation.edges[0].role, evaluation.edges[1].role);
assert_ne!(
evaluation.edges[0].to.record_key(),
evaluation.edges[1].to.record_key(),
"To must differ - if it does not, Hands' equivalence claim survives this construction"
);
// The comparator's to component is what decides the order: "alpha" < "zebra" in code-point
// order, opposite of push order (the self edge is always pushed before the cites edge), so
// dropping or reversing to.record_key flips this rather than merely agreeing by luck.
assert_eq!(evaluation.edges[0].to.record_key(), "alpha");
assert_eq!(evaluation.edges[1].to.record_key(), "zebra");
}
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
// S16: the four comparisons at the boundary, including the row whose value equals the compared
// number exactly.
#[test]
fn compares_numbers_at_the_boundary_for_all_four_operators() {
let rows = vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
FixtureRow::leaf(leaf_a(), "lo", 1, "k", "4"),
FixtureRow::leaf(leaf_a(), "eq", 2, "k", "5"),
FixtureRow::leaf(leaf_a(), "hi", 3, "k", "6"),
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
];
let matches_for = |op: &str| {
let selection = Selection {
fields: Some(vec![FieldPredicate {
index: "mass".into(),
op: op.into(),
values: None,
number: Some("5".into()),
}]),
..Selection::default()
};
let mut ids: Vec<String> = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key())
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
.unwrap()
.rows
.iter()
.map(|r| r.record_key.to_string())
.collect();
ids.sort();
ids
};
assert_eq!(matches_for("lt"), vec!["lo"]);
assert_eq!(matches_for("le"), vec!["eq", "lo"]);
assert_eq!(matches_for("ge"), vec!["eq", "hi"]);
assert_eq!(matches_for("gt"), vec!["hi"]);
}
// S18: an edge naming a row outside the reference's declared target refuses the selection.
#[test]
fn refuses_an_edge_outside_its_declared_target() {
let rows = vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
FixtureRow::leaf(leaf_b(), "b", 1, "k", "1"),
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
FixtureRow {
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
schema_id: NARROW_CITER,
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
record_key: "bad",
ordinal: 2,
kind: None,
mass: None,
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
references: vec![("narrow_ref".to_string(), "b".to_string(), None)],
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
},
];
let selection = Selection {
cited: Some(Incoming { role: "narrow_ref".into(), exists: true }),
..Selection::default()
};
let result = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key());
assert!(matches!(result, Err(SelectionRefusal::ReferenceOutsideTarget { .. })));
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF, Soul's SM-1 shape table (docs/cultnet-selection-cut.md): a record key shared by two rows
// of different schemas, one inside the declared target and one outside it. The in-target row must
// resolve regardless of which schema's row happens to enumerate first - the edge itself carries no
// schema to break the tie (`Row::references`'s doc comment), so only the declared leaf set
// decides. This is the shape neither runtime's parity vectors covered before this cut (zero
// duplicate record keys across the original fixture's ten rows).
#[test]
fn shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order() {
let leaf_a_row = FixtureRow::leaf(leaf_a(), "dup-key", 1, "weapon", "1");
let leaf_b_row = FixtureRow::leaf(leaf_b(), "dup-key", 2, "shield", "2");
let citer_row = FixtureRow {
schema_id: NARROW_CITER,
record_key: "narrow-citer",
ordinal: 3,
kind: None,
mass: None,
references: vec![("narrow_ref".to_string(), "dup-key".to_string(), None)],
};
let selection = Selection {
cited: Some(Incoming { role: "narrow_ref".into(), exists: true }),
..Selection::default()
};
for rows in [
vec![leaf_a_row.clone(), leaf_b_row.clone(), citer_row.clone()],
vec![leaf_b_row.clone(), leaf_a_row.clone(), citer_row.clone()],
] {
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
assert_eq!(evaluation.edges.len(), 1);
assert_eq!(
evaluation.edges[0].to.schema_id(),
leaf_a(),
"must resolve to leaf_a, the only in-target row, never leaf_b, in either row order"
);
assert_eq!(evaluation.edges[0].to.record_key(), "dup-key");
}
}
R-AN/R-AO: Rust edge-order and shared-key regression tests R-AN: the existing edge-order test ties From and To across its two edges, so it only pins from.record_key as sole discriminator - dropping role entirely still survives (Soul's SM-4). Two new tests isolate the other two components of EdgesFor's comparator: role (same citer, same target, two roles - Citation.role: None matches any reference) and from.schema_id (two different-schema citers sharing one record key and role). Both confirmed to kill their own dropped-clause mutation by hand (reverted after). R-AO: shared_key_with_two_in_target_candidates_resolves_to_whichever_the_- caller_listed_first pins "first row in the caller's order wins" - R-AF's existing vector only covered one-in/one-out. Confirmed to kill a last-wins mutation by hand (reverted after). Also extends the shared vector case list with shared_key_design_resolves_the_first_in_target_candidate_- in_row_order (paired with the fixture/vector commit). To.SchemaId/To.Key were re-checked per Self's ruling: within any one page_position bucket, `cites` always ties every edge's To to the query's own single target, and `cited` always ties it to the anchor row's own identity - the only way for To to vary at a tied page_position is a mixed cites+cited hop on one row, which always ties or untangles via From first. No construction was found where To is the sole discriminator; the earlier "equivalent within one hop direction" claim holds. No code change, no artificial test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:17:59 +02:00
// R-AO: the test above covers one in-target candidate and one out - resolve_reference_target's
// `for &candidate in candidates { ... return Ok(Some(candidate)) }` picks the first match, but
// nothing pinned that rule specifically when *two* in-target rows share a key (Soul's SM-4: "the
// first row in the caller's order wins" was a coincidence of source, not a rule). "Design"'s
// declared target is [leaf_a, leaf_b] (FixtureRowSet::target_leaves) - both schemas are in-target,
// so which one resolves depends only on which the caller listed first in `rows`/`by_key`'s
// insertion order. Both orders are exercised, each expecting whichever row was listed first in
// *that* run - proving the rule is "first in the caller's order", not merely "leaf_a happens to
// win" or some other fixed tiebreak.
#[test]
fn shared_key_with_two_in_target_candidates_resolves_to_whichever_the_caller_listed_first() {
let leaf_a_row = FixtureRow::leaf(leaf_a(), "dup-key-both-in", 1, "weapon", "1");
let leaf_b_row = FixtureRow::leaf(leaf_b(), "dup-key-both-in", 2, "shield", "2");
let citer_row = FixtureRow::citer(
"both-in-citer",
3,
vec![("Design".to_string(), "dup-key-both-in".to_string(), None)],
);
let selection =
Selection { cited: Some(Incoming { role: "Design".into(), exists: true }), ..Selection::default() };
for (rows, expected_first_schema) in [
(vec![leaf_a_row.clone(), leaf_b_row.clone(), citer_row.clone()], leaf_a()),
(vec![leaf_b_row.clone(), leaf_a_row.clone(), citer_row.clone()], leaf_b()),
] {
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
assert_eq!(evaluation.edges.len(), 1);
assert_eq!(
evaluation.edges[0].to.schema_id(),
expected_first_schema,
"must resolve to whichever in-target row the caller listed first, not a fixed schema"
);
assert_eq!(evaluation.edges[0].to.record_key(), "dup-key-both-in");
}
}
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF, the mirror shape: the only row at the key is outside the declared target - the edge must
// refuse the selection (S18), not silently produce an empty page. Opposite of
// `refuses_an_edge_outside_its_declared_target` above only in that this key is one a same-role,
// in-target row could also have shared (see the test above) - it just doesn't here.
#[test]
fn shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target() {
let leaf_b_row = FixtureRow::leaf(leaf_b(), "narrow-only", 1, "shield", "2");
let citer_row = FixtureRow {
schema_id: NARROW_CITER,
record_key: "narrow-citer",
ordinal: 2,
kind: None,
mass: None,
references: vec![("narrow_ref".to_string(), "narrow-only".to_string(), None)],
};
let rows = vec![leaf_b_row, citer_row];
let selection = Selection {
cited: Some(Incoming { role: "narrow_ref".into(), exists: true }),
..Selection::default()
};
let result = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key());
assert!(matches!(result, Err(SelectionRefusal::ReferenceOutsideTarget { .. })));
}
// R-T/F15: `cites` checks every edge of the *queried role* a citer carries against its declared
// target, not only the one edge whose key matches the citation - a many-reference member can carry
// several edges under the one role/member name, and a corrupt one among them must not hide behind
// the key filter. Two edges under "Design" here (this crate's `FixtureRow` has no distinct
// many-reference shape - two tuples sharing one role name is the same thing D11 gives one edge per
// element of), one in-target and one out, both under the role the citation asks about - mirrors
// C#'s own R-T(F15) test exactly (Evaluator_CitesRefusesAnOutOfTargetEdgeEvenWhenTheQueriedKey-
// MatchesNeitherEdge, CultNetSelectionFixBatchTests.cs), which queries the *same* role/member that
// carries both elements.
//
// R-AF (found landing it, matches_citation's own comment): an *unrelated* role's out-of-target edge
// no longer poisons a citation for a *different*, explicitly-requested role - that was this test's
// original shape, and it tested a stronger guarantee than the C# reference actually gives
// (ReferenceMembers(descriptor, citation.Role) filters by role before R-W's per-edge check ever
// runs, CultNetSelectionEvaluator.cs:451-458).
#[test]
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
fn cites_refuses_an_out_of_target_edge_under_the_same_queried_role() {
let rows = vec![
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// Design's declared target is [leaf_a, leaf_b] (FixtureRowSet::target_leaves) - a citer
// schema row is outside it, so a "Design" edge pointed here is the out-of-target one.
FixtureRow::citer("other-citer", 2, Vec::new()),
FixtureRow::citer(
"bad-citer",
3,
vec![
("Design".to_string(), "a-eq".to_string(), None),
("Design".to_string(), "other-citer".to_string(), None),
],
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
),
];
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// Queries a key neither Design edge carries - the door for the pre-fix bug this pins: skipping
// straight to "does any edge's key match" would find nothing and answer an empty page, never
// looking at the out-of-target second edge at all.
let selection = Selection {
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
cites: Some(Citation { target: rr(leaf_a(), "does-not-exist"), role: Some("Design".into()) }),
..Selection::default()
};
let result = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key());
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
assert!(matches!(result, Err(SelectionRefusal::ReferenceOutsideTarget { .. })));
}
// S19-equivalent (D11): a dictionary reference's two entries keep distinct payload bytes.
// Row::references() is where Rust exposes this - there is no separate cache layer to test it
// against, unlike C#'s ReferencesOf/Cache_EnumeratesADictionaryReferenceAsEdgesCarryingItsValues.
#[test]
fn dictionary_reference_entries_keep_distinct_payload_bytes() {
let citer = FixtureRow::citer(
"assembly",
3,
vec![
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
("components".to_string(), "part-1".to_string(), Some(vec![1, 5])),
("components".to_string(), "part-2".to_string(), Some(vec![2, 5])),
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
],
);
let payloads: std::collections::HashSet<Vec<u8>> =
citer.references().into_iter().filter_map(|(_, _, payload)| payload).collect();
assert_eq!(payloads.len(), 2, "the two dictionary entries must keep distinct payloads");
}
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
// R-B: hop edges follow the hop's direction. A `cites` selection's page holds the citer, so its
// edges (From = citer) survive the page filter, mirroring
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
// `EvaluatorHopsOneEdgeByDeclaredReferenceAndRole`. A `cited` selection's page holds the *citee*
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
// instead, so - as of R-B - its edges survive when their *To* is on the page, not their *From*:
// the citer ("assembly") need not itself be on the page at all. (Before R-B, Rust filtered both
// hop kinds by `edge.From` on the page, so a `cited` selection's edges were silently empty
// whenever the citer itself did not separately match - Soul's F2 finding.)
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
#[test]
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
fn cited_selections_page_the_citee_and_their_edges_survive_the_page_filter() {
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
let rows = vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
FixtureRow::leaf(leaf_a(), "part-1", 1, "k", "1"),
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
FixtureRow::citer(
"assembly",
2,
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
vec![("components".to_string(), "part-1".to_string(), Some(vec![9]))],
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
),
];
let selection = Selection {
cited: Some(Incoming { role: "components".into(), exists: true }),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
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
assert_eq!(evaluation.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["part-1"]);
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert_eq!(evaluation.edges.len(), 1, "the citee (part-1) is on the page, so its incoming edge survives");
assert_eq!(evaluation.edges[0].from.record_key, "assembly");
assert_eq!(evaluation.edges[0].to.record_key, "part-1");
assert_eq!(evaluation.edges[0].payload, Some(vec![9]));
}
// R-B: the converse of the test above - a `cites` selection's edges still key off the *citer*
// being on the page (unchanged by R-B), so an edge whose citee is on the page but whose citer is
// not filtered out does not leak in under `cites`.
#[test]
fn cites_selections_still_page_the_citer_and_key_edges_off_it() {
let rows = vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), "a-eq".to_string(), None)]),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
];
let selection = Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert_eq!(evaluation.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["citer-1"]);
assert_eq!(evaluation.edges.len(), 1);
assert_eq!(evaluation.edges[0].from.record_key, "citer-1");
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
}
// S23: the evaluator matches every row sharing an index value - not a cache's last-writer-wins
// unique-index lookup (Rust's toy Row set never had that bug, but the rule is pinned here too:
// nothing in select() short-circuits after the first match).
#[test]
fn matches_every_row_sharing_an_index_value() {
let rows = base_rows();
let selection = Selection {
fields: Some(vec![FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["shared".into()]),
number: None,
}]),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
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
let mut ids: Vec<&str> = evaluation.rows.iter().map(Row::record_key).collect();
ids.sort_unstable();
assert_eq!(ids, vec!["shared-1", "shared-2", "shared-3"]);
}
// S1: the door refuses typed rather than answering an empty page.
#[test]
fn validation_refuses_undeclared_index_role_and_empty_lists() {
let empty_schemas = Selection {
schemas: Some(Vec::new()),
..Selection::default()
};
assert_eq!(validate(&empty_schemas, &FixtureRowSet).unwrap_err().field, "schemas");
let empty_keys = Selection {
keys: Some(Vec::new()),
..Selection::default()
};
assert_eq!(validate(&empty_keys, &FixtureRowSet).unwrap_err().field, "keys");
let undeclared_index = Selection {
fields: Some(vec![FieldPredicate {
index: "not_declared".into(),
op: "any_of".into(),
values: Some(vec!["x".into()]),
number: None,
}]),
..Selection::default()
};
assert_eq!(
validate(&undeclared_index, &FixtureRowSet).unwrap_err().field,
"fields[0].index"
);
let undeclared_role = Selection {
cited: Some(Incoming { role: "no_such_role".into(), exists: true }),
..Selection::default()
};
assert_eq!(validate(&undeclared_role, &FixtureRowSet).unwrap_err().field, "cited.role");
let non_numeric_compare = Selection {
fields: Some(vec![FieldPredicate {
index: "kind".into(),
op: "gt".into(),
values: None,
number: Some("1".into()),
}]),
..Selection::default()
};
assert_eq!(
validate(&non_numeric_compare, &FixtureRowSet).unwrap_err().field,
"fields[0].index"
);
}
// Self's ruling, 2026-09-22 (docs/cultnet-selection-cut.md, commit 2 fix batch): the door refuses
// a blank entry inside a present schemas/keys list, not only an empty list - the evaluator must
// never have to decide what "" or " " means.
#[test]
fn validation_refuses_a_blank_entry_in_schemas_or_keys() {
let blank_schema = Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schemas: Some(vec![leaf_a().into(), " ".into()]),
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
..Selection::default()
};
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
assert_eq!(validate(&blank_schema, &FixtureRowSet).unwrap_err().field, "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
let blank_key = Selection {
keys: Some(vec!["".into()]),
..Selection::default()
};
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
assert_eq!(validate(&blank_key, &FixtureRowSet).unwrap_err().field, "keys");
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
}
// Q-J: the door refuses every named non-canonical spelling of a comparison number.
#[test]
fn validation_refuses_non_canonical_number_spellings() {
for spelling in ["+1", "1.0", "01", "1e3", "-0"] {
let selection = Selection {
fields: Some(vec![FieldPredicate {
index: "mass".into(),
op: "gt".into(),
values: None,
number: Some(spelling.into()),
}]),
..Selection::default()
};
let error = validate(&selection, &FixtureRowSet)
.expect_err(&format!("{spelling:?} must be refused"));
assert_eq!(error.field, "fields[0].number", "spelling {spelling:?}");
}
}
// S13a: nothing on the wire is serde-untagged (docs/cultnet-selection-cut.md section 2).
#[test]
fn selection_is_never_untagged() {
let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
for entry in fs::read_dir(&src_dir).expect("src directory exists") {
let entry = entry.expect("readable directory entry");
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) != Some("rs") {
continue;
}
let text = fs::read_to_string(&path).unwrap_or_default();
assert!(
!text.contains("untagged"),
"{} must not contain #[serde(untagged)] (section 2's rule)",
path.display()
);
}
}
// S13b: the selection types round-trip through the named-map MessagePack encoding every runtime
// on this wire uses (rmp_serde::to_vec_named / from_slice), across every shape this cut adds.
#[test]
fn selection_round_trips_through_named_messagepack() {
let selection = Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schemas: Some(vec![leaf_a().into(), leaf_b().into()]),
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
keys: Some(vec!["k1".into()]),
fields: Some(vec![
FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["weapon".into()]),
number: None,
},
FieldPredicate {
index: "mass".into(),
op: "ge".into(),
values: None,
number: Some("5".into()),
},
]),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
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: Some(Incoming { role: "Design".into(), exists: true }),
projection: "document".into(),
descending: true,
limit: Some(50),
cursor: Some("opaque".into()),
};
let bytes = rmp_serde::to_vec_named(&selection).expect("encodes");
let decoded: Selection = rmp_serde::from_slice(&bytes).expect("decodes");
assert_eq!(decoded, selection);
}
// ------------------------------------------------------------------------------------------
// S12: parity vectors. contracts/cultnet/interop/selection-vectors.cs-written.json is written by
// the C# reference (SelectionParityVectorTests.WriteVectors, gated on CULTNET_WRITE_VECTORS=1)
// and judged here; selection-vectors.rs-written.json is the mirror this binary writes (gated the
// same way) for the C# side to judge. A within-runtime round trip pins nothing - both directions
// run against the fixture's *own* rows, decoding only the Selection bytes across the wire.
// ------------------------------------------------------------------------------------------
#[derive(serde::Serialize, serde::Deserialize)]
struct VectorFile {
vectors: Vec<Vector>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Vector {
name: String,
#[serde(rename = "selectionMessagePackBase64")]
selection_message_pack_base64: String,
#[serde(rename = "asOf")]
as_of: u64,
// Present iff this vector is a door refusal (Self's ruling, 2026-09-22): the selection must
// be refused by `validate`/`CultNetSelection.Validate` with exactly this field name, and no
// evaluation runs. Absent for an ordinary evaluated vector.
#[serde(rename = "expectedRefusalField", default, skip_serializing_if = "Option::is_none")]
expected_refusal_field: Option<String>,
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF: a small, separate signal from expected_refusal_field above - that one is the door
// (validate, never touches select); this one names an evaluation-time typed refusal select
// itself must return (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.
#[serde(rename = "expectedEvaluationRefusal", default, skip_serializing_if = "Option::is_none")]
expected_evaluation_refusal: Option<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
#[serde(rename = "expectedIds", default)]
expected_ids: Vec<String>,
#[serde(default)]
matched: u32,
#[serde(rename = "hasNext", default)]
has_next: bool,
#[serde(rename = "expectedEdges", default)]
expected_edges: Vec<VectorEdge>,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct VectorEdge {
#[serde(rename = "fromId")]
from_id: String,
role: String,
#[serde(rename = "toId")]
to_id: String,
#[serde(rename = "payloadBase64")]
payload_base64: Option<String>,
}
fn row_id(schema_id: &str, record_key: &str) -> String {
format!("{schema_id}/{record_key}")
}
fn contracts_dir() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("contracts")
.join("cultnet")
.join("interop")
}
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
// Self's ruling, 2026-09-22 (shared-fixture ruling): a real parity defect must be reported, not
// papered over by aborting the run at the first one. Returns Err(description) instead of
// panicking when `select` refuses a vector this file did not mark as a refusal vector - R-F wires
// the door inside `select`, and the C# reference's Validate has no equivalent check for an
// unresolved `cites.target.schemaId` (CultNetSelection.cs:361-391), so a vector the reference
// evaluates for real can still be one this crate's door refuses. The caller collects every such
// mismatch instead of stopping at the first one.
fn evaluate_vector(vector: &Vector) -> Result<(Vec<String>, u32, bool, Vec<VectorEdge>), 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
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&vector.selection_message_pack_base64)
.expect("vector selection bytes are valid base64");
let selection: Selection =
rmp_serde::from_slice(&bytes).expect("vector selection bytes decode as MessagePack");
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let rows = all_fixture_rows();
let evaluation = select(&FixtureRowSet, &rows, &selection, vector.as_of, test_cursor_key())
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
.map_err(|refusal| format!("select() refused: {refusal:?}"))?;
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
let ids: Vec<String> = evaluation
.rows
.iter()
.map(|row| row_id(row.schema_id(), row.record_key()))
.collect();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
// R-G/P-1: matched is the total match count, not the page count - the two differ whenever a
// vector's limit clips the page.
let matched = evaluation.matched;
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
let has_next = evaluation.next_cursor.is_some();
let edges = evaluation
.edges
.iter()
.map(|edge| VectorEdge {
from_id: row_id(edge.from.schema_id(), edge.from.record_key()),
role: edge.role.clone(),
to_id: row_id(edge.to.schema_id(), edge.to.record_key()),
payload_base64: edge
.payload
.as_ref()
.map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes)),
})
.collect();
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
Ok((ids, matched, has_next, edges))
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: a door-refusal vector decodes its (necessarily malformed, by the
// v1 rule) selection bytes and asserts `validate` refuses it at exactly the named field, rather
// than evaluating it - "the evaluator never sees []" means these selections are never run
// through `select` at all, on either side of the vector.
fn assert_refusal_vector(vector: &Vector, expected_field: &str) {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&vector.selection_message_pack_base64)
.expect("vector selection bytes are valid base64");
let selection: Selection =
rmp_serde::from_slice(&bytes).expect("vector selection bytes decode as MessagePack");
let error = validate(&selection, &FixtureRowSet)
.expect_err(&format!("vector {:?} must be refused", vector.name));
assert_eq!(error.field, expected_field, "vector {:?}: refusal field", vector.name);
}
/// Vectors written by the reference (C#) decode and evaluate identically in Rust.
#[test]
fn selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust() {
Rust R-AB: vendor the parity fixture too, so cargo-mutants' baseline actually runs The schema_discovery.rs include_str! escape was not the only one: tests/selection.rs's fixture() read ../../contracts/cultnet/interop/selection-vectors.fixture.json, the same three-levels-up path shape, and 37 of 45 tests panicked on it under cargo-mutants' isolated copy - confirmed on Yggdrasil after the schema_discovery.rs fix alone still landed "cargo test failed in an unmutated tree, so no mutants were tested". selection-vectors.fixture.json is a checked-in input (not a generated artifact), so it gets the same treatment as the 22 schemas: a byte-identical vendored copy under packages/cultnet-rs/contracts/cultnet/interop/, a drift test (fixture_contract_matches_the_canonical_source, same skip-when-absent shape as schema_discovery_contracts.rs), and the loader reads the vendored copy unconditionally. selection-vectors.cs-written.json is different in kind: it is the C# reference's own generated output, produced only by a real cross-runtime checkout running SelectionParityVectorTests with CULTNET_WRITE_VECTORS=1. There is nothing to vendor - an isolated Rust-only copy can never satisfy that test's premise regardless of what this crate does with its own files. That one test now skips (with an explanation) when the canonical contracts/ tree is entirely absent, rather than panicking and failing the whole suite over a cross-runtime artifact the isolated copy cannot produce by construction. Verified locally by hiding contracts/ entirely and rerunning the full suite (217 tests across every binary): unchanged pass count, same as with it present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:11:35 +02:00
if !contracts_dir().is_dir() {
// R-AB: cs-written.json is the C# reference's own generated output - there is nothing to
// vendor (it is not a checked-in input like the fixture; it exists only once the C# side
// has actually run its writer against a real checkout of both runtimes). An isolated
// Rust-only copy, such as cargo-mutants' baseline build, can never satisfy this test's
// premise no matter what this crate does with its own files, so it skips rather than
// failing the whole suite over a cross-runtime artifact the isolated copy cannot produce.
eprintln!(
"skipping: {:?} not present beside the package (isolated build, no C# checkout to \
have written cs-written.json against)",
contracts_dir()
);
return;
}
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
let path = contracts_dir().join("selection-vectors.cs-written.json");
let Ok(text) = fs::read_to_string(&path) else {
panic!(
"{} is missing - run the C# writer (SelectionParityVectorTests, CULTNET_WRITE_VECTORS=1) first",
path.display()
);
};
let file: VectorFile = serde_json::from_str(&text).expect("vector file is valid JSON");
assert!(!file.vectors.is_empty(), "the vector file must carry at least one vector");
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
// Collect every mismatch instead of stopping at the first one (see evaluate_vector's comment) -
// a real parity defect is reported in full, not truncated by whichever vector happened to come
// first in the file.
let mut failures: Vec<String> = Vec::new();
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 vector in &file.vectors {
if let Some(expected_field) = &vector.expected_refusal_field {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let bytes_ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_refusal_vector(vector, expected_field)
}));
if let Err(panic) = bytes_ok {
let message = panic
.downcast_ref::<String>()
.cloned()
.or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_else(|| "<non-string panic>".into());
failures.push(format!("{}: {}", vector.name, 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 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
if let Some(expected_kind) = &vector.expected_evaluation_refusal {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&vector.selection_message_pack_base64)
.expect("vector selection bytes are valid base64");
let selection: Selection =
rmp_serde::from_slice(&bytes).expect("vector selection bytes decode as MessagePack");
let rows = all_fixture_rows();
match select(&FixtureRowSet, &rows, &selection, vector.as_of, test_cursor_key()) {
Ok(_) => failures.push(format!(
"{}: expected select() to refuse ({expected_kind}) but it accepted the selection",
vector.name
)),
Err(SelectionRefusal::ReferenceOutsideTarget { .. }) if expected_kind == "reference_outside_target" => {
// expected
}
Err(other) => failures.push(format!(
"{}: expected select() to refuse with {expected_kind}, got {other:?}",
vector.name
)),
}
continue;
}
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let evaluated = match evaluate_vector(vector) {
Ok(evaluated) => evaluated,
Err(reason) => {
failures.push(format!(
"{}: expected real evaluation (expectedIds={:?}) but {reason}",
vector.name, vector.expected_ids
));
continue;
}
};
let (ids, matched, has_next, edges) = evaluated;
if ids != vector.expected_ids {
failures.push(format!("{}: row ids - expected {:?}, got {:?}", vector.name, vector.expected_ids, ids));
}
if matched != vector.matched {
failures.push(format!("{}: matched - expected {}, got {}", vector.name, vector.matched, matched));
}
if has_next != vector.has_next {
failures.push(format!("{}: hasNext - expected {}, got {}", vector.name, vector.has_next, has_next));
}
if edges.len() != vector.expected_edges.len() {
failures.push(format!(
"{}: edge count - expected {}, got {}",
vector.name,
vector.expected_edges.len(),
edges.len()
));
continue;
}
for (i, (actual, expected)) in edges.iter().zip(vector.expected_edges.iter()).enumerate() {
if actual.from_id != expected.from_id
|| actual.role != expected.role
|| actual.to_id != expected.to_id
|| actual.payload_base64 != expected.payload_base64
{
failures.push(format!("{}: edge[{i}] mismatch", vector.name));
}
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 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
assert!(
failures.is_empty(),
"{} of {} vectors disagreed with the reference:\n{}",
failures.len(),
file.vectors.len(),
failures.join("\n")
);
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
}
/// The Rust->C# mirror: builds `selection-vectors.rs-written.json` from this binary's own
/// selections and its own evaluation of the same fixture, for the C# test
/// `SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference` to judge. Committed,
/// not regenerated on every run - rerun with `CULTNET_WRITE_VECTORS=1` after a real change to the
/// evaluator or the vocabulary.
#[test]
fn write_selection_vectors_for_the_reference() {
if std::env::var("CULTNET_WRITE_VECTORS").as_deref() != Ok("1") {
return;
}
use base64::Engine;
let cases: Vec<(&str, Selection)> = vec![
(
"any_of_plus_ge_conjunction",
Selection {
fields: Some(vec![
FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["weapon".into()]),
number: None,
},
FieldPredicate {
index: "mass".into(),
op: "ge".into(),
values: None,
number: Some("5".into()),
},
]),
..Selection::default()
},
),
(
"descending_with_limit",
Selection {
descending: true,
limit: Some(3),
..Selection::default()
},
),
(
"gt_numeric_boundary",
Selection {
fields: Some(vec![FieldPredicate {
index: "mass".into(),
op: "gt".into(),
values: None,
number: Some("1".into()),
}]),
..Selection::default()
},
),
(
"incoming_exists_true",
Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schemas: Some(vec![leaf_a().into()]),
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: Some(Incoming { role: "Design".into(), exists: true }),
..Selection::default()
},
),
(
"incoming_negation",
Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schemas: Some(vec![leaf_a().into()]),
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: Some(Incoming { role: "Design".into(), exists: false }),
..Selection::default()
},
),
(
"shared_index_value_three_rows",
Selection {
fields: Some(vec![FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["shared".into()]),
number: None,
}]),
..Selection::default()
},
),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
// R-E/Soul "Settled": with real SHA-256 schema ids in the fixture, `cites.target.schemaId`
// agrees byte for byte across runtimes - the earlier name-as-id fixture masked this. This
// is the exact-id form.
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +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: 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.
// Fixed above (see matches_citation's own comment) 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 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
(
"hop_by_role_cites_exact_id",
Selection {
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
keys: Some(vec!["citer-1".into()]),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
..Selection::default()
},
),
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// R-E: the same citation, but the target is named by the alias form C#'s production rule
// resolves ("leaf_a.v9", stripped to "leaf_a" and compared against the schema's declared
// name) - as of this cut's fix, Row::schema_name/RowSet::schema_name give this crate's own
// schema_alias the same name to resolve against, so this now agrees with the reference too.
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
// R-AF: keys scoping for the same reason as hop_by_role_cites_exact_id above.
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
(
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
"hop_by_role_cites_name_alias",
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
Selection {
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
keys: Some(vec!["citer-1".into()]),
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
cites: Some(Citation { target: rr(leaf_a_name_alias(), "a-eq"), role: Some("Design".into()) }),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
..Selection::default()
},
),
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// R-E, Self's ruling 2026-09-22 ("the C# reference's alias rule is the rule"): a hash id
// with a synthetic ".v1" appended is not a wire form either runtime's production rule
// resolves - it strips the ".v1" and compares the bare hash text against the schema's
// declared *name* ("leaf_a"), never equal. This is a shared negative check now: both
// runtimes give an empty reachable set for `schemas`, hence empty expected_ids here.
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
(
"schemas_by_hash_alias",
Selection {
schemas: Some(vec![leaf_a_hash_alias().into()]),
..Selection::default()
},
),
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// R-E: `schemas` reaches a schema by its name alias - as of this cut's fix, this crate's
// schema_alias resolves it the same way CultNetSchemaAliasMatching's descriptor overload
// does (the only overload any C# call site uses): strip the trailing ".v9" and compare
// "leaf_a" to the schema's declared name. Previously this crate's alias matcher had no
// concept of a schema's name at all and gave an empty result here, disagreeing with the
// reference - that cross-runtime defect is what this cut fixes.
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
(
"schemas_by_name_alias",
Selection {
schemas: Some(vec![leaf_a_name_alias().into()]),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
..Selection::default()
},
),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
// R-D: the row's canonical rendering of 2233759.25f32 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. Mirrors SelectionParityVectorTests.Cases's float_tie_exact_ge.
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
(
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
"float_tie_exact_ge",
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
fields: Some(vec![FieldPredicate {
index: "mass".into(),
op: "ge".into(),
values: None,
number: Some("2233759.25".into()),
}]),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
..Selection::default()
},
),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
// R-C: the astral/BMP-private-use key pair sorts by code point, not UTF-16 code unit.
// Mirrors SelectionParityVectorTests.Cases's astral_key_code_point_order.
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
(
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
"astral_key_code_point_order",
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
fields: Some(vec![FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["unicode".into()]),
number: None,
}]),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
..Selection::default()
},
),
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +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.
(
"row_tiebreak_schema_id_before_record_key_at_tied_ordinal",
Selection {
fields: Some(vec![FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["tie-schema".into()]),
number: None,
}]),
..Selection::default()
},
),
// 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.
(
"shared_key_narrow_ref_resolves_leaf_within_target",
Selection {
keys: Some(vec!["narrow-citer-accepted".into()]),
cites: Some(Citation { target: rr(leaf_a(), "dup-key"), role: Some("narrow_ref".into()) }),
..Selection::default()
},
),
R-AN/R-AO: Rust edge-order and shared-key regression tests R-AN: the existing edge-order test ties From and To across its two edges, so it only pins from.record_key as sole discriminator - dropping role entirely still survives (Soul's SM-4). Two new tests isolate the other two components of EdgesFor's comparator: role (same citer, same target, two roles - Citation.role: None matches any reference) and from.schema_id (two different-schema citers sharing one record key and role). Both confirmed to kill their own dropped-clause mutation by hand (reverted after). R-AO: shared_key_with_two_in_target_candidates_resolves_to_whichever_the_- caller_listed_first pins "first row in the caller's order wins" - R-AF's existing vector only covered one-in/one-out. Confirmed to kill a last-wins mutation by hand (reverted after). Also extends the shared vector case list with shared_key_design_resolves_the_first_in_target_candidate_- in_row_order (paired with the fixture/vector commit). To.SchemaId/To.Key were re-checked per Self's ruling: within any one page_position bucket, `cites` always ties every edge's To to the query's own single target, and `cited` always ties it to the anchor row's own identity - the only way for To to vary at a tied page_position is a mixed cites+cited hop on one row, which always ties or untangles via From first. No construction was found where To is the sole discriminator; the earlier "equivalent within one hop direction" claim holds. No code change, no artificial test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:17:59 +02:00
// 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) that shares the key.
(
"shared_key_design_resolves_the_first_in_target_candidate_in_row_order",
Selection {
keys: Some(vec!["citer-both-in-target".into()]),
cites: Some(Citation { target: rr(leaf_a(), "dup-key"), role: Some("Design".into()) }),
..Selection::default()
},
),
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
];
let mut vectors = Vec::new();
for (name, selection) in cases {
let bytes = rmp_serde::to_vec_named(&selection).expect("encodes");
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let rows = all_fixture_rows();
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key())
.unwrap_or_else(|e| panic!("case {name:?} failed: {e:?}"));
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
let ids: Vec<String> = evaluation
.rows
.iter()
.map(|row| row_id(row.schema_id(), row.record_key()))
.collect();
let edges = evaluation
.edges
.iter()
.map(|edge| VectorEdge {
from_id: row_id(edge.from.schema_id(), edge.from.record_key()),
role: edge.role.clone(),
to_id: row_id(edge.to.schema_id(), edge.to.record_key()),
payload_base64: edge
.payload
.as_ref()
.map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes)),
})
.collect();
vectors.push(Vector {
name: name.to_string(),
selection_message_pack_base64: base64::engine::general_purpose::STANDARD.encode(&bytes),
as_of: 1,
expected_refusal_field: None,
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
expected_evaluation_refusal: None,
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
matched: evaluation.matched,
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
has_next: evaluation.next_cursor.is_some(),
expected_ids: ids,
expected_edges: edges,
});
}
// Self's ruling, 2026-09-22 (commit 2 fix batch): door-refusal vectors. These selections are
// never evaluated on either side - only decoded and handed to validate/Validate, which must
// refuse at exactly the named field.
let refusal_cases: Vec<(&str, Selection, &str)> = vec![
(
"refuses_empty_schemas_list",
Selection {
schemas: Some(Vec::new()),
..Selection::default()
},
"schemas",
),
(
"refuses_blank_key_entry",
Selection {
keys: Some(vec!["a-eq".into(), " ".into()]),
..Selection::default()
},
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
"keys",
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 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
// R-E: an unmatched cites.target.schemaId is refused at the door, never silently answered
// with an empty page.
(
"refuses_unmatched_cites_target_schema",
Selection {
cites: Some(Citation {
target: rr("sha256:not-a-known-schema", "a-eq"),
role: Some("Design".into()),
}),
..Selection::default()
},
"cites.target.schemaId",
),
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// R-E, Self's ruling 2026-09-22: a hash-shaped alias resolves to no known schema in either
// runtime now (see schemas_by_hash_alias above), so naming one as a cites.target is the
// same "unmatched target" refusal as any other unresolvable schema id.
(
"refuses_cites_target_by_hash_shaped_alias",
Selection {
cites: Some(Citation {
target: rr(leaf_a_hash_alias(), "a-eq"),
role: Some("Design".into()),
}),
..Selection::default()
},
"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
];
for (name, selection, expected_field) in refusal_cases {
let bytes = rmp_serde::to_vec_named(&selection).expect("encodes");
vectors.push(Vector {
name: name.to_string(),
selection_message_pack_base64: base64::engine::general_purpose::STANDARD.encode(&bytes),
as_of: 1,
expected_refusal_field: Some(expected_field.to_string()),
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
expected_evaluation_refusal: None,
matched: 0,
has_next: false,
expected_ids: Vec::new(),
expected_edges: Vec::new(),
});
}
// 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. Keys scopes the candidate set to narrow-citer-refused alone (see the comment
// on shared_key_narrow_ref_resolves_leaf_within_target above).
let evaluation_refusal_cases: Vec<(&str, Selection)> = vec![(
"shared_key_narrow_ref_refuses_when_only_candidate_is_outside_target",
Selection {
keys: Some(vec!["narrow-citer-refused".into()]),
cites: Some(Citation { target: rr(leaf_a(), "narrow-only"), role: Some("narrow_ref".into()) }),
..Selection::default()
},
)];
for (name, selection) in evaluation_refusal_cases {
let rows = all_fixture_rows();
let result = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key());
assert!(
matches!(result, Err(SelectionRefusal::ReferenceOutsideTarget { .. })),
"{name} must be refused by select() before it can be committed as an evaluation-refusal vector, got {result:?}"
);
let bytes = rmp_serde::to_vec_named(&selection).expect("encodes");
vectors.push(Vector {
name: name.to_string(),
selection_message_pack_base64: base64::engine::general_purpose::STANDARD.encode(&bytes),
as_of: 1,
expected_refusal_field: None,
expected_evaluation_refusal: Some("reference_outside_target".to_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
matched: 0,
has_next: false,
expected_ids: Vec::new(),
expected_edges: Vec::new(),
});
}
let file = VectorFile { vectors };
let json = serde_json::to_string_pretty(&file).expect("serializes");
let dir = contracts_dir();
fs::create_dir_all(&dir).expect("contracts/cultnet/interop exists or is created");
fs::write(dir.join("selection-vectors.rs-written.json"), json).expect("writes the vector file");
}
// A canary that the canonical_number module used above stays reachable from an integration test
// (it is exercised indirectly through Selection.fields[].number already, but this names the rule
// directly): a lexicographic compare would put "10" below "9".
#[test]
fn canonical_number_compares_by_magnitude() {
assert_eq!(
canonical_number::compare("10", "9"),
std::cmp::Ordering::Greater
);
assert!(canonical_number::is_canonical("10"));
assert!(!canonical_number::is_canonical("+10"));
}
// Q-J: a negative value orders below a positive one of the same magnitude - a comparator that
// ignores sign would treat "-1" and "1" as equal (same integer/fraction digits).
#[test]
fn canonical_number_compares_by_sign() {
assert_eq!(canonical_number::compare("-1", "1"), std::cmp::Ordering::Less);
assert_eq!(canonical_number::compare("1", "-1"), std::cmp::Ordering::Greater);
assert_eq!(canonical_number::compare("-10", "-2"), std::cmp::Ordering::Less);
}
#[allow(dead_code)]
fn unused_operator_reference(op: SelectionOperator) -> &'static str {
op.as_wire_str()
}
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
// ------------------------------------------------------------------------------------------
// R-F: the door is inside `select` - it validates first and returns the typed refusal, and the
// single-row fast path refuses (loudly, not just in debug builds) to answer a hop-bearing
// selection at all.
// ------------------------------------------------------------------------------------------
#[test]
fn select_refuses_an_invalid_selection_instead_of_evaluating_it() {
let rows = base_rows();
let selection = Selection { schemas: Some(Vec::new()), ..Selection::default() };
let result = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key());
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert!(matches!(result, Err(SelectionRefusal::Invalid(ref invalid)) if invalid.field == "schemas"));
}
// A plain assert (not #[should_panic]) so a mutant that removes the panic reads as an ordinary
// FAILED test rather than the harness having to special-case should_panic's own output shape.
#[test]
fn matches_refuses_a_hop_bearing_selection_even_in_a_release_style_assert() {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let row = FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5");
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
let selection = Selection {
cited: Some(Incoming { role: "Design".into(), exists: true }),
..Selection::default()
};
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cultnet_rs::matches(&row, &selection);
}));
let panicked = outcome.is_err();
assert!(panicked, "matches() must panic on a hop-bearing selection, not answer wrong");
}
// ------------------------------------------------------------------------------------------
// R-H: the cursor digest is length-prefixed, so no separator character can make two distinct
// selections collide (Soul: `["a|b"]` and `["a","b"]` used to digest the same).
// ------------------------------------------------------------------------------------------
#[test]
fn cursor_digest_does_not_collide_on_differently_split_lists() {
let one_joined_value = Selection {
fields: Some(vec![FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["a|b".into()]),
number: None,
}]),
..Selection::default()
};
let two_split_values = Selection {
fields: Some(vec![FieldPredicate {
index: "kind".into(),
op: "any_of".into(),
values: Some(vec!["a".into(), "b".into()]),
number: None,
}]),
..Selection::default()
};
assert_ne!(
Cursor::compute_digest(1, 1, "s", "k", &one_joined_value, test_cursor_key()),
Cursor::compute_digest(1, 1, "s", "k", &two_split_values, test_cursor_key()),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
"a length-prefixed digest must not let list-splitting collide"
);
let one_key = Selection { keys: Some(vec!["a,b".into()]), ..Selection::default() };
let two_keys = Selection { keys: Some(vec!["a".into(), "b".into()]), ..Selection::default() };
assert_ne!(
Cursor::compute_digest(1, 1, "s", "k", &one_key, test_cursor_key()),
Cursor::compute_digest(1, 1, "s", "k", &two_keys, test_cursor_key())
);
}
// ------------------------------------------------------------------------------------------
// R-Y: the digest covers the cursor's own body (`asOf`, `ordinal`, `schemaId`, `recordKey`), not
// only the selection - a caller holding one valid cursor cannot rewrite its position and reuse
// the digest.
// ------------------------------------------------------------------------------------------
#[test]
fn cursor_digest_does_not_collide_across_rewritten_positions() {
let selection = Selection::default();
let key = test_cursor_key();
let base = Cursor::compute_digest(1, 5, "schema-a", "k1", &selection, key);
let rewritten_ordinal = Cursor::compute_digest(1, 6, "schema-a", "k1", &selection, key);
let rewritten_key = Cursor::compute_digest(1, 5, "schema-a", "k2", &selection, key);
let rewritten_as_of = Cursor::compute_digest(2, 5, "schema-a", "k1", &selection, key);
let rewritten_schema = Cursor::compute_digest(1, 5, "schema-b", "k1", &selection, key);
assert_ne!(base, rewritten_ordinal, "a rewritten ordinal must not reuse the digest");
assert_ne!(base, rewritten_key, "a rewritten record key must not reuse the digest");
assert_ne!(base, rewritten_as_of, "a rewritten asOf must not reuse the digest");
assert_ne!(base, rewritten_schema, "a rewritten schemaId must not reuse the digest");
}
// A cursor whose position was rewritten but whose digest bytes were reused (a forgery only
// possible with access to the encoded body, not the process key) is covered by the in-module
// unit test next to `Cursor` itself (`selection.rs`'s `cursor_tests`), where the private `digest`
// field and the length-prefix helpers used to splice a forged body are directly reachable. This
// integration test only exercises the public surface: two cursors minted for different positions
// under the same selection and key never share a digest, which is what makes that forgery
// impossible without the raw bytes.
#[test]
fn cursor_minted_for_a_different_position_never_shares_a_digest() {
let rows = base_rows();
let selection = Selection { limit: Some(1), ..Selection::default() };
let page = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
let genuine = page.next_cursor.expect("more than one row");
let parsed = Cursor::parse(&genuine).expect("mints decode");
let d1 = Cursor::compute_digest(1, parsed.ordinal, &parsed.schema_id, &parsed.record_key, &selection, test_cursor_key());
let d2 = Cursor::compute_digest(1, parsed.ordinal + 1, &parsed.schema_id, &parsed.record_key, &selection, test_cursor_key());
let d3 = Cursor::compute_digest(1, parsed.ordinal, &parsed.schema_id, "some-other-key", &selection, test_cursor_key());
assert_ne!(d1, d2, "a rewritten ordinal must not reuse the digest");
assert_ne!(d1, d3, "a rewritten record key must not reuse the digest");
}
// ------------------------------------------------------------------------------------------
// R-O: the cursor is keyed. A forged cursor, a cursor minted by another process, and a record
// key carrying the pre-fix delimiter character are all refused or round-trip correctly under the
// keyed, length-prefixed cursor.
// ------------------------------------------------------------------------------------------
#[test]
fn cursor_forged_without_the_key_is_refused() {
let rows = base_rows();
let selection = Selection { limit: Some(1), ..Selection::default() };
let page = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
let genuine = page.next_cursor.expect("more than one row");
// A forgery attempt: decode the genuine cursor, flip one byte inside its digest (the tail of
// the length-prefixed body), and re-encode - the shape (five length-prefixed fields) stays
// valid, only the digest content is wrong, exactly what an attacker guessing the selection's
// shape without the key would produce.
let mut bytes = base64::Engine::decode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
&genuine,
)
.expect("genuine cursor decodes");
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
let forged = base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &bytes);
let result = select(
&FixtureRowSet,
&rows,
&Selection { limit: Some(1), cursor: Some(forged), ..Selection::default() },
1,
test_cursor_key(),
);
assert!(matches!(result, Err(SelectionRefusal::CursorInvalid { .. })));
}
#[test]
fn cursor_minted_by_another_process_is_refused() {
let rows = base_rows();
let selection = Selection { limit: Some(1), ..Selection::default() };
let matched = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
let last_row = matched.rows.last().expect("more than one row");
// A different process holds a different random key (R-O) - this cursor is genuine, just not
// minted under the key the answering server in this test holds.
let other_process_key = CursorKey::random();
let cursor = Cursor::mint(1, last_row, &selection, &other_process_key);
let result = select(
&FixtureRowSet,
&rows,
&Selection { limit: Some(1), cursor: Some(cursor), ..Selection::default() },
1,
test_cursor_key(),
);
assert!(matches!(result, Err(SelectionRefusal::CursorInvalid { .. })));
}
// F6: the pre-fix C# cursor used U+241F as its field delimiter, so a record key carrying that
// exact character broke the server's own cursor. R-O's length-prefixed body has no delimiter
// character at all, so this (and any other character a record key might carry) round-trips.
#[test]
fn cursor_round_trips_a_record_key_carrying_the_pre_fix_delimiter_character() {
let row = FixtureRow::leaf(leaf_a(), "row-\u{241F}-with-the-old-delimiter", 7, "weapon", "5");
let selection = Selection::default();
let cursor_text = Cursor::mint(9, &row, &selection, test_cursor_key());
let cursor = Cursor::parse(&cursor_text).expect("length-prefixed body decodes");
assert_eq!(cursor.as_of, 9);
assert_eq!(cursor.ordinal, 7);
assert_eq!(cursor.schema_id, leaf_a());
assert_eq!(cursor.record_key, "row-\u{241F}-with-the-old-delimiter");
}
// R-N (docs/cultnet-selection-cut.md, fix batch 3) is pending on this branch: the C# reference's
// exact `code`/`details` shape has not landed, and Self's instruction (2026-09-22) is not to guess
// it. No test here exercises it yet; add the wire-decode tests alongside the mapping once it lands.
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
// ------------------------------------------------------------------------------------------
// R-E: the alias matcher, exercised at the `select`/`validate` level (the module's own unit
// tests in cultnet_rs::selection::schema_alias::tests cover the string algorithm directly).
// ------------------------------------------------------------------------------------------
#[test]
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
fn schemas_reaches_a_schema_by_its_unversioned_name_alias() {
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
let rows = base_rows();
let selection = Selection {
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
schemas: Some(vec![leaf_a_name_alias().into()]),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
let mut ids: Vec<&str> = evaluation.rows.iter().map(Row::record_key).collect();
ids.sort_unstable();
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
assert_eq!(ids, vec!["a-eq", "a-lo", "shared-1", "shared-3"], "every leaf_a row, by name alias");
}
// R-E, Self's ruling 2026-09-22: a hash-shaped alias ("<hash>.v1") is not a wire form the C#
// reference's production rule ever resolves - it strips the ".v1" and compares the bare hash text
// against the schema's declared *name*, never equal. `schemas` never refuses (unlike cites.target
// below), so an unresolved alias just narrows reachability to nothing.
#[test]
fn schemas_does_not_reach_a_schema_by_a_hash_shaped_alias() {
let rows = base_rows();
let selection = Selection {
schemas: Some(vec![leaf_a_hash_alias().into()]),
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
assert!(evaluation.rows.is_empty(), "a hash-shaped alias must not resolve to leaf_a");
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
}
// R-E, at the fast path `matches()` uses directly (the v0 snapshot server's own caller,
// serve_read_only_raw_snapshot, never calls validate/select - v0 has its own lowering instead).
// An empty-but-present schemas list must match nothing there too, the same as the door's own
// rule for select() - schema_alias::matches_any's own "empty candidates matches everything"
// convention (ported faithfully from the C# reference's MatchesAny, for the door/reachability
// call sites that already refuse or normalise an empty list before this ever runs) must not leak
// into this call site, which has no such guarantee upstream of it.
#[test]
fn matches_treats_an_empty_but_present_schemas_list_as_matching_nothing() {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
let row = FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5");
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
let selection = Selection { schemas: Some(Vec::new()), ..Selection::default() };
assert!(
!cultnet_rs::matches(&row, &selection),
"an empty-but-present schemas list must never be read as \"no filter\" by the evaluator"
);
}
#[test]
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
fn cites_target_resolves_through_the_name_alias_matcher() {
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
let rows = base_rows();
let selection = Selection {
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
cites: Some(Citation { target: rr(leaf_a_name_alias(), "a-eq"), role: Some("Design".into()) }),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
..Selection::default()
};
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert_eq!(evaluation.rows.iter().map(Row::record_key).collect::<Vec<_>>(), vec!["citer-1"]);
}
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
// R-E, Self's ruling 2026-09-22: a hash-shaped alias resolves to no known schema (see
// schemas_does_not_reach_a_schema_by_a_hash_shaped_alias above), so a cites.target naming one is
// refused at the door (R-E's unmatched-target rule), not silently answered with an empty page.
#[test]
fn cites_target_by_a_hash_shaped_alias_is_refused_as_unmatched() {
let selection = Selection {
cites: Some(Citation { target: rr(leaf_a_hash_alias(), "a-eq"), role: Some("Design".into()) }),
..Selection::default()
};
let error = validate(&selection, &FixtureRowSet).expect_err("hash-shaped alias must be refused");
assert_eq!(error.field, "cites.target.schemaId");
}
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
#[test]
fn validate_refuses_a_cites_target_schema_that_matches_no_known_schema() {
let selection = Selection {
cites: Some(Citation {
target: rr("sha256:not-a-known-schema", "a-eq"),
role: Some("Design".into()),
}),
..Selection::default()
};
let error = validate(&selection, &FixtureRowSet).expect_err("unmatched target must be refused");
assert_eq!(error.field, "cites.target.schemaId");
}
#[test]
fn schema_alias_module_is_reachable_from_the_crate_root() {
CultNet selection: port the C# alias rule into cultnet-rs, delete the hash matcher Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an alias is a schema *name* with a trailing .vN suffix, resolved against the descriptor's declared name - never against a hash id. C#'s production call sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare id-vs-id overload. - Row gains schema_name() (defaults to schema_id() for rows with no separate name, e.g. raw wire records); RowSet gains schema_name(schema_id) -> Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which this crate has no type for (D7). - schema_alias::matches/matches_any now take (candidate, schema_id, schema_name) and resolve a candidate's inferred name against the declared name, exactly as the C# descriptor overload does - not against schema_id's own inferred name, which is what the deleted hash-stripping matcher did. - Every call site (reachable_schemas, the cites.target door check, matches_schema_keys_fields's D6 fast path, matches_citation) now resolves and passes a schema's name instead of comparing two schema-id strings. - Deleted: the hash-stripping schema_alias::matches(candidate, schema_id) two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form either runtime's production rule ever resolves. Behaviour verified against a standalone run of the exact C# algorithm (CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all convention, ordinal case sensitivity, ".v" with no trailing digits, and multiple ".v" markers (last one wins) - every case is now a Rust unit test in schema_alias::tests. Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries are now a shared negative check (neither runtime resolves them), and the nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime defect (schemas_by_name_alias). Regenerated contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16 vectors): added refuses_cites_target_by_hash_shaped_alias, renamed hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias. cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two failures are both the pending unmatched-cites.target door refusal (CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the other Hands' C# door fix covers both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:35 +02:00
assert!(schema_alias::matches(leaf_a_name_alias(), leaf_a(), leaf_a_name()));
assert!(!schema_alias::matches(leaf_a_name_alias(), leaf_a(), leaf_b_name()));
assert!(!schema_alias::matches(leaf_a_hash_alias(), leaf_a(), leaf_a_name()));
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
}
// ------------------------------------------------------------------------------------------
// R-G/P-1: `matched` is the total match count, not the page count.
// ------------------------------------------------------------------------------------------
#[test]
fn matched_is_the_total_count_not_the_page_count() {
let rows = base_rows();
let selection = Selection { limit: Some(2), ..Selection::default() };
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).unwrap();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert_eq!(evaluation.rows.len(), 2, "the page is clipped to the limit");
assert_eq!(evaluation.matched, 7, "matched counts every row base_rows() carries");
}
// ------------------------------------------------------------------------------------------
// R-I: `select_page` projects `select`'s generic rows into the wire's `SelectionPage` - a header
// projection carries no payload, in the page's rows and in its edges alike (S20).
// ------------------------------------------------------------------------------------------
fn document_record_for(row: &FixtureRow) -> SelectionDocumentRecord {
SelectionDocumentRecord {
schema_id: row.schema_id.to_string(),
schema_name: None,
schema_version: None,
schema_content_hash: None,
record_key: row.record_key.to_string(),
stored_at: "2026-09-22T00:00:00Z".to_string(),
payload_encoding: "messagepack".to_string(),
payload: vec![0xC0], // a nonempty payload every header-projection assertion must not see
source_runtime_id: None,
source_agent_id: None,
source_role: None,
tags: None,
}
}
#[test]
fn select_page_header_projection_carries_no_payload_in_rows_or_edges() {
let rows = vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), "a-eq".to_string(), Some(vec![7]))]),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
];
let selection = Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
projection: "header".into(),
..Selection::default()
};
let page: SelectionPage =
select_page(&FixtureRowSet, &rows, &selection, 1, test_cursor_key(), document_record_for).unwrap();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert!(page.documents.is_none());
let headers = page.headers.expect("header projection carries headers");
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].record_key, "citer-1");
let edges = page.edges.expect("a hop-bearing selection carries edges");
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].payload, None, "S20: a header projection's edges carry no payload");
assert_eq!(edges[0].payload_encoding, None);
}
#[test]
fn select_page_document_projection_carries_payload_in_rows_and_edges() {
let rows = vec![
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
R-AF/R-AG/R-AJ: Rust parity, hostile-cursor, and ordering-composition tests R-AF: FixtureRow's references (and every fixture/hand-built row that constructs them) carry a bare target key now, matching Row::references's new contract. Adds citer_narrow() (the shared fixture's real schema id, distinct from this file's own local NARROW_CITER) to the fixture machinery, and two direct behavioural tests (shared_key_resolves_to_the_row_inside_the_declared_target_in_either_row_order, shared_key_refuses_when_the_only_candidate_is_outside_the_declared_target) pinning both shapes from Soul's table at the select() level, plus a shared-fixture vector case for the accept shape and an evaluation_refusal_cases entry (expected_evaluation_refusal, read by the matching C# addition) for the refuse shape. Rewrites cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target (renamed cites_refuses_an_out_of_target_edge_under_the_same_queried_role): its original shape queried one role while expecting refusal from an edge under an unrelated role, a stronger guarantee than C#'s own R-T(F15) test gives (same role, two elements) - found because it broke once matches_citation's role-filter order was fixed to match C#. The new version mirrors C#'s actual test shape. R-AG: hostile cursor tests feeding Cursor::parse a length prefix near usize::MAX (Rust's own proven overflow), near i32::MAX (C#'s proven overflow, fed here too per the ruling - neither side's tests are evidence about the other), a prefix wider than any integer type, and a negative-looking prefix - all under std::panic::catch_unwind, asserting a typed CursorInvalid and never a panic. R-AJ: edge_order_is_from_key_before_role_when_both_vary_at_the_same_anchor (a both-hops selection with a hub/other citer pair whose keys and roles are deliberately anti-correlated, so a Role-first mutant and the real rule disagree) and row_tiebreak_orders_by_schema_id_before_record_key_when_both_vary_at_a_tied_ordinal (the same anti-correlation trick applied to order_rows, computed against the real leaf_a/leaf_b ids at runtime). FixtureRow gains #[derive(Debug)] - needed for an assert message on the new evaluation-refusal-case writer, harmless elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:43:46 +02:00
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), "a-eq".to_string(), Some(vec![7]))]),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
];
let selection = Selection {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
projection: "document".into(),
..Selection::default()
};
let page: SelectionPage =
select_page(&FixtureRowSet, &rows, &selection, 1, test_cursor_key(), document_record_for).unwrap();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert!(page.headers.is_none());
let documents = page.documents.expect("document projection carries documents");
assert_eq!(documents.len(), 1);
assert_eq!(documents[0].payload, vec![0xC0]);
let edges = page.edges.expect("a hop-bearing selection carries edges");
assert_eq!(edges[0].payload, Some(vec![7]));
assert_eq!(edges[0].payload_encoding.as_deref(), Some("messagepack"));
}
#[test]
fn select_page_matched_is_the_total_count() {
let rows = base_rows();
let selection = Selection { limit: Some(2), ..Selection::default() };
let page: SelectionPage = select_page(&FixtureRowSet, &rows, &selection, 1, test_cursor_key(), document_record_for).unwrap();
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
assert_eq!(page.matched, 7);
assert_eq!(page.headers.unwrap().len(), 2);
}
// ------------------------------------------------------------------------------------------
// R-M: `SelectionDocumentRecord`/`RawDocumentHeader`/`Edge`/`RecordRef` used to be encoded and
// decoded by ~335 lines of hand-written `rmpv::Value` construction (contracts.rs). R-M replaced
// that with the generic `rmpv::ext::from_value`/`to_value` bridge over their own serde derives.
// This proves the replacement round-trips through the real message envelope
// (`cultnet.snapshot_response_raw.v1`, MessagePack, named maps) both ways - headers, documents
// with a real payload, and edges with and without a payload.
// ------------------------------------------------------------------------------------------
#[test]
fn snapshot_response_raw_v1_round_trips_headers_documents_and_edges_through_the_generic_codec() {
let message = CultNetMessage::SnapshotResponseRawV1 {
message_id: "msg-1".to_string(),
matched: 3,
as_of: 42,
next: Some("cursor-1".to_string()),
// A message carries exactly one of headers/documents (validate_message's own invariant,
// matching Selection::projection); this vector exercises the document side, the richer of
// the two.
headers: None,
documents: Some(vec![SelectionDocumentRecord {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schema_id: leaf_a().to_string(),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
schema_name: None,
schema_version: None,
schema_content_hash: None,
record_key: "a-eq".to_string(),
stored_at: "2026-09-22T00:00:00Z".to_string(),
payload_encoding: "messagepack".to_string(),
payload: vec![1, 2, 3, 4, 5],
source_runtime_id: None,
source_agent_id: None,
source_role: None,
tags: None,
}]),
edges: Some(vec![
Edge {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
from: rr(citer(), "citer-1"),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
role: "Design".to_string(),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
to: rr(leaf_a(), "a-eq"),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
payload_encoding: Some("messagepack".to_string()),
payload: Some(vec![9, 9]),
},
Edge {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
from: rr(citer(), "citer-1"),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
role: "parent".to_string(),
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
to: rr(leaf_b(), "b-hi"),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
payload_encoding: None,
payload: None,
},
]),
shard_id: Some("shard-1".to_string()),
shard_epoch: Some(5),
shard_log_sequence: Some(100),
};
let bytes =
encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0).expect("encodes");
let decoded = decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0)
.expect("decodes");
assert_eq!(decoded, message);
}
// The header side of the same message shape (S20: no payload field at all, since
// `RawDocumentHeader` has none to carry).
#[test]
fn snapshot_response_raw_v1_round_trips_headers_through_the_generic_codec() {
let message = CultNetMessage::SnapshotResponseRawV1 {
message_id: "msg-2".to_string(),
matched: 1,
as_of: 7,
next: None,
headers: Some(vec![RawDocumentHeader {
CultNet selection cut1 Rust: build from the shared parity fixture Companion to the C# side's shared-fixture commit. Replaces the invented fake sha256-shaped LEAF_A/LEAF_B/CITER/LEAF_A_ALIAS consts with lazily-loaded values read from contracts/cultnet/interop/selection-vectors.fixture.json (a std::sync::OnceLock-backed loader; FixtureRowSet's declared_indexes/ declared_roles/is_numeric/target_leaves become runtime string comparisons instead of const match patterns). NARROW_CITER stays a local, unshared id - it never appears in a cross-runtime vector. base_rows() keeps S2-S23's original 7-row set (now sourced from the fixture's first 7 rows, unchanged in value or order, so every test that enumerates it - orders_by_ordinal_then_identity_and_reverses, pages_exactly_once_and_refuses_a_stale_or_mismatched_cursor, matched_is_the_total_count_not_the_page_count, ... - still holds unchanged). A new all_fixture_rows() carries all ten rows (the fix-batch float-tie and astral/BMP rows too) for the two parity-vector functions only. write_selection_vectors_for_the_reference gains float_tie_exact_ge, astral_key_code_point_order, and the fixture's hash alias (leaf_a_hash_alias(), a synthetic ".v1" this crate's own schema_alias resolves) and name alias (leaf_a_name_alias(), "leaf_a.v9" - the form only CultNetSchemaAliasMatching's descriptor overload resolves) probes, mirroring the C# side. selection_vectors_written_by_the_ reference_decode_and_evaluate_identically_in_rust no longer panics on the first disagreement (evaluate_vector now returns a Result instead of .expect()'ing); it collects every vector's mismatch and fails once with the full list, matching the C# reader's same change. Regenerates selection-vectors.rs-written.json from the shared fixture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:33:55 +02:00
schema_id: leaf_a().to_string(),
CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs. - R-B: hop edges now follow the hop's direction - cites keys an edge off its From (the citer), cited off its To (the citee), each in page-row order then (from, role, to) code-point order. Before this, both hop kinds keyed off From, so a cited selection's edges were silently empty unless the citer also happened to be on the page (Soul's F2). build_incoming_index iterates the caller's own row slice, never a HashMap, for its own order. - R-C: code-point order was already Rust's default str::cmp (UTF-8 byte order); no change needed there. - R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754 decimal expansion from mantissa and exponent, via a small fixed-point bignum (~60 lines), replacing the previous shortest-round-trip-form path that rounded ties differently from .NET. expand_scientific_notation and its test are gone (R-M); the Display probe test is kept per the map. - R-E: schema_alias (ported from CultNetSchemaAliasMatching's string overload) is the one schema-identity rule for `schemas` and `cites.target.schemaId`; an unmatched cites target is refused at the door. Found and fixed along the way: matches_schema_keys_fields must not use schema_alias::matches_any's own "empty candidates matches everything" shortcut (correct for the door/reachability call sites it mirrors from C#) - v0's snapshot server calls matches() directly, without validate(), so an empty-but-present schemas list must still match nothing there too. - R-F: select() validates first and returns the typed refusal; matches()'s hop guard is a real assert!, not a debug_assert! a release build would compile out. - R-G/P-1: Evaluation and SelectionPage both carry matched, the total match count, not the page count. - R-H: Cursor::compute_digest is length-prefixed for every string and list, closing the collision Soul found (["a|b"] and ["a","b"] used to digest the same). - R-I: select_page projects a selection into the wire's SelectionPage - headers or documents under Selection::projection, matched, and edges; a header projection carries no payload, in edges too (S20). Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the old fixture's schema names doubling as ids, which had silently masked the R-E gap (vectors passed only because "leaf_a" happened to already be both the alias and the id). Two documented discrepancies against the map's own worked numbers, both verified independently outside this crate (Rust bit decomposition plus a Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact expansion, not "0.0000001" (a stale shortest-form assertion this fix batch did not originally touch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:08:06 +02:00
schema_name: Some("leaf_a".to_string()),
schema_version: Some("v1".to_string()),
schema_content_hash: Some("hash".to_string()),
record_key: "a-eq".to_string(),
stored_at: "2026-09-22T00:00:00Z".to_string(),
source_runtime_id: Some("runtime-1".to_string()),
source_agent_id: None,
source_role: None,
tags: Some(vec!["tag-a".to_string()]),
}]),
documents: None,
edges: None,
shard_id: None,
shard_epoch: None,
shard_log_sequence: None,
};
let bytes =
encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0).expect("encodes");
let decoded = decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0)
.expect("decodes");
assert_eq!(decoded, message);
}