CultLib/src/GameCult.Networking/CultNetDatabaseSubscriptionServer.cs

745 lines
35 KiB
C#
Raw Normal View History

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Net;
using System.Threading.Tasks;
using GameCult.Caching;
using R3;
namespace GameCult.Networking
{
/// <summary>
/// Publishes live database changes through any schema-v0 server transport.
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
/// Selection match runs after authorization and owns both initial
/// snapshot and live-update delivery for a peer. Projection (header vs.
/// document) is a value on the request's selection, not a delegate.
/// </summary>
public sealed class CultNetDatabaseSubscriptionServer : IDisposable
{
private readonly ICultNetSchemaServer _server;
private readonly CultNetDatabase _database;
private readonly Func<CultNetDatabaseSubscribeMessage, ICultNetSchemaServerPeer, Task> _subscribe;
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
private readonly Func<CultNetDatabaseSubscribeV1Message, ICultNetSchemaServerPeer, Task> _subscribeV1;
private readonly Func<CultNetDatabaseUnsubscribeMessage, ICultNetSchemaServerPeer, Task> _unsubscribe;
private readonly Func<CultNetDatabaseSubscribeMessage, ICultNetSchemaServerPeer, bool>? _authorizeRequest;
private readonly Func<CultNetDatabaseSubscribeMessage, ICultNetSchemaServerPeer, string, string, bool>? _authorizeRecord;
private readonly ICultNetSchemaServerPeerLifecycle? _peerLifecycle;
private readonly ConcurrentDictionary<SubscriptionKey, IDisposable> _subscriptions =
new ConcurrentDictionary<SubscriptionKey, IDisposable>(SubscriptionKeyComparer.Instance);
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
private readonly ConcurrentDictionary<SubscriptionKey, SubscriptionRequest> _requests =
new ConcurrentDictionary<SubscriptionKey, SubscriptionRequest>(SubscriptionKeyComparer.Instance);
2026-08-19 13:55:03 +02:00
private readonly ConcurrentDictionary<SubscriptionKey, SubscriptionProjectionState> _projections =
new ConcurrentDictionary<SubscriptionKey, SubscriptionProjectionState>(SubscriptionKeyComparer.Instance);
private readonly object _lifecycleGate = new();
private bool _disposed;
/// <summary>Attaches live database subscription handlers to a schema server.</summary>
public CultNetDatabaseSubscriptionServer(
ICultNetSchemaServer server,
CultNetDatabase database,
Func<CultNetDatabaseSubscribeMessage, ICultNetSchemaServerPeer, bool>? authorizeRequest = null,
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
Func<CultNetDatabaseSubscribeMessage, ICultNetSchemaServerPeer, string, string, bool>? authorizeRecord = null)
{
_server = server ?? throw new ArgumentNullException(nameof(server));
_database = database ?? throw new ArgumentNullException(nameof(database));
_authorizeRequest = authorizeRequest;
_authorizeRecord = authorizeRecord;
_subscribe = HandleSubscribeAsync;
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
_subscribeV1 = HandleSubscribeV1Async;
_unsubscribe = HandleUnsubscribeAsync;
_server.OnCultNet(_subscribe);
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
_server.OnCultNet(_subscribeV1);
_server.OnCultNet(_unsubscribe);
_peerLifecycle = server as ICultNetSchemaServerPeerLifecycle;
if (_peerLifecycle != null)
_peerLifecycle.PeerDisconnected += HandlePeerDisconnected;
}
/// <summary>
/// Raised when an exact subscription is activated or withdrawn. Providers can use
/// the requested records and schemas to materialize reactive state only while a
/// consumer needs it; body publishers additionally use the negotiated body route.
/// </summary>
public event Action<CultNetDatabaseSubscriptionDemand>? DemandChanged;
2026-08-19 13:55:03 +02:00
/// <summary>
/// Re-evaluates every live peer projection against current authorization and source state.
/// Records that lost visibility are explicitly removed and body demand is withdrawn while
/// the request is unauthorized. The subscription intent remains available for a later
/// authorized generation on the same established peer.
/// </summary>
public void Reconcile()
{
lock (_lifecycleGate)
{
if (_disposed) return;
foreach (var entry in _requests.ToArray())
Reconcile(entry.Key, entry.Value);
}
}
/// <summary>Detaches handlers and releases all active watches.</summary>
public void Dispose()
{
2026-08-19 13:55:03 +02:00
lock (_lifecycleGate)
{
2026-08-19 13:55:03 +02:00
if (_disposed) return;
_disposed = true;
_server.RemoveCultNetMessageListener<CultNetDatabaseSubscribeMessage>(_subscribe);
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
_server.RemoveCultNetMessageListener<CultNetDatabaseSubscribeV1Message>(_subscribeV1);
2026-08-19 13:55:03 +02:00
_server.RemoveCultNetMessageListener<CultNetDatabaseUnsubscribeMessage>(_unsubscribe);
if (_peerLifecycle != null)
_peerLifecycle.PeerDisconnected -= HandlePeerDisconnected;
foreach (var entry in _subscriptions.ToArray())
Withdraw(entry.Key, sendRemovals: false, forgetRequest: true);
_subscriptions.Clear();
_requests.Clear();
_projections.Clear();
}
}
private void HandlePeerDisconnected(ICultNetSchemaServerPeer peer)
{
2026-08-19 13:55:03 +02:00
lock (_lifecycleGate)
{
2026-08-19 13:55:03 +02:00
foreach (var key in _subscriptions.Keys.Where(candidate =>
SubscriptionKeyComparer.SamePeer(candidate.Peer, peer)).ToArray())
Withdraw(key, sendRemovals: false, forgetRequest: true);
}
}
private Task HandleSubscribeAsync(CultNetDatabaseSubscribeMessage request, ICultNetSchemaServerPeer peer)
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
{
// v0 has no engine of its own (docs/cultnet-selection-cut.md, D3/D4): it lowers into a
// Selection and is matched by the one evaluator, same as a v1 subscribe.
var selection = new CultNetSelection
{
Schemas = request.SchemaIds,
Keys = request.RecordKeys,
Projection = CultNetSelectionProjections.Document
};
var subscriptionRequest = new SubscriptionRequest(
selection, request.MessageId, request.SubscriptionId, request.IncludeSnapshot,
request.ConsumerRuntimeId, request.BodyIds, request.SupportedBodyTransports, isV1: false);
return Subscribe(subscriptionRequest, peer);
}
private Task HandleSubscribeV1Async(CultNetDatabaseSubscribeV1Message request, ICultNetSchemaServerPeer peer)
{
var subscriptionRequest = new SubscriptionRequest(
request.Selection, request.MessageId, request.SubscriptionId, request.IncludeSnapshot,
request.ConsumerRuntimeId, request.BodyIds, request.SupportedBodyTransports, isV1: true);
return Subscribe(subscriptionRequest, peer);
}
// R-A: the one subscribe core both v0 (lowered) and v1 selections run through - the door
// (validate), the watch (D6: reconcile on every change for a hop-bearing selection, the fast
// single-row path otherwise), the initial snapshot in each version's own wire shape, and demand.
private Task Subscribe(SubscriptionRequest request, ICultNetSchemaServerPeer peer)
{
var subscriptionId = string.IsNullOrWhiteSpace(request.SubscriptionId)
? request.MessageId
: request.SubscriptionId;
if (string.IsNullOrWhiteSpace(subscriptionId))
{
peer.SendCultNet(new CultNetErrorMessage { Error = "Database subscription requires a subscriptionId or messageId." });
return Task.CompletedTask;
}
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
try
{
request.Selection.Validate(_database.Cache.Registry.AllDescriptors.ToArray());
}
catch (CultNetSelectionInvalidException ex)
{
Fix batch 3: R-N wire refusal codes, R-P one authorizer id, R-T removal/cites gaps R-N: cultnet.error.v0 gains additive code/details fields (contracts/cultnet). CultNetErrorMessage grows ForSelectionInvalid/ForCursor/ForReferenceOutsideTarget factories; both C# servers (CultNetDatabaseServer for snapshot+live, CultNetDatabaseSubscriptionServer) use them at every existing catch site, plus two sites that previously let the exception escape uncaught instead of reaching the peer (CultNetDatabaseServer's v1 subscribe snapshot branch, CultNetDatabaseSubscriptionServer's Subscribe around CreateProjectedSnapshot). CultNetSelectionCursorException now carries AsOf/Current for cursor_stale. R-P: CultNetDatabaseSubscriptionServer.CreateMatchedRecord (the live fast path) authorized against descriptor.SchemaId; the snapshot path (CreateProjectedSnapshot) already authorized against the wire id. Both now call the same CultNetDocumentRegistry.WireSchemaId (made internal, was private) so one id reaches the authorizer on the snapshot, live and reconcile paths alike. R-T(a): CultNetSelectionEvaluator.MatchesSchemaKeysFields no longer reads a Removed change's (always-null) document against Fields - it matches on keys/schemas alone and treats a field predicate as unknown instead of throwing TryGetIndexValue(null). R-T(b): EnsureWithinDeclaredTarget no longer skips the out-of-target check when TargetType is null; it checks against every registered leaf (unconstrained is a real, checked target set, not a bypass). CultCache's ResolveReferenceTarget also now infers the target from a many reference's CultRecordRef<T> element when no explicit [CultReference(typeof(...))] is declared, which was the one remaining source of a null TargetType reaching that check on a real, testable path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:44:18 +02:00
peer.SendCultNet(CultNetErrorMessage.ForSelectionInvalid(ex));
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
return Task.CompletedTask;
}
var legacyRequest = request.ToLegacyShape(subscriptionId);
if (_authorizeRequest?.Invoke(legacyRequest, peer) == false)
{
peer.SendCultNet(new CultNetErrorMessage { Error = "Database subscription is not authorized for this peer." });
return Task.CompletedTask;
}
2026-08-19 13:55:03 +02:00
lock (_lifecycleGate)
{
2026-08-19 13:55:03 +02:00
var key = new SubscriptionKey(peer, subscriptionId);
Withdraw(key, sendRemovals: true, forgetRequest: true);
var projection = new SubscriptionProjectionState();
_projections[key] = projection;
_requests[key] = request;
try
{
2026-08-19 13:55:03 +02:00
_subscriptions[key] = Watch(request, subscriptionId, peer, key);
if (request.IncludeSnapshot)
{
var snapshot = CreateProjectedSnapshot(request, peer);
foreach (var entry in snapshot.BySourceRecordKey)
projection.DeliveredBySourceRecordKey[entry.Key] = entry.Value;
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
SendSnapshot(peer, request, snapshot);
2026-08-19 13:55:03 +02:00
}
else
{
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
SendSnapshot(peer, request, ProjectedSnapshot.Empty);
2026-08-19 13:55:03 +02:00
}
PublishDemand(request, key, active: true);
projection.DemandActive = true;
}
Fix batch 3: R-N wire refusal codes, R-P one authorizer id, R-T removal/cites gaps R-N: cultnet.error.v0 gains additive code/details fields (contracts/cultnet). CultNetErrorMessage grows ForSelectionInvalid/ForCursor/ForReferenceOutsideTarget factories; both C# servers (CultNetDatabaseServer for snapshot+live, CultNetDatabaseSubscriptionServer) use them at every existing catch site, plus two sites that previously let the exception escape uncaught instead of reaching the peer (CultNetDatabaseServer's v1 subscribe snapshot branch, CultNetDatabaseSubscriptionServer's Subscribe around CreateProjectedSnapshot). CultNetSelectionCursorException now carries AsOf/Current for cursor_stale. R-P: CultNetDatabaseSubscriptionServer.CreateMatchedRecord (the live fast path) authorized against descriptor.SchemaId; the snapshot path (CreateProjectedSnapshot) already authorized against the wire id. Both now call the same CultNetDocumentRegistry.WireSchemaId (made internal, was private) so one id reaches the authorizer on the snapshot, live and reconcile paths alike. R-T(a): CultNetSelectionEvaluator.MatchesSchemaKeysFields no longer reads a Removed change's (always-null) document against Fields - it matches on keys/schemas alone and treats a field predicate as unknown instead of throwing TryGetIndexValue(null). R-T(b): EnsureWithinDeclaredTarget no longer skips the out-of-target check when TargetType is null; it checks against every registered leaf (unconstrained is a real, checked target set, not a bypass). CultCache's ResolveReferenceTarget also now infers the target from a many reference's CultRecordRef<T> element when no explicit [CultReference(typeof(...))] is declared, which was the one remaining source of a null TargetType reaching that check on a real, testable path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:44:18 +02:00
catch (CultNetSelectionInvalidException ex)
{
// R-N: a hop-bearing selection re-validates inside CreateProjectedSnapshot's
// EvaluateAll, so a selection that passed the door check above can still refuse here.
Withdraw(key, sendRemovals: false, forgetRequest: true);
peer.SendCultNet(CultNetErrorMessage.ForSelectionInvalid(ex));
return Task.CompletedTask;
}
catch (CultNetSelectionReferenceOutsideTargetException ex)
{
Withdraw(key, sendRemovals: false, forgetRequest: true);
peer.SendCultNet(CultNetErrorMessage.ForReferenceOutsideTarget(ex));
return Task.CompletedTask;
}
catch (NotSupportedException refusal)
{
// Q6: a snapshot that selects a variant is refused to this subscriber as a typed error.
Withdraw(key, sendRemovals: false, forgetRequest: true);
peer.SendCultNet(CultNetErrorMessage.ForVariantUnsupported(refusal.Message, subscriptionId));
return Task.CompletedTask;
}
2026-08-19 13:55:03 +02:00
catch
2026-08-19 13:22:36 +02:00
{
2026-08-19 13:55:03 +02:00
Withdraw(key, sendRemovals: false, forgetRequest: true);
throw;
2026-08-19 13:22:36 +02:00
}
2026-07-13 04:19:54 +01:00
}
return Task.CompletedTask;
}
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
private static void SendSnapshot(ICultNetSchemaServerPeer peer, SubscriptionRequest request, ProjectedSnapshot snapshot)
{
if (request.IsV1)
{
var wantDocument = request.Selection.Projection == CultNetSelectionProjections.Document;
peer.SendCultNet(new CultNetSnapshotResponseRawV1Message
{
MessageId = request.MessageId,
Matched = (uint)snapshot.Documents.Length,
Documents = wantDocument ? snapshot.Documents : null,
Headers = wantDocument ? null : snapshot.Documents.Select(CultNetRawDocumentHeader.FromRecord).ToArray(),
Edges = request.Selection.HasHop ? snapshot.Edges : null
});
}
else
{
peer.SendCultNet(new CultNetSnapshotResponseRawMessage
{
MessageId = request.MessageId,
Documents = snapshot.Documents
});
}
}
private Task HandleUnsubscribeAsync(CultNetDatabaseUnsubscribeMessage request, ICultNetSchemaServerPeer peer)
{
var subscriptionId = string.IsNullOrWhiteSpace(request.SubscriptionId)
? request.MessageId
: request.SubscriptionId;
if (!string.IsNullOrWhiteSpace(subscriptionId))
{
var key = new SubscriptionKey(peer, subscriptionId);
2026-08-19 13:55:03 +02:00
lock (_lifecycleGate)
Withdraw(key, sendRemovals: false, forgetRequest: true);
}
return Task.CompletedTask;
}
private void PublishDemand(
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
SubscriptionRequest request,
SubscriptionKey key,
bool active)
{
DemandChanged?.Invoke(new CultNetDatabaseSubscriptionDemand(
string.IsNullOrWhiteSpace(request.ConsumerRuntimeId) ? key.Id : request.ConsumerRuntimeId!,
key.Id,
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
(request.Selection.Keys ?? Array.Empty<string>())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.Ordinal)
.ToArray(),
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
(request.Selection.Schemas ?? Array.Empty<string>())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.Ordinal)
.ToArray(),
(request.BodyIds ?? Array.Empty<string>())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.Ordinal)
.ToArray(),
(request.SupportedBodyTransports ?? Array.Empty<string>())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray(),
IsSameMachine(key.Peer),
active));
}
private static bool IsSameMachine(ICultNetSchemaServerPeer peer)
{
if (peer is ICultNetSchemaServerPeerLocation located && located.RemoteEndPoint is IPEndPoint ip)
return IPAddress.IsLoopback(ip.Address);
return false;
}
private IDisposable Watch(
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
SubscriptionRequest request,
string subscriptionId,
2026-08-19 13:55:03 +02:00
ICultNetSchemaServerPeer peer,
SubscriptionKey key)
{
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
var legacyRequest = request.ToLegacyShape(subscriptionId);
return _database.WatchAllChanges().Subscribe(change =>
{
2026-08-19 13:55:03 +02:00
lock (_lifecycleGate)
{
if (_disposed || !_requests.ContainsKey(key) || !_projections.TryGetValue(key, out var projection))
return;
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
if (_authorizeRequest?.Invoke(legacyRequest, peer) == false)
{
Reconcile(key, request);
return;
}
// D6: cites/cited are set-dependent - a change to row B can change whether row A
// matches - so a hop-bearing selection runs the full diff-against-delivered
// reconcile on every change instead of the single-row fast path.
if (request.Selection.HasHop)
2026-08-19 13:55:03 +02:00
{
Reconcile(key, request);
return;
}
var sourceRecordKey = ResolveChangeRecordKey(change);
CultNetRawDocumentRecord? matched;
try
{
matched = CreateMatchedRecord(change, request, peer);
}
catch (NotSupportedException refusal)
{
// Q6: a variant is not a record CultNet can carry; the subscriber that would have received it is told, by key.
peer.SendCultNet(CultNetErrorMessage.ForVariantUnsupported(refusal.Message));
return;
}
2026-08-19 13:55:03 +02:00
ApplyProjectedChange(
projection,
sourceRecordKey,
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
matched,
2026-08-19 13:55:03 +02:00
peer,
subscriptionId);
}
});
}
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
private void Reconcile(SubscriptionKey key, SubscriptionRequest request)
2026-08-19 13:55:03 +02:00
{
if (!_projections.TryGetValue(key, out var projection)) return;
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
var legacyRequest = request.ToLegacyShape(key.Id);
var authorized = _authorizeRequest?.Invoke(legacyRequest, key.Peer) != false;
ProjectedSnapshot next;
try
{
next = authorized
? CreateProjectedSnapshot(request, key.Peer)
: ProjectedSnapshot.Empty;
}
catch (NotSupportedException refusal)
{
// Q6: the selection selects a variant, which CultNet cannot carry. The subscription is withdrawn, so the peer is
// told once, not once per store change; the rows it delivered are withdrawn too (removals first), so the peer
// does not keep them as live, and the typed error names the subscription that ended. A change never throws out of
// the change handler.
Withdraw(key, sendRemovals: true, forgetRequest: true);
key.Peer.SendCultNet(CultNetErrorMessage.ForVariantUnsupported(refusal.Message, key.Id));
return;
}
2026-08-19 13:55:03 +02:00
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:12:49 +02:00
// 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.
2026-08-19 13:55:03 +02:00
foreach (var previous in projection.DeliveredBySourceRecordKey.ToArray())
{
if (!next.BySourceRecordKey.TryGetValue(previous.Key, out var current))
{
SendRemoval(key.Peer, key.Id, previous.Value);
continue;
}
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:12:49 +02:00
if (!Equivalent(previous.Value, current))
2026-08-19 13:55:03 +02:00
{
SendUpsert(key.Peer, key.Id, current, added: false);
}
}
foreach (var current in next.BySourceRecordKey)
{
if (!projection.DeliveredBySourceRecordKey.ContainsKey(current.Key))
SendUpsert(key.Peer, key.Id, current.Value, added: true);
}
projection.DeliveredBySourceRecordKey.Clear();
foreach (var current in next.BySourceRecordKey)
projection.DeliveredBySourceRecordKey[current.Key] = current.Value;
if (projection.DemandActive != authorized)
{
projection.DemandActive = authorized;
PublishDemand(request, key, authorized);
}
}
private ProjectedSnapshot CreateProjectedSnapshot(
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
SubscriptionRequest request,
2026-08-19 13:55:03 +02:00
ICultNetSchemaServerPeer peer)
{
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
var legacyRequest = request.ToLegacyShape(request.SubscriptionId);
// Both v0 (lowered) and v1 selections evaluate through SelectAll: one evaluation over the
// whole matching set (R-G), documents always (this projects the wire record regardless of
// the selection's own header/document projection - SendSnapshot applies that afterward).
var documentSelection = new CultNetSelection
{
Schemas = request.Selection.Schemas,
Keys = request.Selection.Keys,
Fields = request.Selection.Fields,
Cites = request.Selection.Cites,
Cited = request.Selection.Cited,
Projection = CultNetSelectionProjections.Document
};
var page = _database.Documents.SelectAll(
2026-08-19 13:55:03 +02:00
_database.Cache,
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
documentSelection,
ordinalOf: (schemaId, key) => _database.LastWriteSequence(schemaId, key) ?? 0,
asOf: _database.CurrentAsOf(),
options: null,
rowFilter: _database.Owns);
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:12:49 +02:00
// 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.
2026-08-19 13:55:03 +02:00
var bySourceRecordKey = new Dictionary<string, CultNetRawDocumentRecord>(StringComparer.Ordinal);
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
foreach (var source in page.Documents ?? Array.Empty<CultNetRawDocumentRecord>())
2026-08-19 13:55:03 +02:00
{
var sourceRecordKey = source.RecordKey;
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
if (_authorizeRecord?.Invoke(legacyRequest, peer, sourceRecordKey, source.SchemaId) == false)
2026-08-19 13:55:03 +02:00
continue;
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
bySourceRecordKey[sourceRecordKey] = source;
2026-08-19 13:55:03 +02:00
}
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
return new ProjectedSnapshot(bySourceRecordKey, page.Edges ?? Array.Empty<CultNetEdge>());
2026-08-19 13:55:03 +02:00
}
private void ApplyProjectedChange(
SubscriptionProjectionState projection,
string sourceRecordKey,
CultNetRawDocumentRecord? current,
ICultNetSchemaServerPeer peer,
string subscriptionId)
{
projection.DeliveredBySourceRecordKey.TryGetValue(sourceRecordKey, out var previous);
if (current == null)
{
if (previous != null)
2026-08-19 13:22:36 +02:00
{
2026-08-19 13:55:03 +02:00
projection.DeliveredBySourceRecordKey.Remove(sourceRecordKey);
SendRemoval(peer, subscriptionId, previous);
2026-08-19 13:22:36 +02:00
}
2026-08-19 13:55:03 +02:00
return;
}
if (previous == null)
{
projection.DeliveredBySourceRecordKey[sourceRecordKey] = current;
SendUpsert(peer, subscriptionId, current, added: true);
return;
}
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:12:49 +02:00
// 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.
2026-08-19 13:55:03 +02:00
if (!Equivalent(previous, current))
{
projection.DeliveredBySourceRecordKey[sourceRecordKey] = current;
SendUpsert(peer, subscriptionId, current, added: false);
}
}
private static string ResolveChangeRecordKey(object change)
{
var value = change.GetType().GetProperty("Key")?.GetValue(change);
return value is CultRecordKey key ? key.Value : string.Empty;
}
private static bool Equivalent(CultNetRawDocumentRecord left, CultNetRawDocumentRecord right) =>
string.Equals(left.SchemaId, right.SchemaId, StringComparison.Ordinal) &&
string.Equals(left.SchemaName, right.SchemaName, StringComparison.Ordinal) &&
string.Equals(left.SchemaVersion, right.SchemaVersion, StringComparison.Ordinal) &&
string.Equals(left.SchemaContentHash, right.SchemaContentHash, StringComparison.Ordinal) &&
string.Equals(left.RecordKey, right.RecordKey, StringComparison.Ordinal) &&
string.Equals(left.PayloadEncoding, right.PayloadEncoding, StringComparison.Ordinal) &&
left.Payload.SequenceEqual(right.Payload) &&
string.Equals(left.SourceRuntimeId, right.SourceRuntimeId, StringComparison.Ordinal) &&
string.Equals(left.SourceAgentId, right.SourceAgentId, StringComparison.Ordinal) &&
string.Equals(left.SourceRole, right.SourceRole, StringComparison.Ordinal);
private static void SendUpsert(
ICultNetSchemaServerPeer peer,
string subscriptionId,
CultNetRawDocumentRecord document,
bool added) =>
peer.SendCultNet(new CultNetDatabaseChangeRawMessage
{
MessageId = Guid.NewGuid().ToString("N"),
SubscriptionId = subscriptionId,
ChangeKind = added ? "added" : "updated",
Document = document
});
private static void SendRemoval(
ICultNetSchemaServerPeer peer,
string subscriptionId,
CultNetRawDocumentRecord document) =>
peer.SendCultNet(new CultNetDatabaseChangeRawMessage
{
MessageId = Guid.NewGuid().ToString("N"),
SubscriptionId = subscriptionId,
ChangeKind = "removed",
RecordKey = document.RecordKey,
SchemaId = document.SchemaId
});
2026-08-19 13:55:03 +02:00
private void Withdraw(SubscriptionKey key, bool sendRemovals, bool forgetRequest)
{
if (_subscriptions.TryRemove(key, out var subscription))
subscription.Dispose();
if (_projections.TryRemove(key, out var projection))
{
if (sendRemovals)
{
foreach (var delivered in projection.DeliveredBySourceRecordKey.Values)
SendRemoval(key.Peer, key.Id, delivered);
}
if (projection.DemandActive && _requests.TryGetValue(key, out var activeRequest))
PublishDemand(activeRequest, key, active: false);
}
if (forgetRequest)
_requests.TryRemove(key, out _);
}
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
// One evaluator (docs/cultnet-selection-cut.md, D3/D4): a v0 subscribe request lowers into a
// Selection with no engine of its own, matched and projected exactly as a v1 request would be.
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
// Only called for a non-hop selection (Watch routes a hop-bearing one to Reconcile instead, per
// D6) - CultNetSelectionEvaluator.Matches refuses a hop-bearing selection on this fast path.
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
// Record-level authorization (_authorizeRecord) is not selection and stays layered on top.
private CultNetRawDocumentRecord? CreateMatchedRecord(
object change,
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
SubscriptionRequest request,
ICultNetSchemaServerPeer peer)
{
var changeType = change.GetType();
var key = (CultRecordKey)(changeType.GetProperty("Key")?.GetValue(change) ?? new CultRecordKey(""));
var kind = (CultNetDatabaseChangeKind)(changeType.GetProperty("Kind")?.GetValue(change) ?? CultNetDatabaseChangeKind.Updated);
var document = changeType.GetProperty("Document")?.GetValue(change);
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
var forDescriptor = document ?? changeType.GetProperty("PreviousDocument")?.GetValue(change);
if (forDescriptor == null)
return null;
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
var descriptor = _database.Cache.Registry.GetRequired(forDescriptor.GetType());
var selection = new CultNetSelection
{
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
Schemas = request.Selection.Schemas,
Keys = request.Selection.Keys,
Fields = request.Selection.Fields,
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
Projection = CultNetSelectionProjections.Document
};
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
// R-M: the one binding-alias reconciler (CultNetDocumentRegistry.ExpandSchemaBindingAliases).
var effectiveSelection = _database.Documents.ExpandSchemaBindingAliases(selection);
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
if (!CultNetSelectionEvaluator.Matches(descriptor, key, document, effectiveSelection))
return null;
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
var legacyRequest = request.ToLegacyShape(request.SubscriptionId);
Fix batch 3: R-N wire refusal codes, R-P one authorizer id, R-T removal/cites gaps R-N: cultnet.error.v0 gains additive code/details fields (contracts/cultnet). CultNetErrorMessage grows ForSelectionInvalid/ForCursor/ForReferenceOutsideTarget factories; both C# servers (CultNetDatabaseServer for snapshot+live, CultNetDatabaseSubscriptionServer) use them at every existing catch site, plus two sites that previously let the exception escape uncaught instead of reaching the peer (CultNetDatabaseServer's v1 subscribe snapshot branch, CultNetDatabaseSubscriptionServer's Subscribe around CreateProjectedSnapshot). CultNetSelectionCursorException now carries AsOf/Current for cursor_stale. R-P: CultNetDatabaseSubscriptionServer.CreateMatchedRecord (the live fast path) authorized against descriptor.SchemaId; the snapshot path (CreateProjectedSnapshot) already authorized against the wire id. Both now call the same CultNetDocumentRegistry.WireSchemaId (made internal, was private) so one id reaches the authorizer on the snapshot, live and reconcile paths alike. R-T(a): CultNetSelectionEvaluator.MatchesSchemaKeysFields no longer reads a Removed change's (always-null) document against Fields - it matches on keys/schemas alone and treats a field predicate as unknown instead of throwing TryGetIndexValue(null). R-T(b): EnsureWithinDeclaredTarget no longer skips the out-of-target check when TargetType is null; it checks against every registered leaf (unconstrained is a real, checked target set, not a bypass). CultCache's ResolveReferenceTarget also now infers the target from a many reference's CultRecordRef<T> element when no explicit [CultReference(typeof(...))] is declared, which was the one remaining source of a null TargetType reaching that check on a real, testable path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:44:18 +02:00
// R-P: one id reaches the authorizer, and it is the wire id, on every path. The snapshot
// path (CreateProjectedSnapshot, below) authorizes against the raw record's own SchemaId,
// which is the wire id ToRawRecord/binding emit; this live fast path used to pass
// descriptor.SchemaId instead - the CLR type's own registered id, not the id a binding
// overrides to - so a row delivered by the snapshot could be refused on its first live
// change, or leak through a denylist keyed on the wire id.
if (_authorizeRecord?.Invoke(legacyRequest, peer, key.Value, _database.Documents.WireSchemaId(descriptor)) == false)
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
return null;
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
if (kind == CultNetDatabaseChangeKind.Removed || document == null)
return null;
CultNetDocumentRegistry.RefuseVariantChange(_database.Cache, key);
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
return _database.Documents.ToRawRecord(descriptor, key, document, DateTimeOffset.UtcNow.ToString("O"));
}
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
// R-A: the one internal shape both a lowered v0 subscribe and a v1 subscribe carry through
// Subscribe/Watch/Reconcile/CreateProjectedSnapshot/CreateMatchedRecord/PublishDemand. The
// authorize/authorizeRecord delegates keep their existing public constructor contract (typed
// against the v0 message) rather than becoming a breaking API change in this fix batch;
// ToLegacyShape carries every field those delegates read (schemas/keys/consumer/body demand) -
// fields/cites/cited reach neither delegate under v0 today either, so nothing regresses.
private sealed class SubscriptionRequest
{
public SubscriptionRequest(
CultNetSelection selection,
string messageId,
string subscriptionId,
bool includeSnapshot,
string? consumerRuntimeId,
string[]? bodyIds,
string[]? supportedBodyTransports,
bool isV1)
{
Selection = selection;
MessageId = messageId;
SubscriptionId = subscriptionId;
IncludeSnapshot = includeSnapshot;
ConsumerRuntimeId = consumerRuntimeId;
BodyIds = bodyIds;
SupportedBodyTransports = supportedBodyTransports;
IsV1 = isV1;
}
public CultNetSelection Selection { get; }
public string MessageId { get; }
public string SubscriptionId { get; }
public bool IncludeSnapshot { get; }
public string? ConsumerRuntimeId { get; }
public string[]? BodyIds { get; }
public string[]? SupportedBodyTransports { get; }
public bool IsV1 { get; }
public CultNetDatabaseSubscribeMessage ToLegacyShape(string subscriptionId) => new()
{
MessageId = MessageId,
SubscriptionId = subscriptionId,
SchemaIds = Selection.Schemas,
RecordKeys = Selection.Keys,
IncludeSnapshot = IncludeSnapshot,
ConsumerRuntimeId = ConsumerRuntimeId,
BodyIds = BodyIds,
SupportedBodyTransports = SupportedBodyTransports
};
}
2026-08-19 13:55:03 +02:00
private sealed class SubscriptionProjectionState
{
public Dictionary<string, CultNetRawDocumentRecord> DeliveredBySourceRecordKey { get; } =
new(StringComparer.Ordinal);
public bool DemandActive { get; set; }
}
private sealed class ProjectedSnapshot
{
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
public ProjectedSnapshot(Dictionary<string, CultNetRawDocumentRecord> bySourceRecordKey, CultNetEdge[]? edges = null)
2026-08-19 13:55:03 +02:00
{
BySourceRecordKey = bySourceRecordKey;
Documents = bySourceRecordKey.Values.ToArray();
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
Edges = edges ?? Array.Empty<CultNetEdge>();
2026-08-19 13:55:03 +02:00
}
public static ProjectedSnapshot Empty { get; } =
new(new Dictionary<string, CultNetRawDocumentRecord>(StringComparer.Ordinal));
public Dictionary<string, CultNetRawDocumentRecord> BySourceRecordKey { get; }
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
public CultNetEdge[] Edges { get; }
2026-08-19 13:55:03 +02:00
public CultNetRawDocumentRecord[] Documents { get; }
}
private sealed class SubscriptionKey
{
public SubscriptionKey(ICultNetSchemaServerPeer peer, string id)
{
Peer = peer;
Id = id;
}
public ICultNetSchemaServerPeer Peer { get; }
public string Id { get; }
}
private sealed class SubscriptionKeyComparer : IEqualityComparer<SubscriptionKey>
{
public static SubscriptionKeyComparer Instance { get; } = new SubscriptionKeyComparer();
public bool Equals(SubscriptionKey? x, SubscriptionKey? y) =>
x != null && y != null && SamePeer(x.Peer, y.Peer) && string.Equals(x.Id, y.Id, StringComparison.Ordinal);
public int GetHashCode(SubscriptionKey value) =>
(PeerHash(value.Peer) * 397) ^ StringComparer.Ordinal.GetHashCode(value.Id);
internal static bool SamePeer(ICultNetSchemaServerPeer left, ICultNetSchemaServerPeer right)
{
if (ReferenceEquals(left, right)) return true;
if (left is RudpCultNetSchemaServerPeer leftRudp && right is RudpCultNetSchemaServerPeer rightRudp)
return ReferenceEquals(leftRudp.TransportPeer, rightRudp.TransportPeer);
if (left is CultNetServerPeer leftLite && right is CultNetServerPeer rightLite)
return ReferenceEquals(leftLite.Peer, rightLite.Peer);
return false;
}
private static int PeerHash(ICultNetSchemaServerPeer peer)
{
if (peer is RudpCultNetSchemaServerPeer rudp)
return RuntimeHelpers.GetHashCode(rudp.TransportPeer);
if (peer is CultNetServerPeer lite)
return RuntimeHelpers.GetHashCode(lite.Peer);
return RuntimeHelpers.GetHashCode(peer);
}
}
}
/// <summary>One active or withdrawn exact state subscription and its optional hot-body route.</summary>
public sealed class CultNetDatabaseSubscriptionDemand
{
public CultNetDatabaseSubscriptionDemand(
string consumerRuntimeId,
string subscriptionId,
IReadOnlyList<string> bodyIds,
IReadOnlyList<string> supportedBodyTransports,
bool sameMachine,
bool active)
: this(
consumerRuntimeId,
subscriptionId,
Array.Empty<string>(),
Array.Empty<string>(),
bodyIds,
supportedBodyTransports,
sameMachine,
active)
{
}
public CultNetDatabaseSubscriptionDemand(
string consumerRuntimeId,
string subscriptionId,
IReadOnlyList<string> recordKeys,
IReadOnlyList<string> schemaIds,
IReadOnlyList<string> bodyIds,
IReadOnlyList<string> supportedBodyTransports,
bool sameMachine,
bool active)
{
ConsumerRuntimeId = consumerRuntimeId;
SubscriptionId = subscriptionId;
RecordKeys = recordKeys;
SchemaIds = schemaIds;
BodyIds = bodyIds;
SupportedBodyTransports = supportedBodyTransports;
SameMachine = sameMachine;
Active = active;
}
public string ConsumerRuntimeId { get; }
public string SubscriptionId { get; }
public IReadOnlyList<string> RecordKeys { get; }
public IReadOnlyList<string> SchemaIds { get; }
public IReadOnlyList<string> BodyIds { get; }
public IReadOnlyList<string> SupportedBodyTransports { get; }
public bool SameMachine { get; }
public bool Active { get; }
}
}