CultNet selection cut1 fix batch: regenerate the C#-written parity vectors

Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json
under R-B/R-C/R-D/R-E/R-I.

SelectionParityVectorTests.cs: the six original cases' fixture
(BuildFixture) is left untouched, because SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
reads the already-committed selection-vectors.rs-written.json against
it, and a new row reachable by an unfiltered or loosely-filtered case
(descending_with_limit, gt_numeric_boundary) would silently change that
case's expected page. The fix-batch rows and cases live in a new
BuildExtendedFixture/Cases set, additive over BuildFixture, used only
by WriteSelectionVectors:
- float_tie_exact_ge: a row at 2233759.25f, on the round-to-even tie
  R-D's exact BigInteger rendering fixes (its canonical decimal is
  "2233759.25" regardless of which shortest form a runtime's ToString
  would have picked).
- cites_target_by_alias: cites.target.schemaId given as "leaf_a"
  (ParityLeafA's SchemaName) against a-eq's real content-hash SchemaId,
  exercising R-E's alias-matched cites (the pre-fix code required an
  exact SchemaId match and this always missed).
- astral_key_code_point_order: two rows sharing one ordinal, keyed by
  a BMP private-use character (U+E000) and an astral character
  (U+1F602, surrogate pair 😂), pinning R-C's code-point
  tiebreak order.
- header_projection/document_projection: the same hop-bearing match set
  under each projection.

