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>
Adds a revert (and loosening, where the rule has one) per new rule to
scripts/mutate-cultnet-selection-rust.entries.ps1, and repairs three things
the entries file's own first run against this fix batch found:
- The RS-QJ-ExponentExpansion-Revert entry targeted
expand_scientific_notation, which R-M deleted; replaced by entries
against R-D's real replacement (the mantissa/exponent bignum).
- RS-D-ExponentBranch-Revert's first draft ("New = if false") hung: it
routes every value, including f64::MAX's exponent of ~971, into the
fraction branch, whose "k = (-exponent) as usize" wraps a positive
exponent's negation into a huge usize and spins effectively forever - the
run had to kill that process by PID. "New = if true" is the safe
direction (a negative exponent's "for _ in 0..exponent" is just an empty
range, not a cast) and is pinned against a negative-exponent test
instead.
- RS-V0Lowering-Revert (pre-existing, not from this batch) unexpectedly
survived the first run: matches_schema_keys_fields's move to
schema_alias::matches_any (R-E) picked up that helper's own "empty
candidates matches everything" shortcut, masking the v0-lowering
mutation this entry exists to catch. Fixed in the prior commit
(matches_schema_keys_fields no longer uses matches_any's shortcut); a new
entry, RS-E-EmptySchemasNeverMatchesEverything-Revert, pins the fix
directly rather than only through v0's own lowering.
All commands are narrowed with "-- --skip
selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust":
that vector-file test is legitimately pending the parallel C# Hands's own
regenerated cs-written.json (this suite does not touch that file), the same
carve-out this file already gave the pre-existing
reactive_document_coalesces_direct_same_schema_alias_member_writes flake.
Full run against this batch's final state: 36/36 entries killed, M0 green
on both commands, no survivors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- contracts.rs: replaces the ~335-line hand-written require_*/encode_*
codec for SelectionDocumentRecord/RawDocumentHeader/Edge/RecordRef with
two generic helpers (require_typed/encode_typed) that go through
rmp_serde::to_vec_named/from_slice and rmpv::Value's own Serialize -
these four types already derive Serialize/Deserialize with the wire's
camelCase field names. rmpv::ext::to_value/from_value was tried first and
rejected: it serializes a struct positionally (one array slot per field
in declaration order), which breaks the moment a skip_serializing_if
field is actually skipped and is not the named-map encoding this wire
always uses. Proven both ways by
tests/selection.rs's snapshot_response_raw_v1_round_trips_* tests, which
round-trip real headers, documents-with-payload, and edges with and
without a payload through the real cultnet.snapshot_response_raw.v1
message envelope.
- snapshot_query.rs: deletes reject_duplicates and its two call sites -
dead, because lower_v0_list already dedups (via its own HashSet) before
reject_duplicates ever runs, so its duplicate check can never fire.
(RawDocumentHeader::from_document, the Eq derives on the selection wire
types, and two dead branches the mutation harness's own first run proved -
exact_decimal_from_mantissa's redundant "if mantissa == 0" guard and
big_mul_small's unreachable multi-digit carry loop - are selection.rs
changes already covered by the prior commit in this batch.)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Rust half of the Cut 1 fix batch (docs/cultnet-selection-cut.md, "Self's
rulings for the Cut 1 fix batch, 2026-09-22"), against packages/cultnet-rs.
- R-B: hop edges now follow the hop's direction - cites keys an edge off its
From (the citer), cited off its To (the citee), each in page-row order
then (from, role, to) code-point order. Before this, both hop kinds keyed
off From, so a cited selection's edges were silently empty unless the
citer also happened to be on the page (Soul's F2). build_incoming_index
iterates the caller's own row slice, never a HashMap, for its own order.
- R-C: code-point order was already Rust's default str::cmp (UTF-8 byte
order); no change needed there.
- R-D: canonical_number::render_f32/render_f64 render the exact IEEE-754
decimal expansion from mantissa and exponent, via a small fixed-point
bignum (~60 lines), replacing the previous shortest-round-trip-form path
that rounded ties differently from .NET. expand_scientific_notation and
its test are gone (R-M); the Display probe test is kept per the map.
- R-E: schema_alias (ported from CultNetSchemaAliasMatching's string
overload) is the one schema-identity rule for `schemas` and
`cites.target.schemaId`; an unmatched cites target is refused at the
door. Found and fixed along the way: matches_schema_keys_fields must not
use schema_alias::matches_any's own "empty candidates matches everything"
shortcut (correct for the door/reachability call sites it mirrors from
C#) - v0's snapshot server calls matches() directly, without validate(),
so an empty-but-present schemas list must still match nothing there too.
- R-F: select() validates first and returns the typed refusal; matches()'s
hop guard is a real assert!, not a debug_assert! a release build would
compile out.
- R-G/P-1: Evaluation and SelectionPage both carry matched, the total match
count, not the page count.
- R-H: Cursor::compute_digest is length-prefixed for every string and list,
closing the collision Soul found (["a|b"] and ["a","b"] used to digest
the same).
- R-I: select_page projects a selection into the wire's SelectionPage -
headers or documents under Selection::projection, matched, and edges; a
header projection carries no payload, in edges too (S20).
Test fixture: real SHA-256-shaped schema ids (sha256:<hex>.v1) replace the
old fixture's schema names doubling as ids, which had silently masked the
R-E gap (vectors passed only because "leaf_a" happened to already be both
the alias and the id).
Two documented discrepancies against the map's own worked numbers, both
verified independently outside this crate (Rust bit decomposition plus a
Node/BigInt cross-check): an f32 of 3e20 is 300000006012263202816, not the
map's 300000002010536247296; 1e-7 as f64 is its own 79-digit exact
expansion, not "0.0000001" (a stale shortest-form assertion this fix batch
did not originally touch).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
- cultnet.selection.schema.json: schemas/keys gain minItems: 1 (present
and empty is refused at the door, never "every schema/key" - omit the
field instead); limit documents that it is clamped 1..=200 by the
answering server rather than refused outside that range.
- cultnet_py/interop_peer.py: handle_server_message refused only an
exact ".v1" suffix, so a v2+ selection message fell through to a
silent empty-list answer instead of a loud refusal. is_unsupported_cultnet_version
matches any cultnet.*.vN with N > 0 instead. test_cultnet.py's R-2
test now covers v2 and v10, not only v1.
- mutate-dotnet.ps1: an entry's File (from -Entries, often written with
"/") is looked up against the $targets dictionary keyed by -Target's
own spelling; a "\\" vs "/" mismatch didn't throw at the lookup -
$targets[$file] silently returned $null - so the crash surfaced one
call later as a null-argument error inside a .NET path API instead of
as "unknown target". Both -Target and every File read from an entry
now normalise through Normalize-TargetPath before indexing $targets.
Discrepancy: the ruling names "both R-2 sites" for the Python fix; only
one site with this exact narrow-match bug exists in packages/cultnet-py
and packages/cultmesh-py (cultmesh_py/server.py's handle_message already
refuses every unrecognised schemaVersion, v1+ included, through its own
default branch). Fixed the one that exists; did not invent a second.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- R-I: new CultNetSelectionPageTests.cs kills Soul's two named S20
mutants - dropping `wantDocument &&` in ToEdge (a header-projection
edge would carry document payload bytes anyway) and always emitting
Edges instead of `selection.HasHop ? ... : null` (a hop-less
selection's page would carry an empty array instead of no Edges
member) - plus a matched-vs-page-length test for R-G. Fixing
ToEdge's mutant coverage surfaced a real bug the new tests caught:
it serialized an edge payload through CultDocumentMessagePackSerialization,
which requires the payload's type to be a registered CultDocument
and throws for a plain declared value type (float, in the fixture);
it now serializes directly through MessagePackSerializer.
- R-D: CultNetCanonicalNumberTests gains "5\n"/"5\r\n" cases - .NET's
$ matches immediately before a single trailing newline even without
RegexOptions.Multiline, so the door's pattern is re-anchored with \A/\z
and would have accepted "5\n" as canonical without this. Caching's
TryGetIndexNumberRewritesADoublesExponentNotationToPositionalDigits
is updated for 1e-7's actual exact decimal expansion (the R-D commit's
own effect): the old assertion pinned "0.0000001", the shortest
round-trip form a pre-R-D render would have produced.
- R-K: every serverThread.Join() in NetworkingTests.cs that sat outside
a try/finally now sits inside one, so a failed assertion cannot leave
the poll thread running past the point where `using` disposes the
socket it polls (an ObjectDisposedException on that raw background
Thread was a host crash, not a test failure). The bodyDemand.HasConsumers
assertions wait on the condition instead of assuming
SubscribeAsync's completion and PublishDemand happened in an order
a network round trip does not guarantee.
- Reflection fix: DatabaseSubscription_FiltersCanonicalWireChangesByRequestedSchemaAlias
built a v0-shaped CultNetDatabaseSubscribeMessage to invoke
CreateMatchedRecord by reflection; that method now takes the shared
SubscriptionRequest (R-A), so the test builds one through the same
reflection path instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires v1 into both C# servers (R-A): CultNetDatabaseServer registers
CultNetSnapshotRequestV1Message/CultNetDatabaseSubscribeV1Message
listeners and Server.cs's pre-verification allowlist gains them;
CultNetDatabaseSubscriptionServer's subscribe path is generalised to a
shared SubscriptionRequest so v0 (lowered) and v1 selections run the
same Subscribe/Watch/Reconcile core, with D6's rule wired in: a
hop-bearing selection runs the full reconcile diff on every change
instead of the single-row fast path. CultNetDatabase.CurrentAsOf gives
both servers a real snapshot watermark.
Fixes the confirmed parity and correctness defects from Soul's
whole-cut pass:
- R-B: cited/cites edges are anchored to the row the direction actually
reports against (the citer under cites, the cited row under cited),
not filtered by "From is on the page" regardless of direction; order
is page-row order then (from, role, to) in code-point order.
- R-C: a new CultNetCodePointComparer replaces every StringComparer.Ordinal/
string.CompareOrdinal in the row tiebreak, cursor position and edge
order, so an astral key sorts the same way here as it does under
Rust's UTF-8 byte comparison.
- R-D: CultCache renders a float/double's exact decimal expansion from
its IEEE-754 bits via BigInteger (RenderExactFloat/RenderExactDouble),
never .NET's shortest round-trip form, which rounded a tie the
opposite way from Rust at some values. The now-dead ExpandScientificNotation
is deleted.
- R-E: MatchesCitation compares a cites target through
CultNetSchemaAliasMatching instead of an exact SchemaId string
compare; ToEdge and ToRawRecord now emit the same wire schema id
(WireSchemaId) for the same row, where ToEdge previously used the
descriptor's own id and ToRawRecord the binding's override.
- R-F: CultNetSelectionEvaluator.Select validates first, unconditionally
- the validate:false/true flag on SelectPage/CreateRawSnapshotResponse
is gone, so no public evaluation path can skip the door. Mesh's
EnsureV0Compatible runs the new CultNetSelectionValidation.ValidateShape
(the declaration-independent half of the door) before its own v0
transport refusals, so Keys=[]/[""] is refused there too.
- R-G: Evaluation.TotalMatched is the selection's whole matching count;
SelectPage's Matched field now reports it instead of the page's row
count. CreateRawSnapshotResponse and CreateShardSnapshotResponse
(v0/shard full walks) call the new CultNetDocumentRegistry.SelectAll,
which evaluates once via CultNetSelectionEvaluator.EvaluateAll and
projects every matched row, instead of looping SelectPage per 200-row
page and re-filtering/re-sorting the whole row set each time.
- R-H: the cursor digest is length-prefixed for every string and list
(AppendString/AppendList) instead of delimiter-joined, closing the
collision ["a|b"] == ["a","b"] and ["a,b"] == ["a","b"].
- R-J: any_of on a numeric alias compares TryGetIndexNumber's canonical
rendering, not the string getter's culture-dependent ToString().
- R-M: WithBindingSchemaAlias is deleted; both servers' single-change
fast path calls CultNetDocumentRegistry.ExpandSchemaBindingAliases
instead, the one binding-alias reconciler. The duplicated v0/shard
lower-and-page loop is gone with SelectAll above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two things landed on this branch while commit 3 was in flight, both addressed
here rather than left for the vectors to paper over:
**Self's ruling, 2026-09-22 (commit 2 fix batch, 568e8e3/021e3f1): empty
lists, blank entries, and v0's own cleaning.** Rust already refused an empty
schemas/keys list (inherited from before the ruling); it now also matches
the reference's three further decisions exactly, not just in spirit:
- The door's refusal field is the plain list name ("schemas"/"keys"), not an
indexed "schemas[i]" - selection::validate's SelectionInvalid.field now
matches CultNetSelectionInvalidException("schemas", schema, ...) literally,
because the parity vectors compare this string across runtimes.
- snapshot_query.rs's lower_v0_list now deduplicates a mixed list the same
way CultNetV0SelectionLowering.Lower does (Distinct(StringComparer.Ordinal)
- order-preserving, first occurrence wins), not just blank-filters it.
A new test (serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter,
extended) pins the dedup case directly.
- Two mutation entries (RS-EmptySchemas-Revert, RS-EmptyKeys-Revert) added
for the door's original empty-list refusal, which had tests but no
mutation coverage before this commit.
**S12: the two-way parity vectors**, contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json,
eight vectors each: the six evaluated selections from commit 3 (any_of+ge
conjunction, descending+limit, gt boundary, cited exists true/false, three
rows sharing one index value) plus two door-refusal vectors added for this
ruling - `refuses_empty_schemas_list` (schemas: []) and `refuses_blank_key_entry`
(keys: ["a-eq", " "]) - which decode their selection bytes and assert
`validate`/`Validate` refuses at exactly the named field, never evaluating.
The blank-key vector could not be generated before this commit: the door
rule it exercises did not exist in the reference until 568e8e3 landed.
`cites`-based hop vectors are deliberately absent (documented in both test
files): MatchesCitation compares Citation.Target.SchemaId against the
reference's real SHA-256 schema id exactly, with no SchemaName fallback,
and this fixture pair has no way to agree on that hash across runtimes;
`cited` (role/existence, matched by record key alone) is the hop direction
both sides can cross-check, so incoming_exists_true/incoming_negation carry
that coverage instead. Both reader tests pass:
SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference (C#)
and selection_vectors_written_by_the_reference_decode_and_evaluate_identically_in_rust
(Rust), each judging the other runtime's file.
**The remaining mutation matrix.** Of the coordinator's named set (S1, S2,
S6, S9-S15, S17, S20, S23): S1 was already covered (RS-EmptySchemas/Keys,
RS-BlankSchema/Key); S2 (any_of matches membership, RS-S2-Revert) and S6
(cites.role filters the hop, RS-S6-Revert) are newly covered - S6 needed a
new fixture (cites_role_excludes_a_second_reference_at_the_same_target)
because base_rows()'s citer-1 only ever carries one reference, so a mutant
dropping the role filter had nothing else to wrongly match and survived
against the original hops_one_edge_by_declared_role test; that finding is
recorded in-line as a comment, not silently swapped for an easier mutant.
S9-S15 (subscription reconcile, v0-lowering-of-subscribe) do not apply:
D7 - no Rust caller subscribes in this cut, so there is no subscription
client to mutate. S17 (inherited alias reaches every leaf) does not apply
either: Rust's toy Row/RowSet has no inheritance to begin with - both
leaves in the fixture declare "mass" independently, which is the Rust
spelling of the same guarantee, already exercised throughout the suite
without a dedicated mutant (there is no shared base-type discovery path to
revert). S20 (edges only for a hopping selection) was investigated and
found structurally vacuous in Rust as built: `edges` is only ever populated
inside the cites/cited branches, so a mutant on the has_hop() gate cannot
produce a different observable result for a non-hopping selection - no
entry added; a real gap would need the evaluator's structure to change
first. S23 (matches every row sharing an index value, never a cache's
unique-index winner) has no revertable bug surface: Rust's Row/RowSet
never had a unique-index map to begin with (D7), so this is pinned by
construction, not by a mutation-testable rule; matches_every_row_sharing_an_index_value
still exists as a positive test naming it.
**A pre-existing, unrelated flaky test, found and worked around, not
fixed.** tests/cultnet.rs's reactive_document_coalesces_direct_same_schema_alias_member_writes
fails consistently when the mutation harness runs the full tests/cultnet.rs
binary (both default-parallel and RUST_TEST_THREADS=1), but passes 5/5 in
isolation and passes in a plain `cargo test -p cultnet-rs` run - state
pollution from another test in that binary under the harness's specific
invocation path, not a race this campaign's code participates in (nothing
here touches reactive_document machinery). RS-V0Lowering-Revert's Command
is narrowed to `cargo test --test cultnet serve_read_only_raw_snapshot`
(filtering both M0 and the entry's own run to this campaign's test) so the
suite does not depend on that unrelated test's health; the flake itself is
reported here as a finding, not patched.
Tests: cultnet-rs 174 passed, 0 failed (default parallel run).
GameCult.Networking.Tests 196 passed, 1 skipped (WriteSelectionVectors,
env-gated), 0 failed.
Mutation harness: 24 entries (was 22 in commit 3; +RS-S2-Revert, +RS-S6-Revert;
RS-BlankSchema/BlankKey and RS-V0Lowering-Revert's anchors and field-name
assertions updated to match the landed fix batch's exact spelling), all
KILLED, M0 green.
Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src` empty.
Structural delta, this commit only: 6 files changed, +108/-23 (selection.rs
+15/-9 field-name fix; snapshot_query.rs +25/-14 dedup; tests/cultnet.rs +21
dedup test; tests/selection.rs +35/-2 S6 fixture and field-name fixes;
scripts/mutate-cultnet-selection-rust.entries.ps1 +33/-4 two new entries
plus three corrected anchors/commands; SelectionParityVectorTests.cs +2/-1
field name), plus the two committed vector JSON files (~90 lines each, new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
R-2 was owed by Cut 1 and not yet done (docs/cultnet-selection-cut.md, section
9, Self's rulings 2026-09-22): a v0-only Python peer must refuse a
cultnet.selection.v1 carrier message loudly instead of silently answering
with an empty list.
- packages/cultnet-py/src/cultnet_py/interop_peer.py:
handle_server_message's fallback now recognises any cultnet.*.v1 schema
version and replies cultnet.error.v0 { code: "unsupported_schema_version",
details: { schemaVersion } } via the new unsupported_schema_version_error
helper, matching cultmesh_py's existing error shape. One test
(test_python_interop_peer_refuses_v1_selection_messages) proves a
snapshot_request.v1 and a database_subscribe.v1 message both get the
refusal, not an empty list.
- packages/cultmesh-py/src/cultmesh_py/server.py's delegating path
(_handle_connection_message -> handle_message) already refused every
unsupported schema version generically, v1 included - no code change
needed there. test_cultmesh_local_server_returns_errors_for_bad_requests
gains an explicit assertion for a snapshot_request.v1 message through that
same path, so the coverage is named rather than only implied by the
generic case.
This is the one exception to "do not touch packages/" in this fix batch: only
these two Python packages' interop/server files and their tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul found commit 2 didn't pass (docs/cultnet-selection-cut.md, 2026-09-22):
empty/blank schemas-keys still reached the evaluator, the shard snapshot ran
its own loop instead of the evaluator, Mesh's v0 wire silently dropped v1-only
selection terms, the Mesh one-pass read could return the wrong schema's
record at a shared key, two decode rules for a foreign schema id disagreed,
dead identity-comparison code from the _projectRecord removal was still live,
and a disposed-socket race could abort a mutation run instead of recording
NO VERDICT. This is Self's fix batch for all of it.
- CultNetSelection.Validate now refuses a blank/whitespace entry inside
schemas or keys, not just an empty array. CultNetSelectionEvaluator no
longer treats [] as "no filter" (the door already refuses it before
evaluation). CultNetV0SelectionLowering is the one place that lowers an
empty or all-blank v0 list to null; CreateRawSnapshotResponse and
CreateShardSnapshotResponse both route through it, and CultMesh's own
Clean() now delegates to it instead of duplicating the rule.
- CreateShardSnapshotResponse no longer carries a private loop or its own
schema/key matching: it builds a CultNetSelection and answers through
CultNetDocumentRegistry.SelectPage (the same evaluator path as the
non-shard snapshot), with the shard's own membership check as a row
filter. Binding-alias expansion now runs on both paths identically.
- ResolveDefaultSelection decides "no schema filter" from the cleaned
recordKeys, not the raw array length. OverlaySelection stops deciding what
a missing override means: any explicit schemaIds/recordKeys override
replaces the default selection wholesale; no override keeps the default
untouched.
- CultMeshSnapshotRequestOptions.Selection is now checked by
EnsureV0Compatible before every send: fields, cites, cited, limit, cursor,
descending, and any non-default projection are refused typed
(CultNetSelectionInvalidException), never silently dropped from the v0
wire message. Mesh sending v1 is FU-Mesh-v1, not this cut.
- CultMeshHotBodySubscription.Selection returns a defensive copy on every
read so its arrays can't be mutated out from under the stored selection.
- ReadDocumentFromSnapshotResponse now searches candidates at one recordKey
in one order - exact schema, then alias, then a foreign schema id decoded
from the payload - instead of one OR'd condition where a case-insensitive
or structurally-lucky candidate could win first. The foreign-id decode
reads the payload's embedded schema version with the registry's shared
CultNetDocumentRegistry.TryReadSchemaVersion and alias-matches it against
the caller's own descriptor directly, rather than resolving to "whichever
registered type owns this schema string" - that resolution is ambiguous
when two document types alias the same schema id (MeshNoteDocument and
MeshNoteAliasDocument in the existing fixtures), which is what broke on
the first pass; TryResolveDescriptorByPayloadSchema stays for its existing
caller (ResolveDescriptorForRawDocument) with a note on when not to use it.
DecodeSnapshotDocuments and the Mesh copy of the payload-schema decode
(RawSnapshotPayloadMatchesSchema/TryReadSchemaVersion) are deleted in
favour of the shared one.
- CultNetDatabaseSubscriptionServer: the dead RecordKey/SchemaId identity
comparisons in Reconcile and ApplyProjectedChange, and the duplicate-key
throw in CreateProjectedSnapshot, are deleted - they can no longer fire
now that a projection's dictionary key is always the record's own key.
A new test flips authorizeRecord per key while the whole request stays
authorized (so DemandActive never toggles) and proves both halves of the
remaining diff: the removal of a now-unauthorized key and the add loop
picking up a newly-authorized one.
- Every NetworkingTests.cs test that starts an IsBackground poll thread now
joins it before its server disposes (about 20 tests; one already did).
- scripts/mutate-dotnet.ps1: a dotnet-test-host crash writing to stderr no
longer throws the whole run away under PowerShell 5.1's
$ErrorActionPreference = 'Stop' (2>&1 on a native command wraps stderr in
a terminating NativeCommandError); Run-Tests catches it and the entry loop
records NO VERDICT instead of aborting every remaining entry.
- Two probe tests: CultNetDatabase_ShardAndNonShardSnapshot_Agree{On
UnfilteredRequest,OnBindingSchemaIdOverride} prove the shard and non-shard
snapshot paths agree, including shard-membership exclusion of a document
outside the shard's key prefix and a binding schema-id override matching
on both paths.
- Mutation entries updated/added to match: the stale NET-D4-FourthMatcher
entry (its anchor was the private loop this batch deleted, not refactored)
is replaced by NET-D4-ShardMembership-Revert; the Mesh entries file's
header miscount ("two of three" loosening entries when there are only
two) is corrected and both previously-surviving loosening mutants
(MESH-DEFAULT-Loosening, MESH-KEY-Loosening) now have killing fixtures;
scripts/mutate-cultnet-selection-fixbatch2.entries.ps1 is new, covering
Reconcile's remaining two mutants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/cultnet-selection-cut.md section 6/7: packages/cultnet-rs/src/selection.rs
(new) - the Rust spelling of the vocabulary at wire parity with the corrected
C# reference (e5348d3), matching D7: no cultcache-rs knowledge, the row owner
supplies declared values and edges through two traits.
- Wire types: Selection, FieldPredicate, Citation, Incoming, RecordRef, Edge,
RawDocumentHeader, SelectionDocumentRecord (the v1 full-shape record, distinct
from v0's CultNetRawDocumentRecord, which lacks schemaName/schemaVersion/
schemaContentHash - a pre-existing v0 gap this cut does not touch).
SelectionOperator mirrors CultNetSelectionOperator for evaluator ergonomics;
FieldPredicate.op stays a raw wire string, never a serde-tagged union.
- canonical_number module (Q-J, section 2 "Numbers"): is_canonical (a
hand-rolled digit-grammar parser, no regex dependency added),
canonicalize_decimal_digits, expand_scientific_notation, compare (sign, then
integer length, then digits, then padded fraction - no f64 anywhere), and
render_i64/render_u64/render_f64/render_f32 for a Row implementer's own
rendering. probe_f64_display_never_uses_exponent_notation empirically
verifies the map's claim about Rust's f64 Display (confirmed true) rather
than trusting it.
- Row/RowSet traits (D7): schema_id, record_key, ordinal, values, number
(canonical string, never f64), references for Row; all_schema_ids,
declared_indexes, declared_roles, is_numeric, target_leaves for RowSet.
A blanket `impl<T: Row> Row for &T` lets sorted/filtered slices of
references satisfy generic bounds without an extra copy.
- validate (S1 door refusals), matches (D6 single-row fast path, hop-bearing
selections assert via debug_assert since Rust has no exception type to
mirror C#'s InvalidOperationException), select (order/hop/cursor/paging,
D9's reference_outside_target refusal, S23's per-row match - never through
a unique-index map, because there is none here to begin with).
- Cursor: mint/parse/digest is this runtime's own internal algorithm (a
cursor is opaque and never crosses runtimes - each server answers its own
pages), not a byte-for-byte port of the reference's; what must match is the
*rule* (asOf + last position + a digest bound to the selection), which it
does, pinned by RS-CursorStale-Revert/Loosening and RS-CursorDigest-Revert.
- contracts.rs: three v1 CultNetMessage variants (SnapshotRequestV1,
DatabaseSubscribeV1, SnapshotResponseRawV1) matching the C# messages'
field-for-field shape. SnapshotResponseRawV1 carries binary payloads
(documents[].payload, edges[].payload), so it is routed through the
existing raw JSON-bypass path (parse_raw_cultnet_schema_message/
encode_raw_cultnet_schema_message) alongside v0's DocumentPutRaw/
SnapshotResponseRaw, with its own require_*/encode_* helpers for
SelectionDocumentRecord, RawDocumentHeader, Edge and RecordRef.
SnapshotRequestV1/DatabaseSubscribeV1 carry no binary and use the
ordinary serde_json-intermediate path. No message-layer selection
semantics validation here, matching the reference
(CultNetSchemaMessageSerialization.cs only deserializes; a server calls
selection::validate against its own RowSet before evaluating).
- snapshot_query.rs (D3/D4): serve_read_only_raw_snapshot's two hand-rolled
allowlist filters collapse into one call to selection::matches over a
lowered v0 Selection, through a minimal RawSnapshotRow wrapper whose
ordinal/values/number/references are unreachable (the lowered selection
never sets fields/cites/cited, so matches_schema_keys_fields never calls
them) - documented, not hidden.
Ruling (Self, 2026-09-22, docs/cultnet-selection-cut.md commit 2 fix batch,
mirrored here per the coordinator's mid-task instruction rather than C#'s
pre-fix spelling as it stood at e5348d3):
- validate refuses an empty-but-present schemas/keys list (already had this)
and now also refuses any individual entry that is empty or whitespace
(fields[i]-style names: schemas[i]/keys[i]).
- The evaluator never sees []; matches_schema_keys_fields does not special-case
an empty list as "no filter" (it never did here - that C# fallback,
CultNetSelectionEvaluator.cs:84 pre-fix, was never mirrored into Rust).
- snapshot_query.rs's v0 lowering gets its own cleaning, owned by the
lowering and not the evaluator: lower_v0_list turns an empty or
all-blank v0 schemaIds/recordKeys list into None before it ever reaches
validate or select.
Tests: cultnet-rs 173 passed, 0 failed (was 111 baseline #[test]s pre-cut;
+62 across lib unit tests, tests/selection.rs and tests/cultnet.rs).
cultcache-rs unmodified, 64 passed, 0 failed (untouched by this commit -
confirmed unaffected). GameCult.Networking.Tests (the new
SelectionParityVectorTests.cs file, compiled but not yet fully exercised -
see below) builds clean.
Mutation harness (tools/eureka-mutations.ps1, scripts/mutate-cultnet-
selection-rust.entries.ps1): 22 entries against src/selection.rs and
src/snapshot_query.rs, all KILLED, M0 green. Covers: S1's empty-list and
new blank-entry refusals (schemas and keys, revert x4), order/descending
(revert), cursor staleness (revert + loosening) and digest (revert), S7's
incoming negation (revert + loosening), S16's le/ge boundaries (revert x2),
S18's reference_outside_target (revert), Q-J's door regex and -0 exclusion
(revert x2), Q-J's comparator integer-length and sign (revert x2), Q-J's
integer-through-float64 renderer, decimal trailing-zero stripping and
exponent expansion (revert x3, the last two pinned via direct
canonical_number unit tests since render_f64 itself cannot reach exponent
notation given Rust's Display contract - verified, not assumed), and the
new v0-lowering rule (revert).
Negative grep: `rg -n "untagged" packages/cultnet-rs/src packages/cultcache-rs/src`
empty.
Not included here, landing as commit 4 once ready: the committed parity
vector files (contracts/cultnet/interop/selection-vectors.{cs,rs}-written.json).
Both directions already work for six evaluated selections and the []
door-refusal case; the blank-key-entry refusal vector cannot be generated
from the C# side yet because the parallel C# fix batch implementing that
door check (docs/cultnet-selection-cut.md, "Self's rulings...for the commit
2 fix batch") has not landed on this branch as of this commit. Rust's own
tests (validation_refuses_a_blank_entry_in_schemas_or_keys,
serve_read_only_raw_snapshot_lowers_an_empty_or_blank_v0_schema_list_to_no_filter)
already pin both new rules independently of C#.
Structural delta: packages/cultnet-rs/src/selection.rs +1407 (new),
contracts.rs +616/-21, snapshot_query.rs +76/-21 (net +55, collapsing the
fifth selector engine into one call), lib.rs +2, tests/cultnet.rs +67 (new
v0-lowering test), tests/selection.rs +900 (new, evaluator tests + vector
machinery), scripts/mutate-cultnet-selection-rust.entries.ps1 +233 (new),
tests/GameCult.Networking.Tests/SelectionParityVectorTests.cs +296 (new).
Against section 14's ~430-line estimate for selection.rs+contracts.rs: the
two together land at ~2000 lines added, well over estimate - the map's own
§14 correction already flagged commits 0/1 running roughly 2x estimate for
the same reason (the hop's incoming index, the cursor, and per-field door
messages cost more than the prose assumed), and the v1 raw-message handling
(SelectionDocumentRecord/RawDocumentHeader/Edge require_*/encode_* helpers,
~400 lines) was not separately budgeted at all.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Corrects commit 1's Number field: docs/cultnet-selection-cut.md section 2
"Numbers" (added 2026-09-22, superseding the map's own stale float64 text)
specifies canonical decimal strings per the operator's Q-J ruling, and
commits 0/1 shipped double instead - the Rust Hands caught the conflict
before writing a line and stopped; this is the correction the map's
Progress block calls for before Rust starts.
- CultNetFieldPredicate.Number: double? -> string?. New CultNetCanonicalNumber
(CultNetSelection.cs) is the wire's canonical grammar and comparator:
IsCanonical validates ^-?(0|[1-9][0-9]*)(\.[0-9]*[1-9])?$ and excludes "-0"
(the regex alone matches it); Compare orders by sign, then the integer
part's length, then its digits, then the fraction padded on the right to
equal length - a pure string function, no double anywhere on the path.
ValidateField refuses a non-canonical number at the door
(selection_invalid) rather than normalising it.
- CultNetSelectionEvaluator.CompareNumber now takes two canonical strings and
switches on CultNetCanonicalNumber.Compare's sign instead of comparing
doubles directly.
- CultCache.cs: CultDocumentDescriptor.TryGetIndexNumber and
PersistedMember.NumberOf render a member's exact canonical decimal instead
of a double. An integer renders exact (no 2^53 limit); decimal strips
trailing fractional zeros; float/double use .NET's shortest round-trip
ToString(), rewritten out of exponent notation to positional digits
(ExpandScientificNotation) before the shared CanonicalizeDecimalDigits
strips zeros and collapses a negative-zero result to "0"; NaN and infinity
render null (TryGetIndexNumber returns false, matching "matches no
comparison").
- contracts/cultnet/cultnet.selection.schema.json: "number" is now a string
with the canonical pattern (a defence-in-depth (?!-0$) lookahead beyond
what the door enforces in code), not a JSON number.
- Tests: CultNetCanonicalNumberTests (new) pins IsCanonical/Compare directly,
including the door's five named non-canonical spellings, magnitude vs.
lexicographic ordering, sign, integer-part length, and a long past 2^53
against its float64 rounding. CultNetSelectionEvaluatorTests gains
ValidationRefusesNonCanonicalNumberSpellings (+1, 1.0, 01, 1e3, -0) and its
existing Number literals become canonical strings.
CultDocumentSelectionSurfaceTests gains a dedicated FixtureLeafNumbers
fixture and four tests: the 2^53+1 long rendering exactly, a decimal's
trailing zeros stripped, a double's exponent notation rewritten to
positional digits (1e21, 1e-7), and NaN/Infinity answering false.
Mutation entries (scripts/mutate-dotnet.ps1): four new Caching entries
(integer-through-float64, decimal trailing zeros, exponent expansion,
NaN/Infinity) and five new Networking entries (the door's regex, the door's
explicit "-0" exclusion, the comparator's integer-length check, a
lexicographic-whole-string mutant, and the comparator's sign check) - all
KILLED. The two pre-existing S16 (le/ge boundary) entries had to be
re-anchored: their Old text quoted CompareNumber's previous double-based
`left <= right`/`left >= right` spelling, which no longer exists now that
CompareNumber switches on Compare's int result: `cmp <= 0`/`cmp >= 0`.
Verification: dotnet test - GameCult.Networking.Tests 192/192 (was 183 before
these additions), GameCult.Caching.Tests 192/192 (was 188), GameCult.Mesh.Tests
252/253 (one pre-existing unrelated skip, unaffected by this commit - no Mesh
source touched). Caching mutation run: 10/10 KILLED (6 pre-existing + 4 new),
M0 and final both green. Networking mutation run: 15/15 KILLED (10
pre-existing + 5 new, across CultNetSelectionEvaluator.cs, CultNetDatabase.cs
and CultNetSelection.cs), M0 and final both green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/mutate-cultnet-selection-mesh.entries.ps1 covers the two rules moved in
commit 2d against tests/GameCult.Mesh.Tests: ResolveDefaultSelection's "no schema
filter when keys are given" default, and ReadDocumentFromSnapshotResponse's
recordKey-anchored exact read (including the foreign-schema-id decode fallback it
still honors).
M0: green, 87 passed (CultMeshStreamingTests). Verdicts:
- MESH-DEFAULT-Revert: KILLED
- MESH-DEFAULT-Loosening: SURVIVED
- MESH-KEY-Revert: KILLED
- MESH-KEY-Loosening: SURVIVED
- MESH-FOREIGN-Revert: KILLED
- Final: green, 87 passed. All restores byte-exact (working tree clean after the run).
Both SURVIVED entries are recorded as fixture gaps, not swapped for easier
mutants: no test in CultMeshStreamingTests.cs passes an explicit empty (non-null)
recordKeys array, and none varies a record key's case, so nothing distinguishes
MESH-DEFAULT-Loosening's `recordKeys != null` from the correct `recordKeys is
{ Count: > 0 }`, or MESH-KEY-Loosening's OrdinalIgnoreCase from the correct
Ordinal comparison. MESH-FOREIGN has a revert entry only; no distinct, still-wrong
"loosening" of "decode successfully as TDocument" was found that isn't already
one of the other two entries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collapses the rest of the Mesh selector surface named in docs/cultnet-selection-cut.md
section 4/11 onto CultNetSelection:
- CultMeshSnapshotRequestOptions becomes a record carrying Selection in place of
SchemaIds/RecordKeys; its three hand-written clone functions
(CloneSnapshotRequestOptions x2, CloneSnapshotFacadeRequestOptions) are gone in
favor of `with`.
- CultMeshPeerSnapshotDocumentOptions becomes a record for the same reason;
WithPublicationBindingOptions and ToSnapshotRequestOptions collapse to `with` and
one inlined Selection literal at their single call site.
- CleanSnapshotFilter and the three-times "no schema filter when keys are given"
rule collapse into two owners: CultMesh.ResolveDefaultSelection (the typed-document
default) and CultMesh.OverlaySelection (explicit override over defaults).
- CultMeshHotBodySubscription carries one Selection instead of parallel
RecordKeys/SchemaIds allowlists.
- ReadDocumentFromSnapshotResponse's four-tier fallback collapses to one
recordKey-anchored read: exact schema/alias match, or (foreign/runtime-generated
schema id) a payload that decodes as TDocument. The two tiers that matched by
schema alone or recordKey alone - the ones that could return a record under the
wrong key - are gone; DocumentHandle_ReadsRemotePeerSnapshotsAsTypedDocuments
pins the surviving recordKey-anchored decode fallback.
Two items in section 11's table are not cut here, by finding rather than oversight:
- IsSameCultDocumentSchema (CultMesh.cs) already delegates to the one
CultNetSchemaAliasMatching owner (no second engine); deleting the wrapper would
duplicate its type-to-descriptor resolution at four call sites instead of
collapsing it. Left in place.
- The selector literals in CultMeshClient.cs (574-604, 830-860, 932-962) call
CultNetDatabaseSubscriptionClient.SubscribeAsync, which stays a v0
(schemaIds/recordKeys) API in this cut - that client is not named in section 7's
per-file plan. There is no Selection to move these onto without adding a v1
subscribe path to Networking, which is out of this commit's scope. Left in place.
Tests: GameCult.Mesh.Tests 252/253 (1 pre-existing unrelated skip), 0 failed.
GameCult.Networking.Tests 161/161 excluding one pre-existing flaky test
(DatabaseSubscriptionServer_ReconcilesDeliveredProjectionAndBodyDemandWhenAuthorityChanges,
a background RUDP poll thread racing a disposed socket in code this commit does not
touch; it crashes the test host the same way on this branch before this commit too).
GameCult.Caching.Tests 188/188.
Net line count, src/GameCult.Mesh only: +143/-226, net -83. Section 11 estimated
about -326 for this cut's slice. The gap is mostly the two items above (about -49
of the estimate) plus one already-ruled keep this map's Progress block already
named (the foreign-schema-id decode fallback, -41 in the table, kept per the prior
Hands' ruling and untouched here) and the four-tier fallback surviving as a
two-tier one rather than a zero-fallback exact read (an honest test-enforced
requirement, not a padding choice).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Self's ruling (docs/cultnet-selection-cut.md, 2026-09-22) required a mutation
entry for the fourth schema-matching copy's collapse into
CultNetSchemaAliasMatching, landed in commit 2a. Added
NET-D4-FourthMatcher-Revert, pinned by
CultNetDatabase_Creates_ShardSnapshotResponse_ForCompatibleSchemaAlias: killed.
Every pre-existing entry in this file now carries its own File, so the whole
file runs as one invocation against both CultNetSelectionEvaluator.cs and
CultNetDatabase.cs (it previously assumed a single implicit target and would
throw given two). Ran the combined file: 10/10 entries killed, M0 green, both
targets restored byte-exact (verified by git status showing no diff on
either .cs file after the run).
A loosening mutant for the same site
('(requestedSchemaIds != null && requestedSchemaIds.Length == 0) ||', i.e.
reject only an empty-but-non-null requested-schema list) was run by hand and
SURVIVED: CultNetDatabase_Creates_ShardSnapshotResponse_ForCompatibleSchemaAlias
puts exactly one document in the whole cache, so an over-permissive filter
still returns the same single row. Recorded as a comment in the entries file
per the brief ("report it, do not replace it with an easier mutant") rather
than committed as a fake kill or swapped for an easier mutant. Fixing it needs
a second document of a different schema in the shard's fixture; not added
here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CultMesh.IsSameCultDocumentSchema (CultMesh.cs) and the decode-time alias check
in CultMeshSnapshots.DecodeSnapshotDocuments both re-implemented schema-identity
comparison independently of CultNetSchemaAliasMatching, the one matcher landed
in commit 1. Both now call CultNetSchemaAliasMatching.Matches; the private
InferSchemaName duplicated at CultMeshSnapshots.cs is deleted (Matches already
applies the same .vN inference internally).
Spec discrepancy, reported per the Hands brief (map contradicted by the Body):
docs/cultnet-selection-cut.md section 4 lists CultMeshSnapshots.cs:1033-1047
(RawSnapshotPayloadMatchesSchema) and :1078-1088 (InferSchemaName) as deletions,
on the stated reasoning that decode no longer needs a client-side re-filter
because the server's v1 answer is exact. Deleting RawSnapshotPayloadMatchesSchema
outright breaks two existing, still-relevant Mesh tests:
SnapshotHelpers_DecodeSameSchemaAliasFromForeignSchemaId and
SnapshotEndpoint_SyncsCompatibleForeignSchemaIdIntoLocalCache
(CultMeshStreamingTests.cs). Both cover a wire schemaId this runtime cannot
resolve by any alias at all (a foreign/runtime-generated id, e.g.
"runtime.generated.mesh-note.ui.42") where the only way to decode is to trust
the payload's own embedded schemaVersion. That is a decode capability, not a
re-filter of an already-exact server answer, and the map's stated reason for
deleting it does not apply to it. Smallest change keeping the map's intent
true: kept RawSnapshotPayloadMatchesSchema and TryReadSchemaVersion, deleted
only the duplicate InferSchemaName inside it and routed the comparison through
CultNetSchemaAliasMatching.Matches.
Test counts, no regressions: Networking 162/162, Mesh 252/253 (the one
pre-existing skip), Caching 188/188.
Negative grep: MatchesRequestedSchema empty. The one remaining
"MatchesSchema(" grep hit is RawSnapshotPayloadMatchesSchema's own name and
declaration, kept for the reason above.
Still outstanding from the map's Mesh collapse (§4/§11, ~365 gross lines):
the five cloned CultMeshSnapshotRequestOptions-carrying option records, the
"no schema filter when keys are given" rule (x3), CleanSnapshotFilter, the
four-tier ReadDocumentFromSnapshotResponse fallback, and every selector
literal in CultMeshClient.cs/CultMeshBodyDemand.cs/CultMesh.cs peer-snapshot
and body-subscribe construction. Those require CultMeshSnapshotRequestOptions
(and the v0 wire builders that read it) to carry a Selection instead of
separate SchemaIds/RecordKeys — a wider, structural change across Mesh's
public surface that is not done in this commit. Recorded as the next slice.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_projectRecord (CultNetDatabaseSubscriptionServer.cs) reached no production
caller and ran after authorization with no wire consumer. Removed the field,
constructor parameter, and both call sites in Watch() and
CreateProjectedSnapshot(); projection is a value on a v1 selection now, not a
delegate. The two tests that used projectRecord to simulate a record's
identity changing for the reconcile diff
(DatabaseSubscriptionServer_AuthorizesRequestAndFiltersSnapshotAndLiveRecords,
…ReconcilesDeliveredProjectionAndBodyDemandWhenAuthorityChanges) are rewritten
to drive a real rename through the database (delete the old key, put the new
one) instead, so they still prove authorization filtering and the Reconcile()
diff/body-demand behavior.
Self's ruling (docs/cultnet-selection-cut.md, 2026-09-22): the fourth
schema-matching copy at CultNetDatabase.cs, serving CreateShardSnapshotResponse
and SchemaMatchesCatalogFilter (MatchesRequestedSchema + a duplicate
InferSchemaName), collapses into CultNetSchemaAliasMatching.MatchesAny, the one
matcher landed in commit 1.
Networking tests: 162/162 (was 162/162 after commit 1; no count change, two
tests rewritten not added). Negative grep: _projectRecord|projectRecord empty;
MatchesRequestedSchema gone (one remaining substring hit is
RawSnapshotPayloadMatchesSchema in CultMeshSnapshots.cs, the Mesh copy this
commit does not touch — collapses in the next commit).
Mesh collapse (§4/§11 of the map, ~365 gross lines across CultMesh.cs,
CultMeshSnapshots.cs, CultMeshClient.cs, CultMeshBodyDemand.cs) is not in this
commit; it is large enough to build on its own and follows as commit 2b.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/cultnet-selection-cut.md section 2/4/6/7/8: CultNetSelection (schemas,
keys, fields, cites, cited, projection, descending, limit, cursor) with its
door validation (S1), and CultNetSelectionEvaluator: the single-row Matches
fast path (D6) and the full Select (order, hop, cursor, paging). Neither
reflects over a document; both read only through the cache's public surface
(commit 0).
Three v1 messages beside v0 (cultnet.snapshot_request.v1,
cultnet.database_subscribe.v1, cultnet.snapshot_response_raw.v1), their
schema files under contracts/cultnet, registry entries and dispatch arms.
CultNetSchemaAliasMatching goes internal -> public (D4), the one alias
matcher. CultNetDatabase gains LastWriteSequence(schemaId, key), confirmed
at the one append site (AppendMutationLogEntry) that every committed write
takes a shard-log sequence, including a local PutAsync outside any shard.
Three engines deleted, one evaluator wired in their place:
- CultNetDocumentRegistry: CreateRawSnapshotResponse's reflection loop over
cache.AllEntries and MatchesRequestedSchema gone; cache.AllStoredDocuments
replaces the reflection entirely. v0 lowers into a Selection and walks the
evaluator's own pages so a v0 caller still sees its whole matching set
(D3: v0 has no engine of its own). CreateSelectionResponse/SelectPage/
ToRawRecord/CreateSnapshotRequestV1 added.
- CultNetDatabaseServer: CreateChangeMessage/CreateRawDocumentRecord/Matches/
MatchesSchema/InferSchemaName (122 lines) deleted; CreateSubscription lowers
v0 into a Selection and matches through the evaluator.
- CultNetDatabaseSubscriptionServer: CreateAuthorizedChange/CreateChange/
CreateChangeCore/ResolveWireSchemaId/MatchesRequestedSchema/CreateRawRecord
(92 lines) deleted, replaced by CreateMatchedRecord. _projectRecord and
CreateProjectedSnapshot's collapse are commit 2's, per the map's own
deletion line, and are untouched here.
Discrepancy found and fixed (not in the map): CultNetDocumentBinding can
carry a schema id different from its descriptor's own
(CultNetDocumentBinding.ForDocument's schemaId override), which the old
per-engine matchers checked and a descriptor-only evaluator cannot see by
design (it stays decoupled from CultNetDocumentRegistry). Fixed at the two
call sites and in CultNetDocumentRegistry.SelectPage by resolving the
binding's schema id into the effective selection before evaluation
(CultNetSchemaAliasMatching.WithBindingSchemaAlias,
CultNetDocumentRegistry.ExpandSchemaBindingAliases), not in the evaluator.
Found by DatabaseSubscription_FiltersLiveChangesByWireSchemaBinding timing
out; both call sites now pass.
Second discrepancy, reported not fixed: CultNetDatabase.cs:1707 has a fourth,
undocumented copy of schema-matching logic (MatchesRequestedSchema, private,
serving CreateShardSnapshotResponse), which the map's section 1 audit did not
name among "three separate selector engines". Left untouched this commit:
it is out of the map's explicit deletes list and touching it risks shard
snapshot behavior with no budgeted verification.
Verification: dotnet test tests/GameCult.Networking.Tests - 162 passed (150
existing + 12 new), 0 failed; the seven map-named tests all still pass, two
rewritten for the new signatures (CreateChangeMessage now takes
CultNetSelection; CreateChange/CreateAuthorizedChange collapsed into
CreateMatchedRecord, returning CultNetRawDocumentRecord? directly).
tests/GameCult.Mesh.Tests - 252 passed, 1 pre-existing skip, 0 failed (no
Mesh source touched this commit). tests/GameCult.Caching.Tests - unaffected
(188, unchanged from commit 0).
Negative greps: GetProperty|GetField|GetCustomAttribute in
GameCult.Networking - only against the generic `object change` dispatch
(CultNetDatabaseChange<T>) and CultRecordHandle<T> construction, pre-existing
and out of this cut's scope, never against a document's declared values.
_projectRecord survives in CultNetDatabaseSubscriptionServer.cs (expected,
commit 2). CultNetSchemaAliasMatching is the only schema matcher outside the
one reported discrepancy.
scripts/mutate-dotnet.ps1: the caching mutation runner generalized into a
shared dotnet/NUnit harness (Target/TestProject/Entries/ControlFilter),
replacing scripts/mutate-cultnet-selection-caching.ps1 with an entries file
of the same six mutations (re-verified: all six still KILL). It is now
generic to any NUnit project in this repo, not only this campaign's two
targets - a candidate to sit beside tools/eureka-mutations.ps1 as the
dotnet-side counterpart. Nine new networking entries (S3, S5 x2, S7 x2,
S16 x2, S18) all KILLED. Not yet mutation-run: S1, S2, S6, S23 (pinned by
tests, not by a committed mutant); S9-S15 (subscription reconcile lowering,
v0 no-engine-of-its-own); S17, S20 (pinned by tests only). S19/S21/S22 are
cache-layer, covered in commit 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/cultnet-selection-cut.md section 6/7/10: the reflection loop at
CultNetDocumentRegistry.cs:325-342 exists only because nothing outside
GameCult.Caching can read a declared index value, reference, or numeric
member (Members/IndexAccessors are internal, no InternalsVisibleTo to
Networking). This adds the one public door instead: CultDocumentMemberView,
CultDocumentDescriptor.DeclaredMembers/TryGetIndexValue/TryGetIndexNumber/
ReferencesOf, and CultDocumentRegistry.ResolveTargetLeaves (D9, computed live
against the registry, never cached or persisted).
Two registration refusals land beside DiscoverMembers' existing
DuplicateSlotMessage/HiddenMemberMessage: D10 renames the ArgumentException
ToDictionary already threw for two differently-named members sharing one
index alias into a rejection that names the schema, both members and the
alias; D11 refuses a reference member whose CLR shape the cache cannot walk
(many:false needs CultRecordRef<T>, many:true needs
IEnumerable<CultRecordRef<T>> or IDictionary<CultRecordRef<T>, V>) instead of
silently answering it as unreadable later.
GameCult.Networking is untouched; the evaluator that will read this surface
does not exist yet (commit 1).
Verification: dotnet test tests/GameCult.Caching.Tests - 188 passed (176
existing + 12 new), 0 failed. New tests: TryGetIndexValue/TryGetIndexNumber
reading an inherited alias; ReferencesOf over a single/many-list/many-dictionary
reference, including two dictionary entries with different payload bytes
(S19); ResolveTargetLeaves against an abstract target with no [CultDocument]
of its own (D9), including a leaf registered after an earlier descriptor was
built; IsNumeric agreement for a nullable numeric and a decimal member (S22);
the two new registration refusals, dynamically emitted so registry discovery
by attribute scan cannot see the rejected shapes.
scripts/mutate-cultnet-selection-caching.ps1: a dedicated dotnet/NUnit
mutation runner (the shared tools/eureka-mutations.ps1 harness only speaks
cargo; scripts/mutate-cultmesh.mjs is the QUIC campaign's file and this
campaign does not edit it, per the map's section 10). Byte-exact restore
verified by SHA-256, a sidecar for crash recovery, M0 no-op control, one
anchor match required per entry. Six entries (D8/S22, D10, D11 - revert and
loosening each) all KILLED; final restore verified green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`*.sh text eol=lf` reaches new clones only. A clone checked out before the
rule keeps scripts/build-quic-native.sh with carriage returns after pulling,
because the blob did not change: git neither rewrites the file nor reports it
modified, and the container still dies on the interpreter `bash\r`.
The README now gives the repair where a newcomer meets the container path,
and .gitattributes points to it. Verified on clones made at 367c9bc and moved
to c25de5a, from Git Bash and from Windows PowerShell 5.1: 109 CR bytes
before, none after, status clean. The line uses `;` so it runs in both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1.0.60 removed public API (CultInspectorAssetPath) under a patch bump; its
own changelog says "Breaking" and the version number lied. Operator ruling:
semver is policy, strictly, for every published package family.
docs/semver-policy.md states what counts as breaking per surface (managed
API, Unity inspector attributes, CultCache wire format/store compatibility,
CultNet protocol, native bridge exports), the 0.y.z pre-1.0 convention, tag
naming, and required changelog shape. Records the 1.0.60 scar as history;
does not renumber it.
scripts/check-changelog-semver.mjs is the mechanical check: refuses a
changelog "### Breaking" section under too small a bump, and refuses a
version that skips, reverses, or repeats the previous published tag. Skips
re-litigating a version that is already tagged (rebuilding immutable
history is not a release action). Wired into publish-packages.yml for the
four npm/PyPI packages, and into build-unity-package.ps1 (cultlib),
packages/cultmath/scripts/build-unity-package.ps1 (cultmath), and a new
scripts/verify-caching-unity-release.ps1 (caching.unity has no build script
of its own to hook into).
18 node:test cases cover the required table (breaking+major/minor/patch,
non-breaking, 0.y.z, skip, downgrade) plus CLI-level git-tag resolution.
Hand-mutated each rule (bump classification, pre-1.0 lane requirement,
breaking-heading detection, breaking early-return, already-published skip)
and confirmed each kill, then restored byte-exact. No mutation harness is
committed; CultLib has none today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul S1: the 1.0.60 entry claimed the OpenSSL MsQuic bridge had shipped
since 1.0.59; git says both blobs first appear in this release. S2: the
package build staged package.json and README only, so the produced package
and the tracked template differed by the changelog.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultAssetGuidKey.cs (and the test file added in 0b88706... no, 6c7cfa7) had a
literal LF working-tree checkout under core.autocrlf=true / text=auto, so a
build run from a stale bypassed-checkout copy of this worktree embedded a
source checksum a fresh clone's CRLF checkout would never reproduce. The
tracked git object was already normalized; only the working-tree bytes in
this worktree were wrong. Restored the CRLF checkout (git add reports no
diff against the index for CultAssetGuidKey.cs itself), then rebuilt with
scripts/build-unity-package.ps1 -UpdateTemplate. GameCult.Caching(.pdb) and
every managed plugin that depends on it (GameCult.Caching.MessagePack,
GameCult.Mesh, GameCult.Mesh.Quic.Native, GameCult.Networking,
GameCult.Networking.WebSockets) picked up new MVIDs/PDB checksums as a
result; the native gamecult_mesh_quic_native.dll/msquic.dll are untouched
since the native build did not run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
*.meta is gitignored repo-wide; every tracked Unity .meta file, these two
included, is force-added on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultLib 1.0.60: CultInspectorAssetPath removed for CultInspectorAssetGuid +
CultAssetGuidKey (breaking for inspector annotations), and the native QUIC
bridge's Schannel to OpenSSL MsQuic 2.5.9 static-link change shipping since
1.0.59. Caching Studio 1.4.0: the GUID picker, sub-asset keys, built-in
refusal, and the stale-value warning/Clear button. build-unity-package.ps1
only stages files it names explicitly, so it neither drops nor overwrites
these; no script change needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TrimEnd('0','f') missed Library/unity default resources built-ins whose GUID
has no all-zero or all-f suffix (e.g. 0000000000000000e000000000000000, the
LegacyRuntime font / Cube mesh in 6000.3.24f1). Ask AssetDatabase instead: a
real project asset is always a main or sub-asset under Assets/ or Packages/;
built-ins are neither. Also add a Clear button beside the stale-value warning
so a non-resolving stored key can be cleared without picking a substitute
asset first (drawing still never rewrites a stored value on its own).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul flagged (F3) two docs still describing the pre-release native QUIC
bridge: unity/org.gamecult.cultlib/README.md said Schannel, and
native/GameCult.Mesh.Quic.Native/README.md still described the older
40,448-byte committed plugin as current. The shipped bridge (since the
1.0.60/1.4.0 release, commit 0155ba8) is the OpenSSL MsQuic 2.5.9 build,
302,080 bytes, static CRT, no CRT import. Both docs now describe that build
and name what it replaced.
The OpenSSL build's NuGet package carries an Apache-2.0 notice for OpenSSL
itself (Microsoft.Native.Quic.MsQuic.OpenSSL's THIRD-PARTY-NOTICES) that the
committed Third Party Notices folder never carried - the Schannel build had
no such dependency to notice. build-quic-native.ps1 now copies it out
alongside MSQUIC-LICENSE.txt, build-unity-package.ps1 stages it into the
package output and the tracked template, and it is now committed as
OPENSSL-NOTICE.txt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ruling S (2026-09-17): a stored [CultInspectorAssetGuid] value is the
Addressables key form itself, the bare GUID for a main asset or
"guid[subAssetName]" for a picked sub-asset (one sprite of a sheet, say) -
the same shape AssetReference keeps and Addressables.LoadAssetAsync<T>
accepts.
CultAssetGuidKey (engine-free, GameCult.Caching) pins the parse/format rule
with unit tests (bare guid, guid[name], names with brackets/spaces,
malformed input), so headless checks can share it without touching Unity.
The Studio drawer's AssetGuid branch now: resolves either key form on read;
refuses to store a built-in resource's GUID (it has no Addressables entry)
and logs why instead of silently writing nothing; shows a warning next to a
stored value that fails to parse or resolve instead of drawing it as an
ordinary empty field, and never rewrites that stale value on its own.
Rebuilt GameCult.Caching/GameCult.Caching.MessagePack plugin DLLs
(scripts/build-unity-package.ps1 -UpdateTemplate) since the new type ships
in GameCult.Caching. Compiled CultCacheStudioDrawers.cs and
CultCacheStudioWindow.cs against Unity 6000.3.24f1's editor reference set
plus these plugins in a scratch csproj (removed after): 0 errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing the public asset path attribute is breaking. Runs
scripts/build-unity-package.ps1 -UpdateTemplate to refresh the tracked
plugin DLLs/pdbs in unity/org.gamecult.cultlib/Runtime/Plugins, and bumps
unity/org.gamecult.cultlib/package.json to 1.0.60 and
src/GameCult.Unity/Assets/Caching/package.json to 1.4.0 (dependency bumped
to cultlib 1.0.60). Tags are the next, separate step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultInspectorAssetPathAttribute stored an engine asset path, which breaks
when the asset moves. CultInspectorAssetGuidAttribute stores the asset's
stable identity (in Unity, the .meta GUID) instead. The Studio drawer now
resolves the stored GUID to a path only to load the preview object, and
writes back AssetPathToGUID on a change. No path form survives for the
next field to pick up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last report quoted the restored sha256 as though two runs agreeing on it
meant they had run the same source. They had not: `text=auto` gives a Windows
checkout carriage returns, so the win32-x64 run restored to b163cab7 and the
linux-x64 run to e3c6528a from the same commit. The digest is a restore check —
the file this run started with is the file it ended with — and it is said where
it is printed, so the next report does not borrow it for something it cannot do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note beside the close's wake claimed the narrowing to `notify_one` was
equivalent, and gave the weaker half of the reason. The equivalence needs two
premises together: `closing` is set permanently under the gate before the notify
and appears in every waiter's predicate, so after a close begins no poller can
block at all and the set of waiters is fixed; and each exiting call's own scope
destructor takes the gate, decrements and wakes everyone, so one wake starts a
chain that reaches all of them. Neither carries it alone. Both are now written
where the claim is made, so it can be checked rather than believed.
Narrowing both wakes also survives, and that is not equivalent by any argument:
a wake consumed by the closer, waiting on the same variable for the count to
reach zero, leaves the chain stopped and the rest of the pollers sitting out
their timeouts. It passes on wait-queue ordering — the pollers are queued before
the closer, so a FIFO head is never the closer until it is the only waiter left.
Recorded as unreached, with the reason, and deliberately not as unreachable:
that claim has been wrong twice in this campaign. A scenario cannot force it
while the first premise holds, since the only way to put the closer ahead of a
poller in that queue is a poller that blocks after the close began. Reaching it
would need a seam that chooses which waiter a notify goes to. The standard does
not promise FIFO, so what survives here is the implementation's doing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last pass's only non-comment source change was held by nothing. Reverting the
hold's guard so it fires unconditionally survived all five scenarios, because no
scenario ever had a call in both states at once: the timeout scenario never arms
the hold, and the hold scenarios never let a timeout expire.
`holdtimeout` is the missing state. The hold is armed, the runtime is idle, and
one poll asks for 150 ms while the hold stays armed for 800. A call the wait woke
is the seam's to park; a call whose own timeout expired is on its way out, and
parking it would let `holdclose` report the bridge keeping a host call inside the
library when what kept it was the fixture. The measurement is which of those two
numbers the poll comes back at.
Two entries, and the loosening is the one worth reading: the same two conditions
in the other order. The hold blocks, so the order is the rule — asked first, it
parks the call and then decides whether it should have. Both die on
`holdtimeout` at about 800 ms and on nothing else; `holdclose` and `latecall`
stay green under both, which is the point.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two bands ruled out a constant and ruled out nothing else. Any mapping that
is the identity at 200 and 900 passed for free, and the late tolerance was a flat
300 ms, so anything within that of the truth passed at every value forever.
Three ordinary ones survived all five scenarios on both targets: clamping the
wait to at most a second, adding 150 ms to it, and scaling it by six fifths.
The clamp is the one that matters. Its failure is verbatim the bug the last pass
claimed to fix — a host asking for five seconds gets one, forever, with nothing
reporting it — and "cap the wait so a shutdown gets noticed" is the most ordinary
spelling that line will ever be given.
So the long probe goes to 5000, above any ceiling a bridge would plausibly clamp
to, which kills the clamp and the scale on the early check and the late one. And
the late tolerance becomes a fraction of what was asked for — 60 ms plus an
eighth — which is what kills the offset: 150 ms is 75% late at the 200 ms probe
and there is no flat gap left to hide in. Measured overshoot on either target is
about 10 ms, against 85 ms allowed at the short probe and 685 at the long.
The table gets the clamp as an entry of its own, because the whole lesson here is
that a table of constants says nothing about a wait computed from the argument,
and a table of constants would go green again the moment these numbers drifted
back. Its honest limit is stated where the tolerance is defined: a scaling
smaller than an eighth survives, and nothing here claims otherwise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The container recipe carried a PowerShell backtick continuation in a snippet
whose next paragraph invites a POSIX shell to run it, where a backtick is not a
continuation. A continuation has to pick a shell, so the line does not have one.
The Dockerfile's own copy of the recipe still mounted one machine's checkout by
absolute path; it mounts whatever checkout is running it, like the other two.
And the requirement a newcomer meets by tripping over it: on win32-x64 the
harness needs the checkout somewhere short. MSBuild's file tracker gives out on
long paths, so a run from a deep temporary directory fails the build for every
mutant, the no-op control included. That is loud — the harness stops on a red
control and prints what the child said — but what the child says is MSBuild, not
the path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`* text=auto` and nothing else, so a Windows clone writes every shell script with
carriage returns. The documented Linux path is a container that runs one of them,
and it dies in under a second with a complaint about an interpreter named
`bash\r`. Nothing in that failure names line endings, and the same clone builds
and tests fine everywhere else, so it reads as a broken image.
`scripts/build-quic-native.sh` is the only shell script in the repository and the
only file on those paths that an interpreter reads by shebang; the mutation
runner is Node and handles carriage returns itself, and the Dockerfile builds
either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No constant sits in both bands, which is true and is not the rule. A wait that
caps itself at a second passes every scenario, and capping a wait so shutdown
gets noticed is the most ordinary way that line will ever be written. An offset
and a scale survive beside it.
A fresh Windows clone also cannot run the documented Linux path at all, because
nothing tells git to leave shell scripts alone and the container dies on a
carriage return in the interpreter line. The mount fix works; this sat behind
it.
The equivalence holds, but on two premises nobody had written down, and
narrowing both wakes survives while being equivalent to nothing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The blind spot was not where the hold sat. No scenario ever let a timeout
finish a poll, because both ended their waits with a close, so the duration
governed nothing anyone could see. A scenario that polls an idle runtime and
measures elapsed against what it asked kills all three constants, and it asks
twice at values whose bands do not overlap, because one value cannot tell a
bridge honouring the host from one waiting on that same number of its own.
The probe that reached the two-phase poll through a closed port is committed,
the wake finally has the entry the table had been claiming, and a surviving
mutant is argued equivalent and handed to Soul rather than swapped for an
easier one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three rules the bridge has always promised, none of which anything committed
could see.
The timeout was the structural one. Both scenarios end their waits with a close,
so the duration the bridge waits on governed nothing either of them observes: a
hard-coded 2000 ms survived the whole Linux matrix and Windows, and a host asking
for five seconds would have got two forever with nothing reporting it. The fix is
not another entry at another constant. A timeout's only observable is how long
the call takes, so `polltimeout` lets an idle runtime's poll be ended by its own
timeout and measures it, twice at different values, because no single constant
sits in both bands. The seam's hold moves behind that too: it parks only a call
the wait woke, so a poll whose own timeout expired is on its way out rather than
held there by the fixture and counted as the bridge's doing.
The refusal during a close (the header's section 4) had no scenario at all. A
call admitted after the closer has begun increments the in-flight count behind a
wait that has already read it. `latecall` holds the close open with the seam and
makes a call into it.
And the two-phase poll was written off as needing a listener, a credential and a
connection. It needs a closed loopback port: the failure always carries a reason,
which is an 87-byte payload, in about a millisecond. Moving the pop above the
copy crashes on both targets.
18 killed, 0 survived, 0 skipped on linux-x64; 16 killed, 0 survived on
win32-x64, where the two ThreadSanitizer entries are honestly skipped. Both runs
restored the source to the same sha256, b163cab7d48ebc93.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mount was this machine's own path, so the one documented way to build and
check the Linux bridge needed editing before it would run anywhere else. It is
the repository root now, named as the shell names it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reference whose target contradicts its declaration is noticed on read in this
cut and refused by name. Writing the same check into the put path is intended
rather than optional: the declaration already names the target and the writer
already holds the record, so it is cheap, and leaving it to consumers means
every tool that writes records reimplements it and a tool that forgets looks
exactly like one that did not until something refuses.
The consumer story rests on less than it sounds. There is no Studio repository
and no project by that name; the tooling that enforces this today is one
console project in one game. That makes now the cheapest moment to move the
rule to its owner.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A generator runs against published schemas, and the published schema entry is
flat: the cache walks the base types to collect members and then throws the
hierarchy away. Fed that, a generator emits one flat class per leaf with the
inherited members copied into each, which is the opposite of building the
hierarchy up for you.
So the catalog carrying each leaf's base chain is a prerequisite of that
campaign rather than a nicety for naming families, and families fall out of it
once it exists. The campaign itself waits until this cut settles the shape it
would be generating against. No generator exists in the repository today.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A caller needing exact pairs asks once per schema. The limit is stated rather
than answered with a third addressing list.
Naming a family is parked, but not because there is nothing to name. In one
process a family is an assignability check; over the wire there is no type
system, and nothing publishes the hierarchy where a caller can see it. The fix
is a catalog entry naming each leaf's base chain, which is small work that
moves the catalog's shape, so it belongs in a cut already touching it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Floats are fragile, and every value in this vocabulary is already a string, so
decimal strings keep the wire to one value encoding rather than adding a
second. The recommendation this reverses had that backwards.
What it costs is a comparison rule, and that is where the danger sits: compared
as text, ten is less than nine. The cut owes a canonical decimal form and a
numeric comparison, both pinned, and it no longer owes anyone a sentence about
where exactness runs out.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A stranger can now rerun both targets from the committed container and the
README, which was the batch's point and holds. The rules are another matter.
The scenario holds its callers after the wait whose duration is the rule, so
a hard-coded two seconds walks through the whole matrix while the committed
entry only ever died because one constant was smaller than another.
The gap recorded as unreachable is reachable in fifteen lines against a closed
port, and the mutant crashes a plain optimised build on contact. Second gap
falsified today. The next batch fixes the cause once instead of adding another
entry at another constant.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The operator ruled ordinals, the two deletions and the leaf titles earlier
today, in the other campaign's map. This one still carried them as open, and
a stale live question is exactly what a later agent reads as a decision still
to make.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>