Commit graph

964 commits

Author SHA1 Message Date
Meta Vens
3317dddf46 CultNet selection cut1: the shared C#/Rust parity fixture
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>
2026-09-22 11:33:29 +02:00
Meta Vens
a088435f9f CultNet selection cut1 Rust fix batch: mutation entries for R-B/D/E/F/G/H/I
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>
2026-09-22 10:11:31 +02:00
Meta Vens
4f81c255a3 CultNet selection cut1 Rust fix batch: R-M deletions
- 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>
2026-09-22 10:11:31 +02:00
Meta Vens
8b114ed353 CultNet selection cut1 Rust fix batch: R-B/C/D/E/F/G/H/I
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>
2026-09-22 10:11:31 +02:00
Meta Vens
3d32c67821 CultNet selection cut1 fix batch: regenerate the C#-written parity vectors
Regenerated contracts/cultnet/interop/selection-vectors.cs-written.json
under R-B/R-C/R-D/R-E/R-I.

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:40:18 +02:00
Meta Vens
c01bc0a129 CultNet selection cut1 fix batch: R-L (contract, Python, mutate-dotnet.ps1)
- 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>
2026-09-22 09:39:53 +02:00
Meta Vens
915262fee2 CultNet selection cut1 fix batch: R-I/K/D tests and R-A/R-M reflection fix
- 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>
2026-09-22 09:39:36 +02:00
Meta Vens
790385364c CultNet selection cut1 fix batch: R-A/B/C/D/E/F/G/H/J/M (C#)
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>
2026-09-22 09:39:04 +02:00
Meta Vens
00f4c02dd7 CultNet selection cut1, commit 4: the parity vectors, and the ruling this branch landed under
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>
2026-09-22 06:55:52 +02:00
Meta Vens
021e3f1bba CultNet selection cut1, commit 2 fix batch: R-2, Python refuses v1 selection messages
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>
2026-09-22 06:13:16 +02:00
Meta Vens
568e8e356f CultNet selection cut1, commit 2 fix batch: door, shard-via-evaluator, Mesh v0 refusal, dead Reconcile code, mutation harness crash handling
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>
2026-09-22 06:13:15 +02:00
Meta Vens
b090ca97f9 CultNet selection cut1, commit 3: the Rust runtime
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>
2026-09-22 05:31:19 +02:00
Meta Vens
e5348d3d57 CultNet selection cut1, commit 2f: the Q-J fix (canonical decimal numbers)
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>
2026-09-22 04:18:22 +02:00
Meta Vens
4c33a62b29 CultNet selection cut1, commit 2e: mutation entries for the Mesh collapse
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>
2026-09-22 03:54:36 +02:00
Meta Vens
5ba48f0ebe CultNet selection cut1, commit 2d: the CultMesh collapse
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>
2026-09-22 03:38:56 +02:00
Meta Vens
4167cc4f24 CultNet selection cut1, commit 2c: mutation entry for the fourth matcher's collapse
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>
2026-09-22 03:05:34 +02:00
Meta Vens
c5b03cb455 CultNet selection cut1, commit 2b: collapse IsSameCultDocumentSchema into the one alias matcher
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>
2026-09-22 02:54:53 +02:00
Meta Vens
0787608cef CultNet selection cut1, commit 2a: delete _projectRecord, collapse the fourth schema matcher
_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>
2026-09-22 02:44:23 +02:00
Meta Vens
17d10e017f CultNet selection cut1, commit 1: the shape, the evaluator, v1 messages
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>
2026-09-22 02:31:42 +02:00
Meta Vens
e420410fe9 CultNet selection cut1, commit 0: the cache's public read surface
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>
2026-09-22 01:57:35 +02:00
Meta Vens
b3d9cf7821 Tell an existing Windows clone how to get its build script back to LF
`*.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>
2026-09-22 01:03:51 +02:00
Meta Vens
c25de5a895 Record Soul's sixth pass: the wait's predicate is pinned by nothing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:50:29 +02:00
Meta Vens
7cd5a2b63f Record the fifth fix batch landing and dispatch Soul's sixth pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:23:24 +02:00
Meta Vens
fa327a0627 Name the CultMesh MCP bridge as a priority campaign
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:37:00 +02:00
Meta Vens
089859deb4 Write and enforce the semver policy, per operator ruling on 1.0.60
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>
2026-09-17 22:03:22 +02:00
Meta Vens
45c2f4006f Say the OpenSSL bridge ships with this release, and pack the changelog caching-unity-v1.4.0 cultlib-unity-v1.0.60
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>
2026-09-17 20:40:12 +02:00
Meta Vens
ef81c96094 Renormalize CultAssetGuidKey.cs to CRLF and rebuild dependent plugin DLLs
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>
2026-09-17 20:12:52 +02:00
Meta Vens
9847a96070 Track the new CHANGELOG.md.meta files (force-added like other tracked .meta)
*.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>
2026-09-17 20:11:48 +02:00
Meta Vens
0b8870677e Add CHANGELOG.md to both Unity packages (UPM convention)
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>
2026-09-17 20:11:33 +02:00
Meta Vens
6c7cfa7a94 Studio: refuse built-ins by asset identity, not GUID digits; add Clear for stale keys
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>
2026-09-17 20:09:53 +02:00
Meta Vens
0c9cb57bea Correct stale Schannel/40,448-byte claims, ship OpenSSL's license notice
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>
2026-09-17 19:49:14 +02:00
Meta Vens
1e398e0116 Studio: read and write sub-asset Addressables keys, warn on stale values
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>
2026-09-17 19:48:45 +02:00
Meta Vens
0155ba8cca Prepare 1.0.60/1.4.0 release: rebuild plugin DLLs, bump package.json
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>
2026-09-17 18:46:07 +02:00
Meta Vens
95a3c32250 Replace the asset path attribute with a stable GUID identity
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>
2026-09-17 18:43:03 +02:00
Meta Vens
244154e2f5 Say what the restore digest is, since it is not an identity
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>
2026-09-17 03:11:05 +02:00
Meta Vens
b985f07887 Say what the single-waiter wake actually rests on, and what is still unreached
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>
2026-09-17 02:47:34 +02:00
Meta Vens
5ab4c627d7 Put a call in both states so the seam's own guard is pinned
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>
2026-09-17 02:46:49 +02:00
Meta Vens
c682619fbd Close the timeout against waits derived from the argument, not only constants
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>
2026-09-17 02:45:57 +02:00
Meta Vens
c177533b4f Offer the container line to both shells, and name the short-path requirement
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>
2026-09-17 02:44:47 +02:00
Meta Vens
ca6c0f5852 Keep shell scripts LF so a Windows clone can run the Linux path
`* 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>
2026-09-17 02:43:37 +02:00
Meta Vens
367c9bc2b8 Record the pass that beat the bands with a clamp
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>
2026-09-17 02:36:37 +02:00
Meta Vens
1845301f91 Record the batch that made the timeout observable
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>
2026-09-17 02:12:12 +02:00
Meta Vens
a937a0b07e Let the bridge's own timeout, refusal and two-phase poll be seen
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>
2026-09-17 02:09:50 +02:00
Meta Vens
25d7de26b3 Point the container recipe at whatever checkout is running it
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>
2026-09-17 02:09:12 +02:00
Meta Vens
122c2172d9 Rule the reference check onto read now and write later
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>
2026-09-17 01:44:09 +02:00
Meta Vens
2df124e269 Name what the generator needs before it can exist
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>
2026-09-17 01:38:24 +02:00
Meta Vens
34df099e8c Rule the cross product, and park families for the right reason
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>
2026-09-17 01:30:45 +02:00
Meta Vens
6e4b9e7eb4 Rule numbers onto decimal strings
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>
2026-09-17 01:23:44 +02:00
Meta Vens
fa6c000e7f Record the pass where the verdict splits
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>
2026-09-17 01:19:02 +02:00
Meta Vens
47618428a3 Close three questions the map still lists as open
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>
2026-09-17 01:13:41 +02:00