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>
This commit is contained in:
parent
b090ca97f9
commit
568e8e356f
14 changed files with 586 additions and 174 deletions
|
|
@ -251,6 +251,9 @@ namespace GameCult.Networking
|
|||
? CreateProjectedSnapshot(request, key.Peer)
|
||||
: ProjectedSnapshot.Empty;
|
||||
|
||||
// With _projectRecord gone (docs/cultnet-selection-cut.md, S2-2), a projection's dictionary
|
||||
// key is always the record's own RecordKey/SchemaId, so the two can never disagree here;
|
||||
// the identity branch that used to catch a re-projected key is dead and deleted.
|
||||
foreach (var previous in projection.DeliveredBySourceRecordKey.ToArray())
|
||||
{
|
||||
if (!next.BySourceRecordKey.TryGetValue(previous.Key, out var current))
|
||||
|
|
@ -258,13 +261,7 @@ namespace GameCult.Networking
|
|||
SendRemoval(key.Peer, key.Id, previous.Value);
|
||||
continue;
|
||||
}
|
||||
if (!string.Equals(previous.Value.RecordKey, current.RecordKey, StringComparison.Ordinal) ||
|
||||
!string.Equals(previous.Value.SchemaId, current.SchemaId, StringComparison.Ordinal))
|
||||
{
|
||||
SendRemoval(key.Peer, key.Id, previous.Value);
|
||||
SendUpsert(key.Peer, key.Id, current, added: true);
|
||||
}
|
||||
else if (!Equivalent(previous.Value, current))
|
||||
if (!Equivalent(previous.Value, current))
|
||||
{
|
||||
SendUpsert(key.Peer, key.Id, current, added: false);
|
||||
}
|
||||
|
|
@ -299,16 +296,15 @@ namespace GameCult.Networking
|
|||
SchemaIds = request.SchemaIds,
|
||||
RecordKeys = request.RecordKeys
|
||||
});
|
||||
// The snapshot is one row per record key by construction, so the duplicate-key throw this
|
||||
// used to guard can no longer fire (docs/cultnet-selection-cut.md, S2-2); deleted rather
|
||||
// than tested around.
|
||||
var bySourceRecordKey = new Dictionary<string, CultNetRawDocumentRecord>(StringComparer.Ordinal);
|
||||
var matchedRecordKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var source in snapshot.Documents)
|
||||
{
|
||||
var sourceRecordKey = source.RecordKey;
|
||||
if (_authorizeRecord?.Invoke(request, peer, sourceRecordKey, source.SchemaId) == false)
|
||||
continue;
|
||||
if (!matchedRecordKeys.Add(source.RecordKey))
|
||||
throw new InvalidOperationException(
|
||||
$"Database subscription snapshot produced duplicate record key '{source.RecordKey}'.");
|
||||
bySourceRecordKey[sourceRecordKey] = source;
|
||||
}
|
||||
return new ProjectedSnapshot(bySourceRecordKey);
|
||||
|
|
@ -337,14 +333,8 @@ namespace GameCult.Networking
|
|||
SendUpsert(peer, subscriptionId, current, added: true);
|
||||
return;
|
||||
}
|
||||
if (!string.Equals(previous.RecordKey, current.RecordKey, StringComparison.Ordinal) ||
|
||||
!string.Equals(previous.SchemaId, current.SchemaId, StringComparison.Ordinal))
|
||||
{
|
||||
SendRemoval(peer, subscriptionId, previous);
|
||||
projection.DeliveredBySourceRecordKey[sourceRecordKey] = current;
|
||||
SendUpsert(peer, subscriptionId, current, added: true);
|
||||
return;
|
||||
}
|
||||
// Same S2-2 fact as Reconcile: previous/current are looked up by the record's own key, so
|
||||
// they can never disagree in RecordKey/SchemaId here either.
|
||||
if (!Equivalent(previous, current))
|
||||
{
|
||||
projection.DeliveredBySourceRecordKey[sourceRecordKey] = current;
|
||||
|
|
|
|||
Loading…
Reference in a new issue