test(ab): hw4 evidence - mixed MSP read (#582), listing order (#572) on the press; AbLegacy bit-write test; gate-results + docs

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 04:10:22 -04:00
parent 82750cccb9
commit 14ec145487
10 changed files with 530 additions and 6 deletions
@@ -247,6 +247,7 @@ public sealed class CipConnection : ICipChannel, IAsyncDisposable
_ => await open.Client.SendRRDataAsync(cip, _o.Timeout, linked.Token).ConfigureAwait(false),
};
_diagnostics.RecordExchange(Stopwatch.GetTimestamp() - started);
_diagnostics.NotifyExchange(cip, reply);
if (_o.Routing == CipRouting.Connected && ConnectionManagerServices.IsConnectionGone(reply.Span) is { } extended)
{
await RetireClosedByTargetAsync(open).ConfigureAwait(false);
@@ -91,6 +91,21 @@ internal sealed class ConnectionDiagnostics
Ms(Interlocked.Read(ref _exchangeTotalTicks)) / count);
}
/// <summary>
/// Evidence tap for the gate harness (#582 / #572): sees the CIP request and the raw CIP reply of every
/// completed exchange. Null in production; set only by the read-only differential harness. A throwing
/// observer is swallowed so it can never affect a request.
/// </summary>
internal Action<ReadOnlyMemory<byte>, ReadOnlyMemory<byte>>? ExchangeObserver { get; set; }
internal void NotifyExchange(ReadOnlyMemory<byte> request, ReadOnlyMemory<byte> reply)
{
var observer = ExchangeObserver;
if (observer is null) return;
try { observer(request, reply); }
catch { /* evidence only */ }
}
internal void RecordExchange(long ticks)
{
Interlocked.Increment(ref _exchangeCount);