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>
This commit is contained in:
Meta Vens 2026-09-23 04:43:46 +02:00
commit dd08be4f32

View file

@ -64,11 +64,12 @@ struct FixtureSchemaDef {
hash_alias: String,
}
// 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.
#[derive(serde::Deserialize)]
struct FixtureReferenceDef {
role: String,
#[serde(rename = "targetSchema")]
target_schema: String,
#[serde(rename = "targetKey")]
target_key: String,
}
@ -156,6 +157,12 @@ fn leaf_b() -> &'static str {
fn citer() -> &'static str {
&fixture_schema("citer").schema_id
}
// 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
}
// 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.
@ -168,6 +175,9 @@ fn leaf_b_name() -> &'static str {
fn citer_name() -> &'static str {
"citer"
}
fn citer_narrow_name() -> &'static str {
"citer_narrow"
}
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 -
@ -201,32 +211,36 @@ fn build_row(row: &FixtureRowDef) -> FixtureRow {
)
}
"citer" => {
// R-AF: the stored edge is a bare target key - the wire carries no schema for it.
let references = row
.references
.iter()
.map(|reference| {
let target_id = match reference.target_schema.as_str() {
"leaf_a" => leaf_a(),
"leaf_b" => leaf_b(),
other => panic!("selection-vectors.fixture.json: unknown reference target schema '{other}'"),
};
(reference.role.clone(), rr(target_id, leak(reference.target_key.clone())), None)
})
.map(|reference| (reference.role.clone(), reference.target_key.clone(), None))
.collect();
FixtureRow::citer(leak(row.key.clone()), row.ordinal, references)
}
"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)
}
other => panic!("selection-vectors.fixture.json: unknown schema '{other}'"),
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
struct FixtureRow {
schema_id: &'static str,
record_key: &'static str,
ordinal: i64,
kind: Option<&'static str>,
mass: Option<&'static str>,
references: Vec<(String, RecordRef, Option<Vec<u8>>)>,
// 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>>)>,
}
impl FixtureRow {
@ -241,7 +255,7 @@ impl FixtureRow {
}
}
fn citer(record_key: &'static str, ordinal: i64, references: Vec<(String, RecordRef, Option<Vec<u8>>)>) -> Self {
fn citer(record_key: &'static str, ordinal: i64, references: Vec<(String, String, Option<Vec<u8>>)>) -> Self {
Self {
schema_id: citer(),
record_key,
@ -251,6 +265,20 @@ impl FixtureRow {
references,
}
}
// 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,
}
}
}
// Shared by Row::schema_name and RowSet::schema_name below - one place that knows which fixture
@ -262,6 +290,8 @@ fn schema_name_for(schema_id: &str) -> Option<&'static str> {
Some(leaf_b_name())
} else if schema_id == citer() {
Some(citer_name())
} else if schema_id == citer_narrow() {
Some(citer_narrow_name())
} else if schema_id == NARROW_CITER {
Some(NARROW_CITER_NAME)
} else {
@ -295,7 +325,7 @@ impl Row for FixtureRow {
None
}
}
fn references(&self) -> Vec<(String, RecordRef, Option<Vec<u8>>)> {
fn references(&self) -> Vec<(String, String, Option<Vec<u8>>)> {
self.references.clone()
}
}
@ -308,6 +338,7 @@ impl RowSet for FixtureRowSet {
leaf_a().into(),
leaf_b().into(),
citer().into(),
citer_narrow().into(),
NARROW_CITER.into(),
]
}
@ -332,7 +363,7 @@ impl RowSet for FixtureRowSet {
// is itself cited by another citer (rather than only ever by a leaf).
"peer".into(),
]
} else if schema_id == NARROW_CITER {
} else if schema_id == NARROW_CITER || schema_id == citer_narrow() {
vec!["narrow_ref".into()]
} else {
Vec::new()
@ -423,6 +454,32 @@ fn orders_by_ordinal_then_identity_and_reverses() {
);
}
// 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")]);
}
// 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]
@ -506,8 +563,8 @@ fn cites_role_excludes_a_second_reference_at_the_same_target() {
"double-citer",
2,
vec![
("Design".to_string(), rr(leaf_a(), "a-eq"), None),
("OtherRole".to_string(), rr(leaf_a(), "a-eq"), None),
("Design".to_string(), "a-eq".to_string(), None),
("OtherRole".to_string(), "a-eq".to_string(), None),
],
),
];
@ -562,19 +619,34 @@ fn cited_exists_is_the_one_negation() {
// 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: 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");
let citer_row =
FixtureRow::citer("citer-1", 3, vec![("Design".to_string(), rr(leaf_a(), "shared-key"), None)]);
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 {
cites: Some(Citation { target: rr(leaf_a(), "shared-key"), role: Some("Design".into()) }),
cites: Some(Citation { target: rr(leaf_a(), "shared-key"), role: Some("narrow_ref".into()) }),
..Selection::default()
};
let cited_selection = Selection {
cited: Some(Incoming { role: "Design".into(), exists: true }),
cited: Some(Incoming { role: "narrow_ref".into(), exists: true }),
..Selection::default()
};
@ -616,8 +688,8 @@ fn row_identity_is_schema_and_key_not_key_alone_in_either_row_order() {
#[test]
fn a_selection_with_both_hops_returns_both_directions_edges() {
let leaf = FixtureRow::leaf(leaf_a(), "leaf-row", 1, "weapon", "5");
let ca = FixtureRow::citer("ca", 2, vec![("Design".to_string(), rr(leaf_a(), "leaf-row"), None)]);
let cb = FixtureRow::citer("cb", 3, vec![("peer".to_string(), rr(citer(), "ca"), None)]);
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 {
@ -643,6 +715,40 @@ fn a_selection_with_both_hops_returns_both_directions_edges() {
assert_eq!(seen, vec![("ca", "Design", "leaf-row"), ("cb", "peer", "ca")]);
}
// 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")]);
}
// S16: the four comparisons at the boundary, including the row whose value equals the compared
// number exactly.
#[test]
@ -688,7 +794,7 @@ fn refuses_an_edge_outside_its_declared_target() {
ordinal: 2,
kind: None,
mass: None,
references: vec![("narrow_ref".to_string(), rr(leaf_b(), "b"), None)],
references: vec![("narrow_ref".to_string(), "b".to_string(), None)],
},
];
let selection = Selection {
@ -699,29 +805,104 @@ fn refuses_an_edge_outside_its_declared_target() {
assert!(matches!(result, Err(SelectionRefusal::ReferenceOutsideTarget { .. })));
}
// R-T/F15: `cites` checks every edge a citer carries against its declared target, not only the
// one the citation's own role/key filters ask about. Before the fix, a `cites` selection that
// asked about the citer's legitimate "Design" edge never looked at its "narrow_ref" edge, so a
// corrupt "narrow_ref" (pointed at a schema outside its declared target) hid behind the filter.
// 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 cites_refuses_a_row_carrying_an_unrelated_edge_outside_its_declared_target() {
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-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]
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"),
FixtureRow::leaf(leaf_b(), "b-hi", 2, "shield", "9"),
FixtureRow {
schema_id: NARROW_CITER,
record_key: "bad-citer",
ordinal: 3,
kind: None,
mass: None,
references: vec![
("Design".to_string(), rr(leaf_a(), "a-eq"), None),
("narrow_ref".to_string(), rr(leaf_b(), "b-hi"), None),
// 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),
],
},
),
];
// 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 {
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
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());
@ -737,8 +918,8 @@ fn dictionary_reference_entries_keep_distinct_payload_bytes() {
"assembly",
3,
vec![
("components".to_string(), rr(leaf_a(), "part-1"), Some(vec![1, 5])),
("components".to_string(), rr(leaf_a(), "part-2"), Some(vec![2, 5])),
("components".to_string(), "part-1".to_string(), Some(vec![1, 5])),
("components".to_string(), "part-2".to_string(), Some(vec![2, 5])),
],
);
let payloads: std::collections::HashSet<Vec<u8>> =
@ -760,7 +941,7 @@ fn cited_selections_page_the_citee_and_their_edges_survive_the_page_filter() {
FixtureRow::citer(
"assembly",
2,
vec![("components".to_string(), rr(leaf_a(), "part-1"), Some(vec![9]))],
vec![("components".to_string(), "part-1".to_string(), Some(vec![9]))],
),
];
let selection = Selection {
@ -782,7 +963,7 @@ fn cited_selections_page_the_citee_and_their_edges_survive_the_page_filter() {
fn cites_selections_still_page_the_citer_and_key_edges_off_it() {
let rows = vec![
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), rr(leaf_a(), "a-eq"), None)]),
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), "a-eq".to_string(), None)]),
];
let selection = Selection {
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
@ -979,6 +1160,13 @@ struct Vector {
// 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: 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>,
#[serde(rename = "expectedIds", default)]
expected_ids: Vec<String>,
#[serde(default)]
@ -1118,6 +1306,30 @@ fn selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in
continue;
}
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;
}
let evaluated = match evaluate_vector(vector) {
Ok(evaluated) => evaluated,
Err(reason) => {
@ -1252,9 +1464,19 @@ fn write_selection_vectors_for_the_reference() {
// 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: 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.
(
"hop_by_role_cites_exact_id",
Selection {
keys: Some(vec!["citer-1".into()]),
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
..Selection::default()
},
@ -1263,9 +1485,11 @@ fn write_selection_vectors_for_the_reference() {
// 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: keys scoping for the same reason as hop_by_role_cites_exact_id above.
(
"hop_by_role_cites_name_alias",
Selection {
keys: Some(vec!["citer-1".into()]),
cites: Some(Citation { target: rr(leaf_a_name_alias(), "a-eq"), role: Some("Design".into()) }),
..Selection::default()
},
@ -1324,13 +1548,48 @@ fn write_selection_vectors_for_the_reference() {
..Selection::default()
},
),
// 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()
},
),
];
let mut vectors = Vec::new();
for (name, selection) in cases {
let bytes = rmp_serde::to_vec_named(&selection).expect("encodes");
let rows = all_fixture_rows();
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key()).expect("valid selection");
let evaluation = select(&FixtureRowSet, &rows, &selection, 1, test_cursor_key())
.unwrap_or_else(|e| panic!("case {name:?} failed: {e:?}"));
let ids: Vec<String> = evaluation
.rows
.iter()
@ -1354,6 +1613,7 @@ fn write_selection_vectors_for_the_reference() {
selection_message_pack_base64: base64::engine::general_purpose::STANDARD.encode(&bytes),
as_of: 1,
expected_refusal_field: None,
expected_evaluation_refusal: None,
matched: evaluation.matched,
has_next: evaluation.next_cursor.is_some(),
expected_ids: ids,
@ -1416,6 +1676,40 @@ fn write_selection_vectors_for_the_reference() {
selection_message_pack_base64: base64::engine::general_purpose::STANDARD.encode(&bytes),
as_of: 1,
expected_refusal_field: Some(expected_field.to_string()),
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()),
matched: 0,
has_next: false,
expected_ids: Vec::new(),
@ -1779,7 +2073,7 @@ fn document_record_for(row: &FixtureRow) -> SelectionDocumentRecord {
fn select_page_header_projection_carries_no_payload_in_rows_or_edges() {
let rows = vec![
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), rr(leaf_a(), "a-eq"), Some(vec![7]))]),
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), "a-eq".to_string(), Some(vec![7]))]),
];
let selection = Selection {
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
@ -1802,7 +2096,7 @@ fn select_page_header_projection_carries_no_payload_in_rows_or_edges() {
fn select_page_document_projection_carries_payload_in_rows_and_edges() {
let rows = vec![
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), rr(leaf_a(), "a-eq"), Some(vec![7]))]),
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), "a-eq".to_string(), Some(vec![7]))]),
];
let selection = Selection {
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),