The hang was the fake server, not the writer: it is bound to one connection id, and two publishes in one test sent a second connect it rejected from inside the socket handler. The unacknowledged-send test also scripted an admit, whose reply piggybacks the put's ack.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Per Heimdall's review. The dedupe key lives for the life of its session, bounded per
session without forgetting an id the server promised to answer. A longer retention
is optional and never to be relied on. After a reconnect callers resend only writes
that are idempotent by record key and read back before resending any other.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- messageId unique per authenticated actor and session; dedupe key (actor, id) bound to SHA-256 of
[operation, schemaId, recordKey, payload]; same id with other content is message_invalid, never the old receipt.
- A late answer after the window is recorded on the write (admitted_late / refused_late); no blind resend.
- A forwarder with no primary answer relays nothing; the client reports unconfirmed.
- RECEIPT_RESULT_WINDOW_MS 5000 (default), LEGACY_RESULT_WINDOW_MS 500 (fallback only), DEDUPE_RETENTION_MS as vector constants.
- The hello capability changes framing only and never authorizes.
- Batches can commit partially; the caller owns retry.
- Vectors: constants, worked content hashes, 7 dedupe cases, late-answer and window scenarios.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Specification and shared vectors only, no runtime code. A write is answered
exactly once with cultnet.write_admitted.v0 or cultnet.error.v1 carrying its
messageId; the new frames go only to connections whose hello lists them (today's
TS decoder rejects unknown keys and versions, so "old clients ignore it" is
false). Clients settle admitted | refused | unconfirmed; the result window is the
pre-receipt fallback and ends unconfirmed. Wire vectors, schema-invalid cases and
16 replay scenarios in write-receipt-vectors.json.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Q6-A of docs/cultnet-error-contract-cut.md. message_invalid joins the closed
cultnet.error.v0 code enum (both schema copies), with details.field naming the
request field at fault ("message" when the whole frame cannot be read).
C# ForMessageInvalid, Rust CultNetErrorCode::MessageInvalid, TS
cultNetMessageInvalidError and Python message_invalid_error all write the same
captured bytes (error-vectors.json plus the C# and Rust cases). cultmesh-py and
cultmesh-ts refuse malformed puts and deletes with it; the C# subscription
servers use it for the missing subscriptionId.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ruling (steward): add the code across C#, Rust, TypeScript and Python, each with a
captured-bytes case. details = { field: "schemaVersion", value: <offered version> }, no routingHint.
- schema (both copies), C# ForUnsupportedSchemaVersion + CultNetErrorCode.UnsupportedSchemaVersion in Rust,
TS code list, Python CULTNET_ERROR_CODES and schema catalog.
- bytes: error-vectors.json gains unsupported_schema_version (C# serializer output, checked by the
C# test, the Rust error_message.rs case, and the TS/Python vector tests; the Python builder reproduces them).
- cultnet-py interop_peer and cultmesh-py server now answer with the contract-shaped error (no extra
messageId, details {field,value}) via messages.unsupported_schema_version_error.
- tests that used unsupported_schema_version as the example of an unknown code now use not_a_code.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Captured-bytes case in cultnet-rs error_message.rs, asserted byte-for-byte by
the C# schema contract test; the cultnet-distributed-database contract and the
Networking README state the remote write authority invariant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Identity: C2a's StoredSchemaId/StoredVersion owns; main's PersistedSchemaId, PersistedIdentity and both MarkWritten are deleted. Stamp rule: ReadStore judges the stamp before the body. Flush over damage refuses (merge-r1-precedence). Mesh single-file writes route through SingleFileMessagePackBackingStore. TS: store-file.ts commits, C2a's header handling kept. Rust: C2a's readable_store_format in front of the grown decoder. Build not yet verified at this commit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Written on Yggdrasil under CULTNET_WRITE_VECTORS=1. 253 existing cases keep
their verdict and bytes unchanged; 14 deep_extra_N / deep_chunkextra_N cases
are added (extra slot accepted through depth 255 / 253, refused from 256 / 254).
cs-written file is unchanged (trailing newline only, not committed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
content-acceptance-vectors.json and cdn-record-key-vectors.json are written by the C# test under
CULTNET_WRITE_VECTORS=1 (Yggdrasil, sdk:10.0); content-vectors.ts-written.json gains the whitespace-version, padded
kind and MIME vector from the TS writer. The cs-written and rs-written vectors are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One browser-safe module owns the manifest and chunk-ref shapes, their MessagePack form (positional ten-array, metadata as
ordered entries, duplicate keys and unsafe integers refused, payload depth through cultcache-ts's own checkPayloadDepth),
normalizeHash/parseSha256Hex, both record-key grammars (StableToken per UTF-16 unit), WebCrypto sha256Hex and packContent.
cultcache-ts/document re-exports checkPayloadDepth. scripts/write-cultmesh-content-vectors.mjs writes the TypeScript
manifest vectors for the C# reference to judge.
Builds. Co-Authored-By below.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
contracts/cultmesh/cdn-record-key-vectors.json is written by the reference under CULTNET_WRITE_VECTORS=1 (19 artifact
keys, 7 chunk keys; ids as UTF-16 unit arrays so a lone surrogate survives). The judge compares text with line endings
normalised so a Windows checkout and a Linux one agree.
Builds. Co-Authored-By below.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R1's rules win where both sides decided the same question: a zero-byte file is
not a store (refused as unreadable in every runtime), only nothing at the path is
empty, a dangling link is an I/O error, refusals never echo stored values and echo
a header only as cultcache.store.v<digits>, writes report Rejected, NotReplaced
and ReplacedNotDurable. C2a's rules win on what it added: the v3 element-id
header, variants, whole-file proof, typed unreadable errors in C#, TypeScript and
Python. Rust keeps R1's CultCacheStoreUnreadable{path, kind}; C2a's
StoreUnreadableError is deleted. The readability manifest's zero-byte row is
refuses x4.
Not yet built or tested: verification follows in the next commits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1: RecordUnappliedEntry keeps the primary's wire entry on the in-memory log
entry (internal Relayed) and ToLogEntryMessage returns it through the same
normalisation the store uses, so a replica with no store no longer relays a
skipped put as a delete; memory and disk relay identically.
Soul's 4d probes are committed (chained replica keeps the row, key-prefixed
reads serve only owned keys, primary-all serves everything; the two
measurements are explicit) with a memory/disk relay-parity test.
Rust: captured-bytes cases for unowned_schema and variant_unsupported (with and
without a subscription).
The error-code set is stated as a closed enum in the schema description (both
copies) and in cultnet-distributed-database.md: adding a code is one
coordinated change across runtimes, since Rust and TypeScript refuse an unknown
code. The Soul 4c probe file's header says what it guards.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Q1: HandlePutAsync/HandleDeleteAsync catch a CultNetShardLogException (or an
AggregateException holding one) before the generic catch and answer nothing: the
write committed and was published, so the peer is not told it was refused. It is
logged at warning level. The wire has no committed-with-degraded-log message.
Q2: GetAsync/GetAll/GetByName/GetByIndex, the shard-less snapshot, the selection
and the subscription snapshot serve only rows a shard owns, so read and watch
agree; the CultMesh collections follow through GetAll, and the Mesh untyped
variant fallback checks ownership. The raw cache still holds everything.
Q3: a replica log apply skips a row its shard does not own, as the snapshot path
does, and still records the entry so a chained replica sees no gap.
API: rowFilter is internal (public CreateSelectionResponse and
CreateRawSnapshotResponse signatures restored, internal overloads added);
exception tags on the write methods; Commit's unwrap rule is XML doc; the
unowned refusal answers on the wire with code unowned_schema (schema enum in both
copies, Rust CultNetErrorCode, CultNetErrorMessage.ForUnownedSchema) and put and
delete log it at warning level.
Dispose keeps journal-first; a test now fails if the observer goes first.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CultWitnessContractPin, CultWitnessArtifactEntry and CultWitnessTimingEntry get
an id slot. The JSON schema contract and the Python schema copy gain an optional
id per element, and the Python and TypeScript fixtures carry and assert ids.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[CultElementId(nameof(Member))] declares an id derived from that member's
invariant text, never random, so content-addressed elements keep encoding to the
same bytes. CultMeshCdnChunkRef derives its id from Offset; PackArtifact sets
it, the Rust pack_content writes it, and the committed content vectors are
re-cut once so both runtimes carry it. Engine tests drive Problems and Assign
directly (InternalsVisibleTo the Caching tests).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reconcile's withdrawal sent an untyped error with no subscription id and left the
peer holding the rows the subscription had delivered. It now sends removals for
those rows, then a variant_unsupported error whose details carry
field "subscriptionId" and the id (the existing details slots; no new wire key).
The subscribe and per-change paths use the same code. The error schema and the
Rust CultNetErrorCode accept variant_unsupported, so a Rust peer decodes it
instead of refusing an unknown code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A C# caller that leaves ChunkHash, MessageId or RecordKey null writes msgpack nil; the
reference reads it back as null, treats it as blank and answers found:false. Rust now
decodes nil as blank in content chunk requests, so those requests are answered with the
reference bytes instead of dropped. Three nil vectors from the reference server are
judged by decoding them in Rust. Record two content-plane limits in the parity scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A hostile manifest of empty chunks no longer becomes a request flood: fetch_content
refuses a zero-size chunk beside other chunks, and settles the content hash and every
chunk hash (normalised, non-empty) before the first request.
Every bad chunk request is now answered found:false in the reference handler's
validation order, never refused at decode. validate_message no longer refuses content
chunk requests or responses, so a blank message id or hash, a prefix-only hash and a
negative size reach answer_content_chunk_request. The answer follows the reference's
double normalisation (handler, then CreateRecordKey), which fixes the prefix-only
hash spelling to ArgumentException on 'chunkHash'. The cs vectors gain nine
reference-server answers (blank and whitespace id, blank, whitespace and prefix-only
hash, negative size, record-key case, blank record key, double prefix) and a
metadata-order manifest; Rust answers all of them byte-identically.
Manifest metadata is Vec<(String, String)> in wire order and refuses a duplicate key,
replacing the BTreeMap that re-sorted keys.
Tests pin the surviving mutants: unusable chunk sizes, the payload-length and
claimed-size halves of the chunk check, stable sort at equal offsets, the normalised
hash and canonical key actually sent, and the kind/mime defaults never trimming.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
content-vectors.rs-written.json is written by cultnet-rs and is identical
to the reference's file. transport-planes.md and content-sessions.md name the
Rust runtime's content path as the explicit compatibility path on the session
the client already holds, lasting until an authenticated content connector
exists behind the same fetch_content owner; runtime-parity-scope.md gets the
evidence row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
content-vectors.cs-written.json is written by CultMeshContentVectorTests on
Linux, and includes the answers CultMeshLegacyRudpContentServer gives, so the
failure spellings are the reference's. tests/content.rs judges it. The
rs-written mirror follows in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds citer-both-in-target to the shared fixture, the first row in file (and
row-order) to cite "dup-key" through Design - both leaf_a and leaf_b at that
key are in Design's declared target, unlike citer_narrow's narrow_ref above
it (only ever one in-target candidate). Adds the matching vector case to
both runtimes' case lists and regenerates both written vector files
(CULTNET_WRITE_VECTORS=1), so cross-runtime agreement on this shape is
pinned by the parity check, not just asserted per-runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The fixture had zero duplicate record keys across its ten rows and no
ordinal tied across two different schemas - both shapes this fix batch's
rulings needed and neither runtime's parity vectors ever covered.
R-AF adds a "citer_narrow" schema (real SHA-256 id, computed the same way
CultDocumentRegistry does - a probe run against a type matching the
eventual C# type's exact namespace/nesting confirmed it) whose one
reference targets leaf_a only, plus rows for both shapes from Soul's
table: a lone out-of-target row at a key (refuses), and two rows sharing
a key where only one is in-target (accepts, resolving the in-target one
regardless of row order).
Also drops "targetSchema" from every reference entry: it was never
anything but documentation (SelectionParityVectorTests.BuildDocument and
cultnet-rs's build_row both already read only targetKey) and keeping a
field the wire does not actually have would contradict the rule R-AF just
established.
R-AJ adds a tied-ordinal pair across leaf_a and leaf_b, with keys chosen
opposite to the real schema ids (probed) on purpose, so a schemaId-first
order and a recordKey-first order are forced to disagree regardless of
which real id happens to sort first - see the row-tiebreak test this
enables.
selection-vectors.cs-written.json and selection-vectors.rs-written.json
are regenerated from the new fixture (CULTNET_WRITE_VECTORS=1 on both
sides) and cross-checked in both directions locally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
share one alias rule
selection-vectors.fixture.json's "//aliases" comment still called the
two alias forms a cross-runtime defect ("no candidate string that
resolves through both"). That was true before the Rust alias port; it
is not true now. Both runtimes resolve a schema alias the same one way
(strip a trailing .vN, compare the remainder against the descriptor's
declared SchemaName): nameAlias resolves through both, and hashAlias -
kept in the fixture on purpose - is refused as unmatched by both, since
neither runtime's production call sites produce or accept it.
Prose-only change to this shared fixture file, as ruled: no vector
values, schema ids, or row data touched.
Also recorded for Self's map: cultnet-rs's alias port (R-E) does not
carry SchemaVersion or CompatibleSchemaIds - no Rust call site needs
either, so schema_alias::matches folds them away and agrees with the C#
rule only on the subset it actually uses (candidate/id/name comparison,
not version or compatibility-list matching).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CultNetErrorMessage is a MessagePack-CSharp map-mode object, so every
declared key is written on the wire, nil included - the same fact R-S
already established for the raw document header. cultnet.error.schema.json
was missing routingHint entirely and typed code/details as always-present
non-null values; Soul's P7 found real bytes failing it on exactly this.
routingHint, code and details are now `required`, with nullable types
matching CultNetErrorMessage/CultNetErrorDetails. A new test decodes real
bytes for all four refusal codes (selection_invalid, cursor_stale,
cursor_invalid, reference_outside_target) against the schema.
MiniJsonSchemaValidator:
- implements minimum, maximum, maxLength and maxItems, which were declared
in committed schemas (fieldPredicate's number pattern, selection.limit,
...) but silently never checked;
- anchors `pattern` to a full match (Index==0 && Length==value.Length)
instead of Regex.IsMatch's "search anywhere", so .NET's `$` (which,
unlike ECMA-262's, matches immediately before one trailing newline even
without RegexOptions.Multiline) can no longer let e.g. "5\n" pass a
schema author wrote as "exactly one digit";
- refuses a schema node that uses a keyword this validator gives no
constraint semantics to, instead of silently ignoring it. Running every
committed contracts/cultnet/*.schema.json schema through this found 12
such uses, none reached by any existing test: `allOf`/`if`/`then`/`else`
in cultnet.database-change-raw.schema.json, and `uniqueItems` (six
places) plus one `anyOf` in the cultmesh.verse-catalog-*.schema.json
pair. contentEncoding, $schema/$id/$defs/title/description/default and
the x-cult-runtime-type vendor extension are treated as metadata with no
constraint semantics to implement, not refused.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New contracts/cultnet/cultnet.raw-document-header.schema.json for
CultNetRawDocumentHeader - it has no payload/payloadEncoding members at all,
so headers must not $ref the record schema (which required both). The
response page's own schema now lists every property CultNetSnapshotResponseRawV1Message
declares as `required` (present on the wire, nil included) rather than only
the four that happened to be non-nullable - MessagePack-CSharp's map-mode
formatter writes every declared key regardless of null, so `headers`/`documents`
are always both present, one an array and the other an explicit null by
projection. The old `oneOf: [{required:[headers]},{required:[documents]}]`
matched both branches on every real page (both keys present) and so never
validated anything; it is now `oneOf` over each key's type (array vs null),
which is mutually exclusive. cultnet.raw-document-record.schema.json gained
schemaName/schemaVersion/schemaContentHash (present on every real record, not
previously in the schema at all - any real page would have failed
additionalProperties:false on them). fieldPredicate's two oneOf branches each
gained `not: {required: [the other]}`, so `values` and `number` cannot both
be present (previously only `required` was checked, which does not exclude
the sibling key).
Added MiniJsonSchemaValidator, a deliberately small draft-2020-12 subset
validator (type/required/additionalProperties/properties/items/const/enum/
pattern/minLength/minItems/oneOf/$ref, same-document and cross-document) -
no schema-validation dependency existed in this repo, and this only needs to
cover what contracts/cultnet/*.schema.json actually use. CultNetSchemaContractTests
decodes real wire bytes from a live CultNetDatabaseServer (header projection,
document projection, and a hop with edges) through MessagePackSerializer.ConvertToJson
and checks them against the committed schema files, plus three
fieldPredicate-shape tests pinning the values/number exclusivity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
R-N: cultnet.error.v0 gains additive code/details fields (contracts/cultnet).
CultNetErrorMessage grows ForSelectionInvalid/ForCursor/ForReferenceOutsideTarget
factories; both C# servers (CultNetDatabaseServer for snapshot+live,
CultNetDatabaseSubscriptionServer) use them at every existing catch site, plus
two sites that previously let the exception escape uncaught instead of reaching
the peer (CultNetDatabaseServer's v1 subscribe snapshot branch,
CultNetDatabaseSubscriptionServer's Subscribe around CreateProjectedSnapshot).
CultNetSelectionCursorException now carries AsOf/Current for cursor_stale.
R-P: CultNetDatabaseSubscriptionServer.CreateMatchedRecord (the live fast path)
authorized against descriptor.SchemaId; the snapshot path (CreateProjectedSnapshot)
already authorized against the wire id. Both now call the same
CultNetDocumentRegistry.WireSchemaId (made internal, was private) so one id
reaches the authorizer on the snapshot, live and reconcile paths alike.
R-T(a): CultNetSelectionEvaluator.MatchesSchemaKeysFields no longer reads a
Removed change's (always-null) document against Fields - it matches on
keys/schemas alone and treats a field predicate as unknown instead of throwing
TryGetIndexValue(null).
R-T(b): EnsureWithinDeclaredTarget no longer skips the out-of-target check when
TargetType is null; it checks against every registered leaf (unconstrained is a
real, checked target set, not a bypass). CultCache's ResolveReferenceTarget also
now infers the target from a many reference's CultRecordRef<T> element when no
explicit [CultReference(typeof(...))] is declared, which was the one remaining
source of a null TargetType reaching that check on a real, testable path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran CULTNET_WRITE_VECTORS=1 dotnet test tests/GameCult.Networking.Tests -c
Release --filter FullyQualifiedName~WriteSelectionVectors on Yggdrasil
against b1932d9 and committed its output verbatim. 17 vectors total (up from
15): the 4 door-refusal vectors now include
refuses_unmatched_cites_target_schema and
refuses_cites_target_by_hash_shaped_alias.
Per the brief, this is the fix-batch-3 report's parity-vector table baseline.
The C# reader (SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyIn
TheReference) now passes all 16 of rs-written.json's vectors, up from 14/16 -
rs-written.json already carried both refusal vectors; only this file and the
door check that makes it agree were missing on the C# side. rs-written.json
itself was not touched (Rust-owned).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Self's ruling, 2026-09-22 ("the C# reference's alias rule is the rule"): an
alias is a schema *name* with a trailing .vN suffix, resolved against the
descriptor's declared name - never against a hash id. C#'s production call
sites (CultNetSelection.cs:438, CultNetSelectionEvaluator.cs:88,340) all use
the descriptor overload of CultNetSchemaAliasMatching.Matches, never the bare
id-vs-id overload.
- Row gains schema_name() (defaults to schema_id() for rows with no separate
name, e.g. raw wire records); RowSet gains schema_name(schema_id) ->
Option<String>. Both stand in for CultDocumentDescriptor.SchemaName, which
this crate has no type for (D7).
- schema_alias::matches/matches_any now take (candidate, schema_id,
schema_name) and resolve a candidate's inferred name against the declared
name, exactly as the C# descriptor overload does - not against schema_id's
own inferred name, which is what the deleted hash-stripping matcher did.
- Every call site (reachable_schemas, the cites.target door check,
matches_schema_keys_fields's D6 fast path, matches_citation) now resolves
and passes a schema's name instead of comparing two schema-id strings.
- Deleted: the hash-stripping schema_alias::matches(candidate, schema_id)
two-string overload. A hash-shaped alias ("<hash>.v1") is not a wire form
either runtime's production rule ever resolves.
Behaviour verified against a standalone run of the exact C# algorithm
(CultNetSchemaAliasMatching, CultNetDatabase.cs:47-97) covering exact-id
match, bare-name match, .vN alias resolution, MatchesAny's empty-matches-all
convention, ordinal case sensitivity, ".v" with no trailing digits, and
multiple ".v" markers (last one wins) - every case is now a Rust unit test
in schema_alias::tests.
Test fixture (tests/selection.rs) updated: the fixture's hashAlias entries
are now a shared negative check (neither runtime resolves them), and the
nameAlias entries now resolve in Rust too, fixing the confirmed cross-runtime
defect (schemas_by_name_alias). Regenerated
contracts/cultnet/interop/selection-vectors.rs-written.json (15 -> 16
vectors): added refuses_cites_target_by_hash_shaped_alias, renamed
hop_by_role_cites_hash_alias to hop_by_role_cites_name_alias.
cultnet-rs: cargo test 223/223. C# reader (rs-written.json): 14/16 - the two
failures are both the pending unmatched-cites.target door refusal
(CultNetSelection.cs:361-391 has no such check yet), not a Rust defect; the
other Hands' C# door fix covers both.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Reconciles the two Hands' independently-regenerated parity vectors
(Soul's whole-cut pass, F3): the C# fixture used schema *names*
("leaf_a") as literal ids, the Rust fixture used invented fake
sha256-shaped ids - every vector carrying an id failed when read by
the other runtime.
Adds contracts/cultnet/interop/selection-vectors.fixture.json: the one
row set both writers now build from, keyed by each schema's real
SHA-256 content-hash id exactly as CultDocumentRegistry computes it
(dumped via a temporary DumpFixtureSchemaIds run). Carries the rows
either runtime needs - astral/BMP-private-use keys, a float tie,
cites/cited, a cites target reached by alias - plus two DIFFERENT
alias forms, because the two runtimes' alias matchers key off
different descriptor attributes and no single candidate resolves
through both (see the fixture's "//aliases" note and the report).
Rewrites SelectionParityVectorTests.cs to load the fixture instead of
hand-building rows (BuildFixture/BuildExtendedFixture deleted), assert
the fixture's declared schema ids still match what the registry
computes (fails loudly on drift instead of silently comparing wrong
ids), and use the real schema id (not SchemaName) as a row's
cross-runtime identity. Also fixes a real bug this file carried since
before the shared fixture: both the writer and the reader compared
`evaluation.Rows.Count` (the page size) against "matched" instead of
`evaluation.TotalMatched` (the total, per R-G) - selections with a
Limit were writing/checking the wrong number. The reader now collects
every vector's mismatch instead of stopping at the first one, so a
real parity defect is reported in full.
Regenerates selection-vectors.cs-written.json from the shared fixture.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>