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>
This commit is contained in:
parent
3317dddf46
commit
defb9a110f
2 changed files with 404 additions and 154 deletions
|
|
@ -5,7 +5,7 @@
|
|||
"selectionMessagePackBase64": "g6ZmaWVsZHOSg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpndlYXBvboOlaW5kZXikbWFzc6JvcKJnZaZudW1iZXKhNapwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-eq"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
|
|
@ -16,11 +16,11 @@
|
|||
"selectionMessagePackBase64": "g6pwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nw6VsaW1pdAM=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:2cefd212c0eaffa1a9fbd68233146958cde9bf6e906fc9a549f968cd3d1f7646.v1/citer-1",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/shared-3",
|
||||
"sha256:0ad8269a37926e20e76a6c70ea86f052ee4ae90bcf2665546b8b008ec5c8b144.v1/shared-2"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/😂-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
|
||||
],
|
||||
"matched": 7,
|
||||
"matched": 10,
|
||||
"hasNext": true,
|
||||
"expectedEdges": []
|
||||
},
|
||||
|
|
@ -29,42 +29,46 @@
|
|||
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRtYXNzom9womd0pm51bWJlcqExqnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfC",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-lo",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-eq",
|
||||
"sha256:0ad8269a37926e20e76a6c70ea86f052ee4ae90bcf2665546b8b008ec5c8b144.v1/b-hi"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/b-hi",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
|
||||
],
|
||||
"matched": 3,
|
||||
"matched": 4,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "incoming_exists_true",
|
||||
"selectionMessagePackBase64": "hKdzY2hlbWFzkdlKc2hhMjU2OmI1MzJjZjAxMWQ2NjVhNDM0NDZkNDA4M2E5NTE0NDg3NzY4NjY5MjgyM2E1NWE0YzgyYjYyMDM4YTdiZWEwZTIudjGlY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"selectionMessagePackBase64": "hKdzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmalY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8OqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-eq"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": [
|
||||
{
|
||||
"fromId": "sha256:2cefd212c0eaffa1a9fbd68233146958cde9bf6e906fc9a549f968cd3d1f7646.v1/citer-1",
|
||||
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
|
||||
"role": "Design",
|
||||
"toId": "sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-eq",
|
||||
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"payloadBase64": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "incoming_negation",
|
||||
"selectionMessagePackBase64": "hKdzY2hlbWFzkdlKc2hhMjU2OmI1MzJjZjAxMWQ2NjVhNDM0NDZkNDA4M2E5NTE0NDg3NzY4NjY5MjgyM2E1NWE0YzgyYjYyMDM4YTdiZWEwZTIudjGlY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"selectionMessagePackBase64": "hKdzY2hlbWFzkdlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmalY2l0ZWSCpHJvbGWmRGVzaWdupmV4aXN0c8KqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-lo",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/shared-1",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/shared-3"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/😂-row"
|
||||
],
|
||||
"matched": 3,
|
||||
"matched": 6,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
|
|
@ -73,9 +77,9 @@
|
|||
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORpnNoYXJlZKpwcm9qZWN0aW9upmhlYWRlcqpkZXNjZW5kaW5nwg==",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/shared-1",
|
||||
"sha256:0ad8269a37926e20e76a6c70ea86f052ee4ae90bcf2665546b8b008ec5c8b144.v1/shared-2",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/shared-3"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
|
||||
"sha256:b1f2407d0227a79dcbffdf7ea317f827eec64be38a3f61b7d4e99e81e3b24b42/shared-2",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3"
|
||||
],
|
||||
"matched": 3,
|
||||
"hasNext": false,
|
||||
|
|
@ -83,51 +87,86 @@
|
|||
},
|
||||
{
|
||||
"name": "hop_by_role_cites_exact_id",
|
||||
"selectionMessagePackBase64": "g6VjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlKc2hhMjU2OmI1MzJjZjAxMWQ2NjVhNDM0NDZkNDA4M2E5NTE0NDg3NzY4NjY5MjgyM2E1NWE0YzgyYjYyMDM4YTdiZWEwZTIudjGpcmVjb3JkS2V5pGEtZXGkcm9sZaZEZXNpZ26qcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"selectionMessagePackBase64": "g6VjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlHc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmapcmVjb3JkS2V5pGEtZXGkcm9sZaZEZXNpZ26qcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:2cefd212c0eaffa1a9fbd68233146958cde9bf6e906fc9a549f968cd3d1f7646.v1/citer-1"
|
||||
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": [
|
||||
{
|
||||
"fromId": "sha256:2cefd212c0eaffa1a9fbd68233146958cde9bf6e906fc9a549f968cd3d1f7646.v1/citer-1",
|
||||
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
|
||||
"role": "Design",
|
||||
"toId": "sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-eq",
|
||||
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"payloadBase64": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hop_by_role_cites_alias_target",
|
||||
"selectionMessagePackBase64": "g6VjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlHc2hhMjU2OmI1MzJjZjAxMWQ2NjVhNDM0NDZkNDA4M2E5NTE0NDg3NzY4NjY5MjgyM2E1NWE0YzgyYjYyMDM4YTdiZWEwZTKpcmVjb3JkS2V5pGEtZXGkcm9sZaZEZXNpZ26qcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"name": "hop_by_role_cites_hash_alias",
|
||||
"selectionMessagePackBase64": "g6VjaXRlc4KmdGFyZ2V0gqhzY2hlbWFJZNlKc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmYudjGpcmVjb3JkS2V5pGEtZXGkcm9sZaZEZXNpZ26qcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:2cefd212c0eaffa1a9fbd68233146958cde9bf6e906fc9a549f968cd3d1f7646.v1/citer-1"
|
||||
"sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": [
|
||||
{
|
||||
"fromId": "sha256:2cefd212c0eaffa1a9fbd68233146958cde9bf6e906fc9a549f968cd3d1f7646.v1/citer-1",
|
||||
"fromId": "sha256:9d0bc9aebc3619b07aabf564f98667afde41444c1b866661d777687d443608dc/citer-1",
|
||||
"role": "Design",
|
||||
"toId": "sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-eq",
|
||||
"toId": "sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"payloadBase64": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "schemas_by_alias",
|
||||
"selectionMessagePackBase64": "g6dzY2hlbWFzkdlHc2hhMjU2OmI1MzJjZjAxMWQ2NjVhNDM0NDZkNDA4M2E5NTE0NDg3NzY4NjY5MjgyM2E1NWE0YzgyYjYyMDM4YTdiZWEwZTKqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"name": "schemas_by_hash_alias",
|
||||
"selectionMessagePackBase64": "g6dzY2hlbWFzkdlKc2hhMjU2OmZmMzVmM2Q5ZDZkZTNmMTA2NmI5MzY5ZTczZmU2Yzc2NDhlODgzNjhkMThlZmMyMjc1Yzk5NDczMGNkODRjZmYudjGqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-lo",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/a-eq",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/shared-1",
|
||||
"sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1/shared-3"
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-lo",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/a-eq",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-1",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/shared-3",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/😂-row"
|
||||
],
|
||||
"matched": 4,
|
||||
"matched": 7,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "schemas_by_name_alias",
|
||||
"selectionMessagePackBase64": "g6dzY2hlbWFzkalsZWFmX2EudjmqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [],
|
||||
"matched": 0,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "float_tie_exact_ge",
|
||||
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRtYXNzom9womdlpm51bWJlcqoyMjMzNzU5LjI1qnByb2plY3Rpb26maGVhZGVyqmRlc2NlbmRpbmfC",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/tie-mass"
|
||||
],
|
||||
"matched": 1,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
{
|
||||
"name": "astral_key_code_point_order",
|
||||
"selectionMessagePackBase64": "g6ZmaWVsZHORg6VpbmRleKRraW5kom9wpmFueV9vZqZ2YWx1ZXORp3VuaWNvZGWqcHJvamVjdGlvbqZoZWFkZXKqZGVzY2VuZGluZ8I=",
|
||||
"asOf": 1,
|
||||
"expectedIds": [
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/-row",
|
||||
"sha256:ff35f3d9d6de3f1066b9369e73fe6c7648e88368d18efc2275c994730cd84cff/😂-row"
|
||||
],
|
||||
"matched": 2,
|
||||
"hasNext": false,
|
||||
"expectedEdges": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,19 +26,139 @@ use cultnet_rs::{
|
|||
// "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
|
||||
// invisible. These are real SHA-256-shaped ids (`sha256:<hex>`), the same shape
|
||||
// `CultCache.cs`'s `Sha256(semanticFingerprint)` produces, each carrying a `.v1` version marker so
|
||||
// `schema_alias::matches` has a real `.v<digits>` suffix to strip - `LEAF_A_ALIAS` is the same
|
||||
// hash with no version marker, a realistic "cite this schema, any version" reference that only
|
||||
// resolves to `LEAF_A` through the alias matcher (R-E), never through exact equality.
|
||||
// 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.
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
const LEAF_A: &str = "sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2.v1";
|
||||
const LEAF_A_ALIAS: &str = "sha256:b532cf011d665a43446d4083a95144877686692823a55a4c82b62038a7bea0e2";
|
||||
const LEAF_B: &str = "sha256:0ad8269a37926e20e76a6c70ea86f052ee4ae90bcf2665546b8b008ec5c8b144.v1";
|
||||
const CITER: &str = "sha256:2cefd212c0eaffa1a9fbd68233146958cde9bf6e906fc9a549f968cd3d1f7646.v1";
|
||||
// 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.
|
||||
const NARROW_CITER: &str = "sha256:356840f80f14bda0186a33eb7aed41c1e644fd4ffc3b925ad0d7f10fe77b52cb.v1";
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct FixtureSchemaDef {
|
||||
#[serde(rename = "schemaId")]
|
||||
schema_id: String,
|
||||
#[serde(rename = "nameAlias")]
|
||||
name_alias: String,
|
||||
#[serde(rename = "hashAlias")]
|
||||
hash_alias: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct FixtureReferenceDef {
|
||||
role: String,
|
||||
#[serde(rename = "targetSchema")]
|
||||
target_schema: 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();
|
||||
|
||||
fn fixture() -> &'static FixtureFile {
|
||||
FIXTURE.get_or_init(|| {
|
||||
let path = contracts_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)
|
||||
.unwrap_or_else(|error| panic!("{} is not valid fixture JSON: {error}", path.display()))
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// The alias form cultnet_rs::selection::schema_alias resolves (a synthetic ".v1" appended to the
|
||||
// real hash - see the module comment above).
|
||||
fn leaf_a_hash_alias() -> &'static str {
|
||||
&fixture_schema("leaf_a").hash_alias
|
||||
}
|
||||
// The alias form only CultNetSchemaAliasMatching's descriptor overload resolves ("leaf_a.v9") -
|
||||
// this crate's schema_alias never sees a descriptor's human name, so a vector built with this
|
||||
// alias is expected to fail here even though the C# reference resolves it for real (the report's
|
||||
// confirmed defect).
|
||||
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" => {
|
||||
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)
|
||||
})
|
||||
.collect();
|
||||
FixtureRow::citer(leak(row.key.clone()), row.ordinal, references)
|
||||
}
|
||||
other => panic!("selection-vectors.fixture.json: unknown schema '{other}'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FixtureRow {
|
||||
schema_id: &'static str,
|
||||
|
|
@ -63,7 +183,7 @@ impl FixtureRow {
|
|||
|
||||
fn citer(record_key: &'static str, ordinal: i64, references: Vec<(String, RecordRef, Option<Vec<u8>>)>) -> Self {
|
||||
Self {
|
||||
schema_id: CITER,
|
||||
schema_id: citer(),
|
||||
record_key,
|
||||
ordinal,
|
||||
kind: None,
|
||||
|
|
@ -106,39 +226,42 @@ struct FixtureRowSet;
|
|||
impl RowSet for FixtureRowSet {
|
||||
fn all_schema_ids(&self) -> Vec<String> {
|
||||
vec![
|
||||
LEAF_A.into(),
|
||||
LEAF_B.into(),
|
||||
CITER.into(),
|
||||
leaf_a().into(),
|
||||
leaf_b().into(),
|
||||
citer().into(),
|
||||
NARROW_CITER.into(),
|
||||
]
|
||||
}
|
||||
fn declared_indexes(&self, schema_id: &str) -> Vec<String> {
|
||||
match schema_id {
|
||||
LEAF_A | LEAF_B => vec!["kind".into(), "mass".into()],
|
||||
_ => Vec::new(),
|
||||
if schema_id == leaf_a() || schema_id == leaf_b() {
|
||||
vec!["kind".into(), "mass".into()]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
fn declared_roles(&self, schema_id: &str) -> Vec<String> {
|
||||
match schema_id {
|
||||
CITER => vec![
|
||||
if schema_id == citer() {
|
||||
vec![
|
||||
"parent".into(),
|
||||
"related".into(),
|
||||
"components".into(),
|
||||
"Design".into(),
|
||||
],
|
||||
NARROW_CITER => vec!["narrow_ref".into()],
|
||||
_ => Vec::new(),
|
||||
]
|
||||
} else if schema_id == NARROW_CITER {
|
||||
vec!["narrow_ref".into()]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
fn is_numeric(&self, schema_id: &str, index: &str) -> bool {
|
||||
matches!(schema_id, LEAF_A | LEAF_B) && index == "mass"
|
||||
(schema_id == leaf_a() || schema_id == leaf_b()) && index == "mass"
|
||||
}
|
||||
fn target_leaves(&self, role: &str) -> Vec<String> {
|
||||
match role {
|
||||
"parent" | "Design" | "related" | "components" => {
|
||||
vec![LEAF_A.into(), LEAF_B.into()]
|
||||
vec![leaf_a().into(), leaf_b().into()]
|
||||
}
|
||||
"narrow_ref" => vec![LEAF_A.into()],
|
||||
"narrow_ref" => vec![leaf_a().into()],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -148,24 +271,17 @@ fn rr(schema_id: &str, record_key: &str) -> RecordRef {
|
|||
RecordRef::new(schema_id, record_key)
|
||||
}
|
||||
|
||||
// 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.
|
||||
fn base_rows() -> Vec<FixtureRow> {
|
||||
vec![
|
||||
FixtureRow::leaf(LEAF_A, "a-lo", 1, "weapon", "4"),
|
||||
FixtureRow::leaf(LEAF_A, "a-eq", 2, "weapon", "5"),
|
||||
FixtureRow::leaf(LEAF_B, "b-hi", 3, "armor", "6"),
|
||||
FixtureRow::leaf(LEAF_A, "shared-1", 4, "shared", "1"),
|
||||
FixtureRow::leaf(LEAF_B, "shared-2", 5, "shared", "1"),
|
||||
FixtureRow::leaf(LEAF_A, "shared-3", 6, "shared", "1"),
|
||||
FixtureRow::citer(
|
||||
"citer-1",
|
||||
7,
|
||||
vec![(
|
||||
"Design".to_string(),
|
||||
rr(LEAF_A, "a-eq"),
|
||||
None,
|
||||
)],
|
||||
),
|
||||
]
|
||||
fixture().rows.iter().take(7).map(build_row).collect()
|
||||
}
|
||||
|
||||
fn all_fixture_rows() -> Vec<FixtureRow> {
|
||||
fixture().rows.iter().map(build_row).collect()
|
||||
}
|
||||
|
||||
// S2: fields conjoin over declared indexes.
|
||||
|
|
@ -277,7 +393,7 @@ fn hops_one_edge_by_declared_role() {
|
|||
let rows = base_rows();
|
||||
let selection = Selection {
|
||||
cites: Some(Citation {
|
||||
target: rr(LEAF_A, "a-eq"),
|
||||
target: rr(leaf_a(), "a-eq"),
|
||||
role: Some("Design".into()),
|
||||
}),
|
||||
..Selection::default()
|
||||
|
|
@ -295,19 +411,19 @@ fn hops_one_edge_by_declared_role() {
|
|||
#[test]
|
||||
fn cites_role_excludes_a_second_reference_at_the_same_target() {
|
||||
let rows = vec![
|
||||
FixtureRow::leaf(LEAF_A, "a-eq", 1, "weapon", "5"),
|
||||
FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
|
||||
FixtureRow::citer(
|
||||
"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(), rr(leaf_a(), "a-eq"), None),
|
||||
("OtherRole".to_string(), rr(leaf_a(), "a-eq"), None),
|
||||
],
|
||||
),
|
||||
];
|
||||
let selection = Selection {
|
||||
cites: Some(Citation {
|
||||
target: rr(LEAF_A, "a-eq"),
|
||||
target: rr(leaf_a(), "a-eq"),
|
||||
role: Some("Design".into()),
|
||||
}),
|
||||
..Selection::default()
|
||||
|
|
@ -325,7 +441,7 @@ fn cited_exists_is_the_one_negation() {
|
|||
&FixtureRowSet,
|
||||
&rows,
|
||||
&Selection {
|
||||
schemas: Some(vec![LEAF_A.into()]),
|
||||
schemas: Some(vec![leaf_a().into()]),
|
||||
cited: Some(Incoming { role: "Design".into(), exists: true }),
|
||||
..Selection::default()
|
||||
},
|
||||
|
|
@ -338,7 +454,7 @@ fn cited_exists_is_the_one_negation() {
|
|||
&FixtureRowSet,
|
||||
&rows,
|
||||
&Selection {
|
||||
schemas: Some(vec![LEAF_A.into()]),
|
||||
schemas: Some(vec![leaf_a().into()]),
|
||||
cited: Some(Incoming { role: "Design".into(), exists: false }),
|
||||
..Selection::default()
|
||||
},
|
||||
|
|
@ -355,9 +471,9 @@ fn cited_exists_is_the_one_negation() {
|
|||
#[test]
|
||||
fn compares_numbers_at_the_boundary_for_all_four_operators() {
|
||||
let rows = vec![
|
||||
FixtureRow::leaf(LEAF_A, "lo", 1, "k", "4"),
|
||||
FixtureRow::leaf(LEAF_A, "eq", 2, "k", "5"),
|
||||
FixtureRow::leaf(LEAF_A, "hi", 3, "k", "6"),
|
||||
FixtureRow::leaf(leaf_a(), "lo", 1, "k", "4"),
|
||||
FixtureRow::leaf(leaf_a(), "eq", 2, "k", "5"),
|
||||
FixtureRow::leaf(leaf_a(), "hi", 3, "k", "6"),
|
||||
];
|
||||
let matches_for = |op: &str| {
|
||||
let selection = Selection {
|
||||
|
|
@ -388,14 +504,14 @@ fn compares_numbers_at_the_boundary_for_all_four_operators() {
|
|||
#[test]
|
||||
fn refuses_an_edge_outside_its_declared_target() {
|
||||
let rows = vec![
|
||||
FixtureRow::leaf(LEAF_B, "b", 1, "k", "1"),
|
||||
FixtureRow::leaf(leaf_b(), "b", 1, "k", "1"),
|
||||
FixtureRow {
|
||||
schema_id: NARROW_CITER,
|
||||
record_key: "bad",
|
||||
ordinal: 2,
|
||||
kind: None,
|
||||
mass: None,
|
||||
references: vec![("narrow_ref".to_string(), rr(LEAF_B, "b"), None)],
|
||||
references: vec![("narrow_ref".to_string(), rr(leaf_b(), "b"), None)],
|
||||
},
|
||||
];
|
||||
let selection = Selection {
|
||||
|
|
@ -415,8 +531,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(), rr(leaf_a(), "part-1"), Some(vec![1, 5])),
|
||||
("components".to_string(), rr(leaf_a(), "part-2"), Some(vec![2, 5])),
|
||||
],
|
||||
);
|
||||
let payloads: std::collections::HashSet<Vec<u8>> =
|
||||
|
|
@ -434,11 +550,11 @@ fn dictionary_reference_entries_keep_distinct_payload_bytes() {
|
|||
#[test]
|
||||
fn cited_selections_page_the_citee_and_their_edges_survive_the_page_filter() {
|
||||
let rows = vec![
|
||||
FixtureRow::leaf(LEAF_A, "part-1", 1, "k", "1"),
|
||||
FixtureRow::leaf(leaf_a(), "part-1", 1, "k", "1"),
|
||||
FixtureRow::citer(
|
||||
"assembly",
|
||||
2,
|
||||
vec![("components".to_string(), rr(LEAF_A, "part-1"), Some(vec![9]))],
|
||||
vec![("components".to_string(), rr(leaf_a(), "part-1"), Some(vec![9]))],
|
||||
),
|
||||
];
|
||||
let selection = Selection {
|
||||
|
|
@ -459,11 +575,11 @@ fn cited_selections_page_the_citee_and_their_edges_survive_the_page_filter() {
|
|||
#[test]
|
||||
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::leaf(leaf_a(), "a-eq", 1, "weapon", "5"),
|
||||
FixtureRow::citer("citer-1", 2, vec![("Design".to_string(), rr(leaf_a(), "a-eq"), None)]),
|
||||
];
|
||||
let selection = Selection {
|
||||
cites: Some(Citation { target: rr(LEAF_A, "a-eq"), role: Some("Design".into()) }),
|
||||
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
|
||||
..Selection::default()
|
||||
};
|
||||
let evaluation = select(&FixtureRowSet, &rows, &selection, 1).unwrap();
|
||||
|
|
@ -549,7 +665,7 @@ fn validation_refuses_undeclared_index_role_and_empty_lists() {
|
|||
#[test]
|
||||
fn validation_refuses_a_blank_entry_in_schemas_or_keys() {
|
||||
let blank_schema = Selection {
|
||||
schemas: Some(vec![LEAF_A.into(), " ".into()]),
|
||||
schemas: Some(vec![leaf_a().into(), " ".into()]),
|
||||
..Selection::default()
|
||||
};
|
||||
assert_eq!(validate(&blank_schema, &FixtureRowSet).unwrap_err().field, "schemas");
|
||||
|
|
@ -604,7 +720,7 @@ fn selection_is_never_untagged() {
|
|||
#[test]
|
||||
fn selection_round_trips_through_named_messagepack() {
|
||||
let selection = Selection {
|
||||
schemas: Some(vec![LEAF_A.into(), LEAF_B.into()]),
|
||||
schemas: Some(vec![leaf_a().into(), leaf_b().into()]),
|
||||
keys: Some(vec!["k1".into()]),
|
||||
fields: Some(vec![
|
||||
FieldPredicate {
|
||||
|
|
@ -620,7 +736,7 @@ fn selection_round_trips_through_named_messagepack() {
|
|||
number: Some("5".into()),
|
||||
},
|
||||
]),
|
||||
cites: Some(Citation { target: rr(LEAF_A, "a-eq"), role: Some("Design".into()) }),
|
||||
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
|
||||
cited: Some(Incoming { role: "Design".into(), exists: true }),
|
||||
projection: "document".into(),
|
||||
descending: true,
|
||||
|
|
@ -691,16 +807,23 @@ fn contracts_dir() -> std::path::PathBuf {
|
|||
.join("interop")
|
||||
}
|
||||
|
||||
fn evaluate_vector(vector: &Vector) -> (Vec<String>, u32, bool, Vec<VectorEdge>) {
|
||||
// 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> {
|
||||
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 = base_rows();
|
||||
let rows = all_fixture_rows();
|
||||
let evaluation = select(&FixtureRowSet, &rows, &selection, vector.as_of)
|
||||
.expect("this campaign's vectors are all valid selections");
|
||||
.map_err(|refusal| format!("select() refused: {refusal:?}"))?;
|
||||
let ids: Vec<String> = evaluation
|
||||
.rows
|
||||
.iter()
|
||||
|
|
@ -723,7 +846,7 @@ fn evaluate_vector(vector: &Vector) -> (Vec<String>, u32, bool, Vec<VectorEdge>)
|
|||
.map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes)),
|
||||
})
|
||||
.collect();
|
||||
(ids, matched, has_next, edges)
|
||||
Ok((ids, matched, has_next, edges))
|
||||
}
|
||||
|
||||
// Self's ruling, 2026-09-22: a door-refusal vector decodes its (necessarily malformed, by the
|
||||
|
|
@ -754,32 +877,75 @@ fn selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in
|
|||
};
|
||||
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");
|
||||
|
||||
// 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();
|
||||
for vector in &file.vectors {
|
||||
if let Some(expected_field) = &vector.expected_refusal_field {
|
||||
assert_refusal_vector(vector, expected_field);
|
||||
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));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (ids, matched, has_next, edges) = evaluate_vector(vector);
|
||||
assert_eq!(ids, vector.expected_ids, "vector {:?}: row ids", vector.name);
|
||||
assert_eq!(matched, vector.matched, "vector {:?}: matched", vector.name);
|
||||
assert_eq!(has_next, vector.has_next, "vector {:?}: hasNext", vector.name);
|
||||
assert_eq!(
|
||||
edges.len(),
|
||||
vector.expected_edges.len(),
|
||||
"vector {:?}: edge count",
|
||||
vector.name
|
||||
);
|
||||
for (actual, expected) in edges.iter().zip(vector.expected_edges.iter()) {
|
||||
assert_eq!(actual.from_id, expected.from_id, "vector {:?}: edge.from", vector.name);
|
||||
assert_eq!(actual.role, expected.role, "vector {:?}: edge.role", vector.name);
|
||||
assert_eq!(actual.to_id, expected.to_id, "vector {:?}: edge.to", vector.name);
|
||||
assert_eq!(
|
||||
actual.payload_base64, expected.payload_base64,
|
||||
"vector {:?}: edge.payload",
|
||||
vector.name
|
||||
);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"{} of {} vectors disagreed with the reference:\n{}",
|
||||
failures.len(),
|
||||
file.vectors.len(),
|
||||
failures.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
/// The Rust->C# mirror: builds `selection-vectors.rs-written.json` from this binary's own
|
||||
|
|
@ -838,7 +1004,7 @@ fn write_selection_vectors_for_the_reference() {
|
|||
(
|
||||
"incoming_exists_true",
|
||||
Selection {
|
||||
schemas: Some(vec![LEAF_A.into()]),
|
||||
schemas: Some(vec![leaf_a().into()]),
|
||||
cited: Some(Incoming { role: "Design".into(), exists: true }),
|
||||
..Selection::default()
|
||||
},
|
||||
|
|
@ -846,7 +1012,7 @@ fn write_selection_vectors_for_the_reference() {
|
|||
(
|
||||
"incoming_negation",
|
||||
Selection {
|
||||
schemas: Some(vec![LEAF_A.into()]),
|
||||
schemas: Some(vec![leaf_a().into()]),
|
||||
cited: Some(Incoming { role: "Design".into(), exists: false }),
|
||||
..Selection::default()
|
||||
},
|
||||
|
|
@ -869,25 +1035,70 @@ fn write_selection_vectors_for_the_reference() {
|
|||
(
|
||||
"hop_by_role_cites_exact_id",
|
||||
Selection {
|
||||
cites: Some(Citation { target: rr(LEAF_A, "a-eq"), role: Some("Design".into()) }),
|
||||
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
|
||||
..Selection::default()
|
||||
},
|
||||
),
|
||||
// R-E: the same citation, but the target is named by its unversioned alias
|
||||
// (`LEAF_A_ALIAS`, the bare hash with no `.v1`) rather than the exact id - only the alias
|
||||
// matcher bridges this to the real, versioned schema id.
|
||||
// R-E: the same citation, but the target is named by this crate's own resolvable alias
|
||||
// (leaf_a_hash_alias(): the real hash with a synthetic ".v1" appended) rather than the exact
|
||||
// id - schema_alias strips the trailing ".v1" from either side and compares the bare hash.
|
||||
(
|
||||
"hop_by_role_cites_alias_target",
|
||||
"hop_by_role_cites_hash_alias",
|
||||
Selection {
|
||||
cites: Some(Citation { target: rr(LEAF_A_ALIAS, "a-eq"), role: Some("Design".into()) }),
|
||||
cites: Some(Citation { target: rr(leaf_a_hash_alias(), "a-eq"), role: Some("Design".into()) }),
|
||||
..Selection::default()
|
||||
},
|
||||
),
|
||||
// R-E: `schemas` reaches a schema by its unversioned alias.
|
||||
// R-E: `schemas` reaches a schema by the same hash alias.
|
||||
(
|
||||
"schemas_by_alias",
|
||||
"schemas_by_hash_alias",
|
||||
Selection {
|
||||
schemas: Some(vec![LEAF_A_ALIAS.into()]),
|
||||
schemas: Some(vec![leaf_a_hash_alias().into()]),
|
||||
..Selection::default()
|
||||
},
|
||||
),
|
||||
// R-E, the reverse probe of SelectionParityVectorTests.Cases's schemas_by_name_alias:
|
||||
// schema_alias has no concept of a schema's human name, only its hash id
|
||||
// (Row::schema_id()), so "leaf_a.v9" resolves to nothing here and this vector's
|
||||
// expected_ids come out empty even though CultNetSchemaAliasMatching's descriptor overload
|
||||
// (the only overload any C# call site uses) really does resolve it - it strips the ".v9" and
|
||||
// compares "leaf_a" to ParityLeafA's descriptor.SchemaName. The C# reader
|
||||
// (SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference) therefore
|
||||
// disagrees with this file's empty expected_ids: confirmed cross-runtime defect: see the
|
||||
// report.
|
||||
(
|
||||
"schemas_by_name_alias",
|
||||
Selection {
|
||||
schemas: Some(vec![leaf_a_name_alias().into()]),
|
||||
..Selection::default()
|
||||
},
|
||||
),
|
||||
// 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.
|
||||
(
|
||||
"float_tie_exact_ge",
|
||||
Selection {
|
||||
fields: Some(vec![FieldPredicate {
|
||||
index: "mass".into(),
|
||||
op: "ge".into(),
|
||||
values: None,
|
||||
number: Some("2233759.25".into()),
|
||||
}]),
|
||||
..Selection::default()
|
||||
},
|
||||
),
|
||||
// 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.
|
||||
(
|
||||
"astral_key_code_point_order",
|
||||
Selection {
|
||||
fields: Some(vec![FieldPredicate {
|
||||
index: "kind".into(),
|
||||
op: "any_of".into(),
|
||||
values: Some(vec!["unicode".into()]),
|
||||
number: None,
|
||||
}]),
|
||||
..Selection::default()
|
||||
},
|
||||
),
|
||||
|
|
@ -896,7 +1107,7 @@ fn write_selection_vectors_for_the_reference() {
|
|||
let mut vectors = Vec::new();
|
||||
for (name, selection) in cases {
|
||||
let bytes = rmp_serde::to_vec_named(&selection).expect("encodes");
|
||||
let rows = base_rows();
|
||||
let rows = all_fixture_rows();
|
||||
let evaluation = select(&FixtureRowSet, &rows, &selection, 1).expect("valid selection");
|
||||
let ids: Vec<String> = evaluation
|
||||
.rows
|
||||
|
|
@ -1028,7 +1239,7 @@ fn select_refuses_an_invalid_selection_instead_of_evaluating_it() {
|
|||
// 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() {
|
||||
let row = FixtureRow::leaf(LEAF_A, "a-eq", 1, "weapon", "5");
|
||||
let row = FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5");
|
||||
let selection = Selection {
|
||||
cited: Some(Incoming { role: "Design".into(), exists: true }),
|
||||
..Selection::default()
|
||||
|
|
@ -1085,7 +1296,7 @@ fn cursor_digest_does_not_collide_on_differently_split_lists() {
|
|||
fn schemas_reaches_a_schema_by_its_unversioned_alias() {
|
||||
let rows = base_rows();
|
||||
let selection = Selection {
|
||||
schemas: Some(vec![LEAF_A_ALIAS.into()]),
|
||||
schemas: Some(vec![leaf_a_hash_alias().into()]),
|
||||
..Selection::default()
|
||||
};
|
||||
let evaluation = select(&FixtureRowSet, &rows, &selection, 1).unwrap();
|
||||
|
|
@ -1103,7 +1314,7 @@ fn schemas_reaches_a_schema_by_its_unversioned_alias() {
|
|||
// 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() {
|
||||
let row = FixtureRow::leaf(LEAF_A, "a-eq", 1, "weapon", "5");
|
||||
let row = FixtureRow::leaf(leaf_a(), "a-eq", 1, "weapon", "5");
|
||||
let selection = Selection { schemas: Some(Vec::new()), ..Selection::default() };
|
||||
assert!(
|
||||
!cultnet_rs::matches(&row, &selection),
|
||||
|
|
@ -1115,7 +1326,7 @@ fn matches_treats_an_empty_but_present_schemas_list_as_matching_nothing() {
|
|||
fn cites_target_resolves_through_the_alias_matcher() {
|
||||
let rows = base_rows();
|
||||
let selection = Selection {
|
||||
cites: Some(Citation { target: rr(LEAF_A_ALIAS, "a-eq"), role: Some("Design".into()) }),
|
||||
cites: Some(Citation { target: rr(leaf_a_hash_alias(), "a-eq"), role: Some("Design".into()) }),
|
||||
..Selection::default()
|
||||
};
|
||||
let evaluation = select(&FixtureRowSet, &rows, &selection, 1).unwrap();
|
||||
|
|
@ -1137,8 +1348,8 @@ fn validate_refuses_a_cites_target_schema_that_matches_no_known_schema() {
|
|||
|
||||
#[test]
|
||||
fn schema_alias_module_is_reachable_from_the_crate_root() {
|
||||
assert!(schema_alias::matches(LEAF_A_ALIAS, LEAF_A));
|
||||
assert!(!schema_alias::matches(LEAF_A_ALIAS, LEAF_B));
|
||||
assert!(schema_alias::matches(leaf_a_hash_alias(), leaf_a()));
|
||||
assert!(!schema_alias::matches(leaf_a_hash_alias(), leaf_b()));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
|
@ -1179,11 +1390,11 @@ fn document_record_for(row: &FixtureRow) -> SelectionDocumentRecord {
|
|||
#[test]
|
||||
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::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]))]),
|
||||
];
|
||||
let selection = Selection {
|
||||
cites: Some(Citation { target: rr(LEAF_A, "a-eq"), role: Some("Design".into()) }),
|
||||
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
|
||||
projection: "header".into(),
|
||||
..Selection::default()
|
||||
};
|
||||
|
|
@ -1202,11 +1413,11 @@ fn select_page_header_projection_carries_no_payload_in_rows_or_edges() {
|
|||
#[test]
|
||||
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::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]))]),
|
||||
];
|
||||
let selection = Selection {
|
||||
cites: Some(Citation { target: rr(LEAF_A, "a-eq"), role: Some("Design".into()) }),
|
||||
cites: Some(Citation { target: rr(leaf_a(), "a-eq"), role: Some("Design".into()) }),
|
||||
projection: "document".into(),
|
||||
..Selection::default()
|
||||
};
|
||||
|
|
@ -1251,7 +1462,7 @@ fn snapshot_response_raw_v1_round_trips_headers_documents_and_edges_through_the_
|
|||
// the two.
|
||||
headers: None,
|
||||
documents: Some(vec![SelectionDocumentRecord {
|
||||
schema_id: LEAF_A.to_string(),
|
||||
schema_id: leaf_a().to_string(),
|
||||
schema_name: None,
|
||||
schema_version: None,
|
||||
schema_content_hash: None,
|
||||
|
|
@ -1266,16 +1477,16 @@ fn snapshot_response_raw_v1_round_trips_headers_documents_and_edges_through_the_
|
|||
}]),
|
||||
edges: Some(vec![
|
||||
Edge {
|
||||
from: rr(CITER, "citer-1"),
|
||||
from: rr(citer(), "citer-1"),
|
||||
role: "Design".to_string(),
|
||||
to: rr(LEAF_A, "a-eq"),
|
||||
to: rr(leaf_a(), "a-eq"),
|
||||
payload_encoding: Some("messagepack".to_string()),
|
||||
payload: Some(vec![9, 9]),
|
||||
},
|
||||
Edge {
|
||||
from: rr(CITER, "citer-1"),
|
||||
from: rr(citer(), "citer-1"),
|
||||
role: "parent".to_string(),
|
||||
to: rr(LEAF_B, "b-hi"),
|
||||
to: rr(leaf_b(), "b-hi"),
|
||||
payload_encoding: None,
|
||||
payload: None,
|
||||
},
|
||||
|
|
@ -1302,7 +1513,7 @@ fn snapshot_response_raw_v1_round_trips_headers_through_the_generic_codec() {
|
|||
as_of: 7,
|
||||
next: None,
|
||||
headers: Some(vec![RawDocumentHeader {
|
||||
schema_id: LEAF_A.to_string(),
|
||||
schema_id: leaf_a().to_string(),
|
||||
schema_name: Some("leaf_a".to_string()),
|
||||
schema_version: Some("v1".to_string()),
|
||||
schema_content_hash: Some("hash".to_string()),
|
||||
|
|
|
|||
Loading…
Reference in a new issue