docs(src): fix CommentChecker findings across src/ — 1,032 → 5 (all false positives)

Comment/XML-doc changes only, no code logic:
- strip project-tracking references (WP*, P*-*, row-NN, #NN, Task N) from
  comments, keeping the explanatory rationale
- add missing summaries, <param>, <returns>, <typeparam> and <inheritdoc/>
- replace misused <inheritdoc/> with real docs; where <inheritdoc/> was
  combined with useful remarks, move the rationale into // comments
- drop stale replicationActor <param> on DeploymentManagerActor ctor

Remaining 5 are false positives (CWE-1236 x2, failures-1, year-0001, M365).
Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 14:12:25 -04:00
parent 3a217b9484
commit 0ffd4bdbea
217 changed files with 1224 additions and 695 deletions
@@ -11,10 +11,16 @@ public static class AuditWireBudget
public const int InitialBytes = 7;
/// <summary>
/// Appends one row to a field-1 repeated-message collection if it fits. A row larger than the
/// Appends one row to a repeated-message collection if it fits. A row larger than the
/// normal budget travels alone, up to the hard channel ceiling. Oversize rows are retained
/// locally and fail explicitly with their identity; they are never acknowledged or truncated.
/// </summary>
/// <typeparam name="T">The protobuf message type of the rows.</typeparam>
/// <param name="rows">The repeated field to append to.</param>
/// <param name="row">The row to append.</param>
/// <param name="wireBytes">The running wire size of the batch; increased by the row's size when appended.</param>
/// <param name="rowIdentity">Identity of the row, used in the error raised when it exceeds the hard ceiling.</param>
/// <returns><c>true</c> when the row was appended; <c>false</c> when it does not fit in the current batch.</returns>
public static bool TryAppend<T>(RepeatedField<T> rows, T row, ref int wireBytes, string rowIdentity)
where T : IMessage<T>
{
@@ -268,7 +268,7 @@ public sealed class CentralChannelProvider : IDisposable
return TimeSpan.FromTicks(scaled >= cap || scaled < 0 ? cap : scaled);
}
/// <inheritdoc />
/// <summary>Stops the failback timer and disposes all cached gRPC channels.</summary>
public void Dispose()
{
lock (_gate)
@@ -74,7 +74,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
public static class CentralControlDtoMapper
{
// -----------------------------------------------------------------------
// Notification Outbox (#21)
// Notification Outbox
// -----------------------------------------------------------------------
/// <summary>Projects a <see cref="NotificationSubmit"/> onto its wire DTO.</summary>
@@ -216,7 +216,7 @@ public static class CentralControlDtoMapper
}
// -----------------------------------------------------------------------
// Audit Log (#23) ingest — envelopes only; rows go through the existing mappers
// Audit Log ingest — envelopes only; rows go through the existing mappers
// -----------------------------------------------------------------------
/// <summary>
@@ -437,7 +437,7 @@ public static class CentralControlDtoMapper
}
// -----------------------------------------------------------------------
// Health Monitoring (#11)
// Health Monitoring
// -----------------------------------------------------------------------
/// <summary>Projects a <see cref="SiteHealthReport"/> onto its wire DTO.</summary>
@@ -559,7 +559,7 @@ public static class CentralControlDtoMapper
dto.SiteAuditBacklog = snapshot;
}
// Review finding M1: wrapper presence keeps null ("no data") distinct from empty.
// Wrapper presence keeps null ("no data") distinct from empty.
if (msg.SecretMirrorStatuses is { } mirrorStatuses)
{
var wrapper = new SecretMirrorStatusListDto();
@@ -277,21 +277,15 @@ public sealed class CentralControlGrpcService : CentralControlService.CentralCon
return CentralControlDtoMapper.ToDto(ack);
}
/// <summary>
/// Application heartbeat — <b>always answers OK</b>, even when this node is not ready.
/// </summary>
/// <remarks>
/// The heartbeat is fire-and-forget on both sides: nothing on the site consumes the reply,
/// and the site's heartbeat timer must never take a fault (a failing heartbeat that raised
/// an error would be a self-inflicted outage on a purely informational signal). So this is
/// the one RPC that does not go through <see cref="RequireReady"/>: a heartbeat arriving
/// before the actor exists is logged and dropped, exactly as the actor itself drops one
/// that arrives before <c>ICentralHealthAggregator</c> is resolvable. Liveness is still
/// detected — the aggregator's offline timeout fires when the heartbeats stop landing.
/// </remarks>
/// <param name="request">The heartbeat.</param>
/// <param name="context">The gRPC call context.</param>
/// <returns>An empty reply, always.</returns>
// Application heartbeat — always answers OK, even when this node is not ready.
// The heartbeat is fire-and-forget on both sides: nothing on the site consumes the reply,
// and the site's heartbeat timer must never take a fault (a failing heartbeat that raised
// an error would be a self-inflicted outage on a purely informational signal). So this is
// the one RPC that does not go through RequireReady: a heartbeat arriving before the actor
// exists is logged and dropped, exactly as the actor itself drops one that arrives before
// ICentralHealthAggregator is resolvable. Liveness is still detected — the aggregator's
// offline timeout fires when the heartbeats stop landing.
/// <inheritdoc />
public override Task<Empty> Heartbeat(HeartbeatDto request, ServerCallContext context)
{
var central = _central;
@@ -8,14 +8,20 @@ public static class CentralControlSiteIdentity
private static readonly object IdentityKey = new();
/// <summary>Stores an identity only after the site's credential has been verified.</summary>
/// <param name="context">The call context to record the identity on.</param>
/// <param name="siteId">The verified site identifier.</param>
public static void SetVerified(ServerCallContext context, string siteId) => context.UserState[IdentityKey] = siteId;
/// <summary>Requires a credential-verified identity on the current call.</summary>
/// <param name="context">The call context carrying the verified identity.</param>
/// <returns>The verified site identifier.</returns>
public static string RequireVerified(ServerCallContext context) =>
context.UserState.TryGetValue(IdentityKey, out var value) && value is string site && !string.IsNullOrWhiteSpace(site)
? site : throw Denied();
/// <summary>Checks every ownership field before dispatching any part of a central control request.</summary>
/// <param name="request">The inbound request whose ownership fields are checked.</param>
/// <param name="context">The call context carrying the verified site identity.</param>
public static void ValidateRequest(object request, ServerCallContext context)
{
var site = RequireVerified(context);
@@ -9,14 +9,20 @@ public static class GrpcRetrySafety
public const string DispatchTrailer = "scadabridge-dispatch";
/// <summary>Creates a readiness rejection that proves no work was dispatched.</summary>
/// <param name="detail">Human-readable rejection detail placed on the status.</param>
/// <returns>An <see cref="RpcException"/> with <c>Unavailable</c> status and the not-dispatched trailer.</returns>
public static RpcException NotDispatched(string detail) => new(
new Status(StatusCode.Unavailable, detail), new Metadata { { DispatchTrailer, "not-started" } });
/// <summary>True only for an explicit rejection from the server before it dispatched work.</summary>
/// <param name="error">The RPC failure to inspect.</param>
/// <returns><c>true</c> when the trailer proves the server never dispatched the command.</returns>
public static bool WasNotDispatched(RpcException error) => error.StatusCode == StatusCode.Unavailable
&& error.Trailers.Any(e => e.Key == DispatchTrailer && e.Value == "not-started");
/// <summary>Reports an ambiguous mutation without inviting a blind replay.</summary>
/// <param name="error">The original RPC failure whose trailers are preserved.</param>
/// <returns>An <see cref="RpcException"/> with <c>Aborted</c> status marking the outcome as unknown.</returns>
public static RpcException OutcomeUnknown(RpcException error) => new(
new Status(StatusCode.Aborted, "OUTCOME_UNKNOWN: connection failed after the command may have been applied. Verify its state before retrying."),
error.Trailers);
@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <para>
/// <b>Per-call deadlines match today's Ask timeouts exactly</b> — see <see cref="ResolveDeadline"/>.
/// An unknown/unconfigured site now fails fast: it surfaces to the caller as a
/// <see cref="Status.Failure"/> immediately (WP1.6) instead of being warned-and-dropped to let the
/// <see cref="Status.Failure"/> immediately instead of being warned-and-dropped to let the
/// caller's Ask burn its full timeout. A transport fault takes the same <see cref="Status.Failure"/>
/// path, which the S&amp;F/audit layers already treat as transient.
/// </para>
@@ -83,7 +83,7 @@ public sealed class GrpcSiteTransport : ISiteCommandTransport
}
catch (SiteChannelUnavailableException ex)
{
// WP1.6: a known-dead send (no configured channel for this site) used to warn-and-drop,
// A known-dead send (no configured channel for this site) used to warn-and-drop,
// leaving the caller's Ask to burn its full timeout for a failure we already know about.
// Fail fast instead — same Status.Failure completion path a transport fault takes below —
// so S&F/audit treat it as transient immediately rather than tens of seconds later.
@@ -130,6 +130,8 @@ public sealed class GrpcSiteTransport : ISiteCommandTransport
}
/// <summary>Only read-only commands can replay after a response may have been lost.</summary>
/// <param name="message">The command message about to be sent.</param>
/// <returns><c>true</c> when the message is a read-only command safe to replay; otherwise <c>false</c>.</returns>
internal static bool IsRetrySafe(object message) => message is
DeploymentStateQueryRequest or BrowseNodeCommand or SearchAddressSpaceCommand or ReadTagValuesCommand
or ListServerCertsCommand or EventLogQueryRequest or DebugSnapshotRequest or ParkedMessageQueryRequest
@@ -56,7 +56,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// Nullable <b>collections</b> are wrapped in a per-collection message, so
/// the null/empty distinction becomes a message-presence question. proto3
/// <c>repeated</c>/<c>map</c> alone collapses the two — precisely the silent
/// data-loss class the transport round-trip guard caught in PLAN-05 T8.
/// data-loss class the transport round-trip guard caught in the transport round-trip tests.
/// </item>
/// <item>
/// <c>Guid?</c> members ride as a <c>string</c> holding the "D" form, with
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// Raised when a site has no usable gRPC channel — an unknown site, or a site with neither
/// <c>GrpcNodeAAddress</c> nor <c>GrpcNodeBAddress</c> configured. <see cref="GrpcSiteTransport"/>
/// treats this as a known-dead send: it fails the caller's <c>Ask</c> fast with an
/// <c>Akka.Actor.Status.Failure</c> wrapping this exception (WP1.6) instead of warning and dropping
/// <c>Akka.Actor.Status.Failure</c> wrapping this exception instead of warning and dropping
/// the message, so the caller does not burn its full Ask timeout on a failure already known at send
/// time (central still never buffers).
/// </summary>
@@ -51,8 +51,7 @@ public sealed class SitePairChannelProvider : IDisposable
private static readonly TimeSpan FailbackSweepInterval = TimeSpan.FromSeconds(1);
/// <summary>
/// How long a replaced/removed channel is PARKED before it is disposed (arch-review
/// 2026-08-19 P3-2). Active calls retain the channel beyond this minimum until completion.
/// How long a replaced/removed channel is PARKED before it is disposed. Active calls retain the channel beyond this minimum until completion.
/// </summary>
internal static readonly TimeSpan ChannelDisposalGrace = TimeSpan.FromMinutes(5);
@@ -366,7 +365,7 @@ public sealed class SitePairChannelProvider : IDisposable
/// <summary>
/// Parks a channel that is no longer reachable through <see cref="_sites"/> instead of
/// disposing it inline (arch-review 2026-08-19 P3-2).
/// disposing it inline.
/// <para>
/// <b>Why deferred.</b> <see cref="ExecuteAsync{T}"/> reads the channel under
/// <c>pair.Gate</c> but invokes the RPC OUTSIDE it, so an inline
@@ -472,7 +471,7 @@ public sealed class SitePairChannelProvider : IDisposable
}
}
/// <inheritdoc />
/// <summary>Disposes every pooled channel and stops the failback sweep and parked-channel disposal.</summary>
public void Dispose()
{
lock (_lifecycleGate)
@@ -500,6 +499,7 @@ public sealed class SitePairChannelProvider : IDisposable
private sealed class SitePair(string siteId)
{
/// <summary>The site identifier this pair of node channels belongs to.</summary>
public string SiteId { get; } = siteId;
public readonly object Gate = new();
public string? NodeAEndpoint;
@@ -515,12 +515,15 @@ public sealed class SitePairChannelProvider : IDisposable
private TimeSpan _backoff = InitialBackoff;
/// <summary>Sets the failback backoff to <paramref name="delay"/> and schedules the next probe that far ahead.</summary>
/// <param name="delay">The delay before the next failback probe.</param>
public void ScheduleFailback(TimeSpan delay)
{
_backoff = delay;
NextProbeUtc = DateTime.UtcNow + _backoff;
}
/// <summary>Doubles the failback backoff (capped at the maximum) and schedules the next probe.</summary>
public void BumpFailbackBackoff()
{
var next = TimeSpan.FromTicks(Math.Min(_backoff.Ticks * 2, MaxBackoff.Ticks));
@@ -528,6 +531,7 @@ public sealed class SitePairChannelProvider : IDisposable
NextProbeUtc = DateTime.UtcNow + _backoff;
}
/// <summary>Resets the backoff to its initial value and clears the scheduled failback probe.</summary>
public void ResetFailback()
{
_backoff = InitialBackoff;
@@ -122,6 +122,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// Creates a test-only instance that has no gRPC channel. Used to test
/// Unsubscribe and Dispose behavior without needing a real endpoint.
/// </summary>
/// <param name="options">Optional communication options; defaults are used when null.</param>
/// <returns>A <see cref="SiteStreamGrpcClient"/> with no channel or client, for testing only.</returns>
internal static SiteStreamGrpcClient CreateForTesting(CommunicationOptions? options = null) => new(options ?? new CommunicationOptions());
@@ -146,8 +147,8 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// <see cref="SubscribeAsync"/> calls briefly share an ID), the prior CTS is
/// <b>cancelled only</b> — never disposed from here. Internal for testability.
/// <para>
/// <b>Cancel-only is the contract</b> (arch-review 2026-08-19 P3-1, the client half of the
/// row-47 class): the prior handler's <see cref="ConsumeStreamAsync"/> is still running and
/// <b>Cancel-only is the contract</b> (the client half of the
/// shared-token-source disposal race): the prior handler's <see cref="ConsumeStreamAsync"/> is still running and
/// still reads <c>cts.Token</c> (opening the call, <c>ReadAllAsync</c>, the
/// <c>IsCancellationRequested</c> checks), so disposing it from the REPLACING path throws
/// <see cref="ObjectDisposedException"/> inside the prior stream — and, worse, out of the
@@ -172,7 +173,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// Cancels a subscription token source, swallowing <see cref="ObjectDisposedException"/>.
/// A CTS whose owning stream loop has already unwound and disposed it is simply a
/// subscription that no longer needs cancelling; the caller must never see that race as a
/// failure (arch-review 2026-08-19 P3-1).
/// failure.
/// </summary>
/// <param name="cts">The token source to cancel.</param>
private static void CancelQuietly(CancellationTokenSource cts)
@@ -249,7 +250,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
}
// This call OWNS `cts` — it created it, so it disposes it once the stream loop has
// fully unwound (P3-1). No other path may dispose it while this one is still reading
// fully unwound. No other path may dispose it while this one is still reading
// cts.Token; a replacing subscription cancels only.
try
{
@@ -270,15 +271,15 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// <summary>
/// Opens the site-wide, <b>alarm-only</b> server-streaming subscription
/// (<c>SubscribeSite</c>) for a whole site rather than a single instance. This is
/// the central per-site feed that backs the aggregated Alarm Summary live cache
/// (plan #10): the site runtime's <c>SubscribeSiteAlarms</c> hub drops the
/// the central per-site feed that backs the aggregated Alarm Summary live cache:
/// the site runtime's <c>SubscribeSiteAlarms</c> hub drops the
/// per-instance filter and carries only <see cref="AlarmStateChanged"/> events for
/// <em>all</em> instances on the site (attributes are deliberately excluded — the
/// summary never shows them and they are far higher-volume).
/// <para>
/// The callback is deliberately <b>typed</b> as <see cref="AlarmStateChanged"/>
/// rather than the generic <c>Action&lt;object&gt;</c> used by
/// <see cref="SubscribeAsync"/>: this stream is alarm-only by contract, so Task 4's
/// <see cref="SubscribeAsync"/>: this stream is alarm-only by contract, so the
/// per-site cache consumes an alarm delta directly with no downstream type test.
/// The mapping is still shared via <see cref="ConvertToDomainEvent"/>; a non-alarm
/// event (which should never appear on this stream) is defensively ignored rather
@@ -504,7 +505,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// <summary>
/// Cancels an active subscription by correlation ID. Cancel-only: the subscription's own
/// <see cref="SubscribeAsync"/>/<see cref="SubscribeSiteAsync"/> call disposes the token
/// source once its stream loop unwinds (P3-1). Cancelling an entry whose owner already
/// source once its stream loop unwinds. Cancelling an entry whose owner already
/// disposed it — or unsubscribing after the client was disposed — is a silent no-op.
/// </summary>
/// <param name="correlationId">Unique identifier of the subscription to cancel.</param>
@@ -664,7 +665,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable
/// here is synchronous (CTS cancellation and <see cref="GrpcChannel.Dispose"/>), so a
/// synchronous <see cref="Dispose"/> can release everything without sync-over-async blocking.
/// <para>
/// Cancel-only, per-entry guarded (P3-1). The token sources are owned by their
/// Cancel-only, per-entry guarded. The token sources are owned by their
/// <see cref="SubscribeAsync"/>/<see cref="SubscribeSiteAsync"/> calls, which dispose them
/// as their loops unwind in response to this cancellation; disposing them here raced those
/// loops, and one <see cref="ObjectDisposedException"/> aborted the whole foreach — which in
@@ -131,7 +131,7 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
/// <summary>
/// Asynchronously disposes all cached clients and clears the cache. Each client's disposal
/// is fault-isolated: one throwing client must not abort the loop and strand every later
/// client's channel undisposed (arch-review 2026-08-19 P3-1).
/// client's channel undisposed.
/// </summary>
/// <returns>A value task that completes when all clients have been disposed.</returns>
public async ValueTask DisposeAsync()
@@ -39,7 +39,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
private volatile bool _shuttingDown;
private long _actorCounter;
// Ask timeout is 30 s. The ingest actor's repo
// calls are sub-100 ms in steady state; a generous timeout absorbs a slow
// calls take under 100 ms in steady state; a generous timeout absorbs a slow
// MSSQL connection without surfacing as a gRPC failure on a healthy site.
// Shared with CentralCommunicationActor (same assembly) so the two audit-ingest
// transports use one source of truth for the timeout.
@@ -275,7 +275,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
// enriched AlarmStateUpdate, so it is reused unchanged.
_streamSubscriber.SubscribeSiteAlarms,
"site-wide alarms",
// Its OWN, much larger channel (WP2.3): sharing the Debug View's 1000-slot
// Its OWN, much larger channel: sharing the Debug View's 1000-slot
// DropOldest meant an alarm burst during a WAN stall silently evicted operator-
// visible transitions to make room for diagnostics traffic.
_siteAlarmChannelCapacity,
@@ -338,7 +338,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
//
// The removal is claimed FIRST, but nothing is cancelled until the concurrency cap
// has had its say, and a REPLACEMENT never consults the cap at all (arch-review
// 2026-08-19 P3-5). Both halves matter:
// 2026-08-19). Both halves matter:
//
// * A replacement is net-neutral on the stream count (one entry out, one back in),
// so refusing it can only ever destroy capacity, never protect it. The old code
@@ -402,7 +402,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
{
// Lossy-under-backpressure is the debug-view spec; make the real
// loss visible: first eviction + every 500th thereafter, plus a
// per-kind counter on the node (WP2.3).
// per-kind counter on the node.
var n = Interlocked.Increment(ref dropped);
Interlocked.Increment(ref _droppedStreamEvents);
ScadaBridgeTelemetry.RecordSiteStreamEventDropped(streamKind);
@@ -447,8 +447,8 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
"Stream {CorrelationId} started for {Description} (subscription {SubscriptionId})",
correlationId, description, subscriptionId);
// Flush response headers NOW, while the relay is attached and before the first event
// (WP2.3). This is the client's "the site accepted my subscription" signal — the
// Flush response headers NOW, while the relay is attached and before the first event.
// This is the client's "the site accepted my subscription" signal — the
// per-site aggregator hangs its once-per-successful-reconnect re-seed off it. Without
// an explicit flush, ASP.NET Core defers headers to the first written message, so a
// quiet site would never report connected. Best-effort: a client that vanished between
@@ -516,12 +516,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
}
}
/// <summary>Retained wire method; ingest is served only by CentralControlService.</summary>
/// <inheritdoc />
public override Task<IngestAck> IngestAuditEvents(AuditEventBatch request, ServerCallContext context)
=> Task.FromException<IngestAck>(new RpcException(new GrpcStatus(
StatusCode.Unimplemented, "Use CentralControlService.IngestAuditEvents on central.")));
/// <summary>Retained wire method; combined ingest is served only by CentralControlService.</summary>
/// <inheritdoc />
public override Task<IngestAck> IngestCachedTelemetry(CachedTelemetryBatch request, ServerCallContext context)
=> Task.FromException<IngestAck>(new RpcException(new GrpcStatus(
StatusCode.Unimplemented, "Use CentralControlService.IngestCachedTelemetry on central.")));
@@ -535,12 +535,11 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
/// <summary>Flow Debugger PullTraceEvents: server-side id cap, matching the site queue's.</summary>
public const int MaxTraceExecutionIdsPerPull = 64;
/// <summary>
/// Flow Debugger (design 2026-09-05 §5.4): point read of this site's audit rows whose
/// <c>ExecutionId</c> or <c>ParentExecutionId</c> is one of the requested ids. NOT a
/// reconciliation path — moves no cursor, flips no ForwardState. Gated by the same
/// <c>ControlPlaneAuthInterceptor</c> prefix as <see cref="PullAuditEvents"/>.
/// </summary>
// Flow Debugger (design 2026-09-05 §5.4): point read of this site's audit rows whose
// ExecutionId or ParentExecutionId is one of the requested ids. NOT a reconciliation
// path — moves no cursor, flips no ForwardState. Gated by the same
// ControlPlaneAuthInterceptor prefix as PullAuditEvents.
/// <inheritdoc />
public override async Task<PullTraceEventsResponse> PullTraceEvents(
PullTraceEventsRequest request,
ServerCallContext context)
@@ -630,12 +629,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
? DateTime.SpecifyKind(request.SinceUtc.ToDateTime(), DateTimeKind.Utc)
: DateTime.MinValue;
// Composite-keyset cursor (WP2.3), mirroring PullSiteCalls: proto3
// Composite-keyset cursor, mirroring PullSiteCalls: proto3
// defaults an unset string to "", which the queue treats as "no cursor"
// (legacy inclusive >= behaviour), so an older central is unaffected.
var afterId = string.IsNullOrEmpty(request.AfterId) ? null : request.AfterId;
// AT-LEAST-ONCE (WP2.3). The incoming cursor is central's receipt for
// AT-LEAST-ONCE. The incoming cursor is central's receipt for
// everything at or before it — that is the ONLY proof the site accepts.
// Rows are flipped to Reconciled here, at the START of the NEXT pull,
// instead of right after the previous response was projected: a fault