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>
This commit is contained in:
Meta Vens 2026-09-22 21:11:35 +02:00
commit 97bc2076b1
2 changed files with 111 additions and 1 deletions

View file

@ -0,0 +1,58 @@
{
"//": "CultNet typed selection, Cut 1 fix batch (docs/cultnet-selection-cut.md, Self's rulings for the Cut 1 fix batch, R-E and the shared-fixture ruling, 2026-09-22). The one row set both the C# reference and cultnet-rs build their parity vectors from. schemaId is each schema's real SHA-256 content-hash id, exactly as GameCult.Caching.CultDocumentRegistry computes it for the fixture's C# document types (leaf_a=ParityLeafA, leaf_b=ParityLeafB, citer=ParityCiter, tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs) - CultCache.cs's Sha256(semanticFingerprint), which carries no '.vN' suffix of its own; the version is folded into the hash, never appended as text. Both writers read this file instead of building their own fixture rows.",
"//aliases": "Two DIFFERENT alias forms, now deliberate rather than a cross-runtime defect (S-12, 2026-09-22: the Rust alias port made this so - see docs/cultnet-selection-cut.md, 'Self's ruling, 2026-09-22: the C# reference's alias rule is the rule'). Both runtimes resolve a schema alias the same one way: strip a trailing .vN and compare the remainder against the descriptor's declared SchemaName (CultNetSchemaAliasMatching.Matches(candidate, CultDocumentDescriptor), src/GameCult.Networking/CultNetDatabase.cs:74-87; cultnet_rs::selection::schema_alias::matches ports it name-for-name and deletes the hash-stripping overload it used to carry). nameAlias is <SchemaName>.v9 and resolves through both runtimes under that one rule. hashAlias is <schemaId>.v1 and is refused as unmatched by both - it is not a wire form either runtime's production call sites ever produce or accept, and it is kept in the fixture specifically to pin that neither runtime resolves it.",
"schemas": {
"leaf_a": {
"schemaId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff",
"nameAlias": "leaf_a.v9",
"hashAlias": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff.v1",
"indexes": [
{ "name": "kind", "numeric": false },
{ "name": "mass", "numeric": true }
]
},
"leaf_b": {
"schemaId": "sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42",
"nameAlias": "leaf_b.v9",
"hashAlias": "sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42.v1",
"indexes": [
{ "name": "kind", "numeric": false },
{ "name": "mass", "numeric": true }
]
},
"citer": {
"schemaId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc",
"nameAlias": "citer.v9",
"hashAlias": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc.v1",
"roles": [
{ "name": "Design", "targets": ["leaf_a", "leaf_b"] }
]
}
},
"rows": [
{ "schema": "leaf_a", "key": "a-lo", "ordinal": 1, "fields": { "kind": "weapon", "mass": "4" } },
{ "schema": "leaf_a", "key": "a-eq", "ordinal": 2, "fields": { "kind": "weapon", "mass": "5" } },
{ "schema": "leaf_b", "key": "b-hi", "ordinal": 3, "fields": { "kind": "armor", "mass": "6" } },
{ "schema": "leaf_a", "key": "shared-1", "ordinal": 4, "fields": { "kind": "shared", "mass": "1" } },
{ "schema": "leaf_b", "key": "shared-2", "ordinal": 5, "fields": { "kind": "shared", "mass": "1" } },
{ "schema": "leaf_a", "key": "shared-3", "ordinal": 6, "fields": { "kind": "shared", "mass": "1" } },
{
"schema": "citer",
"key": "citer-1",
"ordinal": 7,
"references": [ { "role": "Design", "targetSchema": "leaf_a", "targetKey": "a-eq" } ]
},
{
"//": "R-D: 2233759.25 is exactly representable as an f32 and sits on a round-to-even tie between two shortest-form spellings; its exact canonical decimal is 2233759.25 either way. kind=tie so no weapon/armor/shared case can sweep it up.",
"schema": "leaf_a", "key": "tie-mass", "ordinal": 8, "fields": { "kind": "tie", "mass": "2233759.25" }
},
{
"//": "R-C: BMP private-use key (U+E000), same ordinal as the astral row below so the tiebreak - not the ordinal - decides order. kind=unicode, mass=0 so no unfiltered or mass>1 case reaches these.",
"schema": "leaf_a", "key": "-row", "ordinal": 9, "fields": { "kind": "unicode", "mass": "0" }
},
{
"//": "R-C: astral key (U+1F602, encoded as a surrogate pair whose high half 0xD83D is less than 0xE000 - a UTF-16 code-unit compare sorts this first; code-point order sorts it after the BMP row above).",
"schema": "leaf_a", "key": "😂-row", "ordinal": 9, "fields": { "kind": "unicode", "mass": "0" }
}
]
}

View file

@ -92,9 +92,22 @@ struct FixtureFile {
static FIXTURE: std::sync::OnceLock<FixtureFile> = std::sync::OnceLock::new();
// 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")
}
fn fixture() -> &'static FixtureFile {
FIXTURE.get_or_init(|| {
let path = contracts_dir().join("selection-vectors.fixture.json");
let path = vendored_fixture_dir().join("selection-vectors.fixture.json");
let text = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("{} is missing or unreadable: {error}", path.display()));
serde_json::from_str(&text)
@ -102,6 +115,31 @@ fn fixture() -> &'static FixtureFile {
})
}
// 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"
);
}
fn fixture_schema(name: &str) -> &'static FixtureSchemaDef {
fixture()
.schemas
@ -1036,6 +1074,20 @@ fn assert_refusal_vector(vector: &Vector, expected_field: &str) {
/// 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() {
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;
}
let path = contracts_dir().join("selection-vectors.cs-written.json");
let Ok(text) = fs::read_to_string(&path) else {
panic!(