Known-expected failure: SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
fails against the currently-committed selection-vectors.rs-written.json
(incoming_exists_true's edge count) because that file predates R-B and
was not touched here - the parallel Rust Hands owns it. Re-run once it
regenerates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 09:40:18 +02:00
commit 3d32c67821
2 changed files with 157 additions and 10 deletions

View file

@ -18,9 +18,9 @@
"asOf": 1,
"expectedRefusalField": null,
"expectedIds": [
"citer/citer-1",
"leaf_a/shared-3",
"leaf_b/shared-2"
"leaf_a/\uD83D\uDE02-row",
"leaf_a/\uE000-row",
"leaf_a/tie-mass"
],
"matched": 3,
"hasNext": true,
@ -34,9 +34,10 @@
"expectedIds": [
"leaf_a/a-lo",
"leaf_a/a-eq",
"leaf_b/b-hi"
"leaf_b/b-hi",
"leaf_a/tie-mass"
],
"matched": 3,
"matched": 4,
"hasNext": false,
"expectedEdges": []
},
@ -50,7 +51,14 @@
],
"matched": 1,
"hasNext": false,
"expectedEdges": []
"expectedEdges": [
{
"fromId": "citer/citer-1",
"role": "Design",
"toId": "leaf_a/a-eq",
"payloadBase64": null
}
]
},
{
"name": "incoming_negation",
@ -60,9 +68,12 @@
"expectedIds": [
"leaf_a/a-lo",
"leaf_a/shared-1",
"leaf_a/shared-3"
"leaf_a/shared-3",
"leaf_a/tie-mass",
"leaf_a/\uE000-row",
"leaf_a/\uD83D\uDE02-row"
],
"matched": 3,
"matched": 6,
"hasNext": false,
"expectedEdges": []
},
@ -80,6 +91,76 @@
"hasNext": false,
"expectedEdges": []
},
{
"name": "float_tie_exact_ge",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRtYXNzom9womdlpnZhbHVlc8CmbnVtYmVyqjIyMzM3NTkuMjWlY2l0ZXPApWNpdGVkwKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwqVsaW1pdMCmY3Vyc29ywA==",
"asOf": 1,
"expectedRefusalField": null,
"expectedIds": [
"leaf_a/tie-mass"
],
"matched": 1,
"hasNext": false,
"expectedEdges": []
},
{
"name": "cites_target_by_alias",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHPApWNpdGVzgqZ0YXJnZXSCqHNjaGVtYUlkpmxlYWZfYalyZWNvcmRLZXmkYS1lcaRyb2xlwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedIds": [
"citer/citer-1"
],
"matched": 1,
"hasNext": false,
"expectedEdges": [
{
"fromId": "citer/citer-1",
"role": "Design",
"toId": "leaf_a/a-eq",
"payloadBase64": null
}
]
},
{
"name": "astral_key_code_point_order",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
"asOf": 1,
"expectedRefusalField": null,
"expectedIds": [
"leaf_a/\uE000-row",
"leaf_a/\uD83D\uDE02-row"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "header_projection",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfCpWxpbWl0wKZjdXJzb3LA",
"asOf": 1,
"expectedRefusalField": null,
"expectedIds": [
"leaf_a/\uE000-row",
"leaf_a/\uD83D\uDE02-row"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "document_projection",
"selectionMessagePackBase64": "iadzY2hlbWFzwKRrZXlzwKZmaWVsZHORhKVpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWmbnVtYmVywKVjaXRlc8ClY2l0ZWTAqnByb2plY3Rpb26oZG9jdW1lbnSqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",
"asOf": 1,
"expectedRefusalField": null,
"expectedIds": [
"leaf_a/\uE000-row",
"leaf_a/\uD83D\uDE02-row"
],
"matched": 2,
"hasNext": false,
"expectedEdges": []
},
{
"name": "refuses_empty_schemas_list",
"selectionMessagePackBase64": "iadzY2hlbWFzkKRrZXlzwKZmaWVsZHPApWNpdGVzwKVjaXRlZMCqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8KlbGltaXTApmN1cnNvcsA=",

View file

@ -32,6 +32,20 @@ namespace GameCult.Networking.Tests
private static CultNetSelectionEvaluator.Row Row(CultDocumentRegistry registry, object document, string key, long ordinal) =>
new(registry.GetRequired(document.GetType()), new CultRecordKey(key), document, ordinal);
// R-C: two rows sharing one ordinal so the tiebreak - not the ordinal - decides their order.
// U+E000 is a BMP private-use character (code point 0xE000); U+1F602 is astral, encoded as the
// surrogate pair 😂 whose high half (0xD83D) is less than 0xE000. A UTF-16 code-unit
// compare (the pre-R-C bug) therefore sorts the astral key first; code-point order sorts it
// after, since 0x1F602 > 0xE000.
private const string BmpPrivateUseKey = "-row";
private const string AstralKey = "😂-row";
// Unchanged: this exact row set is what the committed selection-vectors.rs-written.json was
// computed against on the Rust side (an independently constructed but value-identical fixture,
// per the comment on Cases() below) - SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
// reads that committed file, so this fixture cannot grow without breaking that comparison until
// the Rust side's own fixture grows to match. New rows for the fix-batch vectors live in
// BuildExtendedFixture, used only by WriteSelectionVectors (this cut's own cs-written.json).
private static (CultDocumentRegistry registry, CultNetSelectionEvaluator.Row[] rows) BuildFixture()
{
var registry = Registry();
@ -43,11 +57,33 @@ namespace GameCult.Networking.Tests
Row(registry, new ParityLeafA { Name = "shared-1", Kind = "shared", Mass = 1 }, "shared-1", 4),
Row(registry, new ParityLeafB { Name = "shared-2", Kind = "shared", Mass = 1 }, "shared-2", 5),
Row(registry, new ParityLeafA { Name = "shared-3", Kind = "shared", Mass = 1 }, "shared-3", 6),
Row(registry, new ParityCiter { Name = "citer-1", Design = new CultRecordRef<ParityMiddle>(new CultRecordKey("a-eq")) }, "citer-1", 7)
Row(registry, new ParityCiter { Name = "citer-1", Design = new CultRecordRef<ParityMiddle>(new CultRecordKey("a-eq")) }, "citer-1", 7),
};
return (registry, rows);
}
// R-B/R-C/R-D/R-E/R-I fix-batch rows, additive over BuildFixture so every pre-existing
// selection's match set is unchanged (a new row reachable by an unfiltered or loosely filtered
// case, such as descending_with_limit or gt_numeric_boundary, would otherwise silently change
// that case's expected page). Only WriteSelectionVectors (this cut's cs-written.json) uses this.
private static (CultDocumentRegistry registry, CultNetSelectionEvaluator.Row[] rows) BuildExtendedFixture()
{
var (registry, baseRows) = BuildFixture();
var extraRows = new[]
{
// R-D: 2233759.25f is exactly representable and sits on a round-to-even tie between two
// .NET-shortest-form spellings ("2233759.2" vs "2233759.3" between runtimes/versions);
// its exact canonical decimal is "2233759.25" either way. Kind "tie" so it cannot be
// swept up by an existing weapon/armor/shared-kind case.
Row(registry, new ParityLeafA { Name = "tie-mass", Kind = "tie", Mass = 2233759.25f }, "tie-mass", 8),
// R-C fixture rows: same ordinal, astral vs BMP-private-use keys (see the constants
// above). Kind "unicode" and Mass 0 so neither an unfiltered nor a mass>1 case reaches them.
Row(registry, new ParityLeafA { Name = "bmp", Kind = "unicode", Mass = 0 }, BmpPrivateUseKey, 9),
Row(registry, new ParityLeafA { Name = "astral", Kind = "unicode", Mass = 0 }, AstralKey, 9),
};
return (registry, baseRows.Concat(extraRows).ToArray());
}
// The same six selections packages/cultnet-rs/tests/selection.rs's
// write_selection_vectors_for_the_reference builds against its own fixture rows (which
// carry the same schema names, record keys, ordinals and declared values as BuildFixture
@ -81,6 +117,36 @@ namespace GameCult.Networking.Tests
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "shared" } } }
});
// R-D: the row's canonical rendering of 2233759.25f is its exact decimal expansion, not a
// shortest round-trip form that could round the tie the other way - ge against the exact
// value must match.
yield return ("float_tie_exact_ge", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "mass", Op = "ge", Number = "2233759.25" } }
});
// R-E: cites.target.schemaId goes through the alias matcher, so "leaf_a" (ParityLeafA's
// SchemaName) matches a-eq's real content-hash SchemaId, same as `schemas` already does.
yield return ("cites_target_by_alias", new CultNetSelection
{
Cites = new CultNetCitation { Target = new CultNetRecordRef { SchemaId = "leaf_a", RecordKey = "a-eq" } }
});
// R-C: BmpPrivateUseKey (U+E000) sorts before AstralKey (U+1F602) in code-point order,
// despite AstralKey's leading UTF-16 code unit (0xD83D) being numerically smaller.
yield return ("astral_key_code_point_order", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "unicode" } } }
});
// R-I: page bytes under each projection, over the same match set.
yield return ("header_projection", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "unicode" } } },
Projection = CultNetSelectionProjections.Header
});
yield return ("document_projection", new CultNetSelection
{
Fields = new[] { new CultNetFieldPredicate { Index = "kind", Op = "any_of", Values = new[] { "unicode" } } },
Projection = CultNetSelectionProjections.Document
});
}
// Self's ruling, 2026-09-22 (commit 2 fix batch): door-refusal vectors. Never evaluated on
@ -157,7 +223,7 @@ namespace GameCult.Networking.Tests
if (Environment.GetEnvironmentVariable("CULTNET_WRITE_VECTORS") != "1")
Assert.Ignore("Set CULTNET_WRITE_VECTORS=1 to (re)write the committed vector file.");
var (registry, rows) = BuildFixture();
var (registry, rows) = BuildExtendedFixture();
var vectors = new List<object>();
foreach (var (name, selection) in Cases())
{