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>