Files
mxaccessgw/docs/plans/2026-08-17-deferred-closeout.md
T
Joseph Doherty b621d692d0
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m14s
ci / java (push) Successful in 2m10s
ci / portable (push) Successful in 8m31s
docs+test(closeout): final-review reservations — stale ACL prose, worker test gaps, config sample fix
2026-08-17 05:23:23 -04:00

30 KiB
Raw Blame History

Deferred Closeout Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task (Opus implementers per user instruction; reviewer chain per each task's Classification).

Goal: Close every item the two perf-remediation plans left recorded-but-open: the dead ISessionManager.ReadEventsAsync chain, the frame-writer lock-parking latency follow-up, SEC-25 (per-session dashboard event ACL, design already approved in docs/plans/2026-07-10-dashboard-session-acl-tst15.md), the structural alarm-truncation degraded-status signal (proto change), and the wnwrap live-alarm probes (GUID identity, ALARM_RECORDS/@COUNT semantics) that need windev state.

Architecture: Same two-phase posture as the prior plans. Gateway-side work builds and tests on macOS via NonWindows.slnx; worker-side work (frame writer, alarm consumer, worker command executor) is edited on the Mac and verified on windev. This plan touches .proto contracts (Task 8) — contracts regeneration and all five clients rebuild are in scope (Task 9), unlike the prior two plans. The windev probe task (Task 3) is gated on external state (live alarms) and may legitimately end "blocked — recorded".

Tech stack: .NET 10 gateway / .NET Framework 4.8 x86 worker / protobuf contracts / five language clients / Blazor Server dashboard / GLAuth LDAP.

Branch: feat/deferred-closeout off main (ac3f04f).


Ground rules for every implementer subagent

  • NEVER run git stash, git reset, git clean, or git checkout <sha/branch>. Commit with explicit pathspecs only — never git add -A / git commit -a.
  • Build/test mutual exclusion: before any dotnet build/dotnet test, acquire the lock via mkdir /private/tmp/claude-501/-Users-dohertj2-Desktop-MxAccessGateway/f36938ae-bbca-4245-b5c9-fac512d69e22/scratchpad/buildlock (retry with backoff while it fails); rmdir it on ALL exit paths, including failures.
  • TreatWarningsAsErrors=true, Nullable=enable — new warnings break the build; fix, don't suppress.
  • Follow docs/style-guides/CSharpStyleGuide.md: file-scoped namespaces, sealed by default, Async suffix, MXAccess-aligned names.
  • Update affected docs in the same commit as the source change.
  • MXAccess parity is the contract; never synthesize events.
  • Never log secrets, API keys, credentials, or tag values.
  • The Files: block is the scope contract. If the task can't be done inside it, that's a plan defect — surface it, don't silently expand scope.
  • On macOS build NonWindows.slnx; the x86 Worker and full slnx only build on windev.

Task 1: Remove the dead ISessionManager.ReadEventsAsync chain

Classification: standard Estimated implement time: ~6 min Parallelizable with: Task 2, Task 3, Task 5

Files:

  • Modify: src/ZB.MOM.WW.MxGateway.Server/Sessions/ISessionManager.cs:42 (remove member)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs:191-197 (remove implementation)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs (~:1517-1545 remove ReadEventsAsync; ~:767-776 rewrite the "keep the two bodies in step" comment on MapWorkerEventsAsync — with the twin gone it now claims the single worker-event read path directly)
  • Modify: every test fake implementing ISessionManager (grep ISessionManager under src/ZB.MOM.WW.MxGateway.Tests/ — the as-built note in docs/plans/2026-08-15-deferred-remediation.md estimated ~15 touches; remove the member from each fake and any tests that exercised it through the interface)
  • Modify: docs/plans/2026-08-15-deferred-remediation.md as-built note "Task 3 — ReadEventsAsync retained" (append one line: removed by this plan, date)

Spec: ISessionManager.ReadEventsAsync has zero production call sites — only test fakes implement and exercise it. Remove the interface member, SessionManager's forwarder, and GatewaySession.ReadEventsAsync (verify with grep first that MapWorkerEventsAsync and the fakes are truly the only remaining references — if a production caller appears, STOP and report; do not force it). IWorkerClient.ReadEventsAsync / WorkerClient.ReadEventsAsync stay — that is the live worker-channel claim; most grep hits are that member, read carefully. Tests that existed solely to exercise the pass-through die with it; tests that used a fake's ReadEventsAsync as a convenience seam get rewired to the distributor path or removed if redundant — judgment call, state it in the commit body.

Steps: grep callers → edit → dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnxdotnet test src/ZB.MOM.WW.MxGateway.Tests/... --filter "FullyQualifiedName~GatewaySession", ~SessionManager, ~EventStreamService → commit refactor(sessions): remove the dead ISessionManager.ReadEventsAsync chain.


Task 2: Frame-writer lock-parking — awaited control-frame completion unparked from the winner's pass

Classification: high-risk Estimated implement time: ~10 min Parallelizable with: Task 1, Task 3, Task 5

Files:

  • Modify: src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs
  • Test: src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/ frame-writer tests (windev-run; write them now)
  • Modify: docs/WorkerFrameProtocol.md (completion-latency contract paragraph)
  • Modify: docs/plans/2026-08-15-deferred-remediation.md as-built note "Task 10" (append: lock-parking closed by this plan)

Spec: Today WriteAsync enqueues the frame, then unconditionally contends for _writeLock; a caller that loses the race stays parked in WaitAsync until the winning drainer releases the lock — even though the winner writes and flushes the loser's control frame mid-pass and completes its per-frame completion source at that moment (WorkerFrameWriter.cs:112-116 documents this parking as the deliberate residual). Close it: after enqueueing, the caller awaits its own frame's completion racing the lock acquisition — when the completion resolves first (frame written+flushed by the winner), the caller returns immediately; its abandoned lock-wait must not leak drain responsibility. Shape (implementer refines, invariants below are the contract):

  1. Await Task.WhenAny(frame.Completion.Task, lockWaitTask).
  2. Completion first → detach: register a continuation on lockWaitTask that, on acquisition, drains any queued frames if present and releases — the lock is never acquired-and-dropped, and a frame enqueued between the winner's last dequeue and its release still gets drained (the existing "drain everything you can see, then release" loop already covers most of this; the continuation is the backstop for the abandoned waiter).
  3. Lock first → drain as today.
  4. Cancellation during the combined wait keeps the existing tombstone semantics (WorkerFrameWriter.cs:104-110): tombstone only if a drainer hasn't claimed the frame; a claimed frame completes normally and cancellation is not surfaced for it.

Invariants that must hold (write a test for each): every enqueued frame is eventually written or tombstoned (no stranded frame when the completion-first path abandons its lock wait); completions still resolve only after write+flush; batch API (WriteBatchAsync) semantics unchanged; no double-drain / double-release; a control-frame caller racing a long event batch observes its completion before the batch drain finishes (the latency win this task exists for — assert with a gated slow-stream fake).

Steps: edit → macOS dotnet build of the shared-source projects is NOT possible for the worker — verify compile on windev (dotnet build src\ZB.MOM.WW.MxGateway.Worker\... -p:Platform=x86) via ssh windev before commit if feasible, else mark the commit "edited, windev-pending" and Task 10/11 gates it → commit perf(worker): unpark awaited control-frame writers from the winning drain pass.


Task 3: windev live-alarm probes — GUID identity and ALARM_RECORDS/@COUNT semantics

Classification: standard (investigation; no gateway code — findings doc only) Estimated implement time: ~15 min wall (timeboxed; may end "blocked") Parallelizable with: Task 1, Task 2, Task 4, Task 5

Files:

  • Read (on windev): src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs (Skip-gated probe; flip Skip=null locally on windev, never commit the flip)
  • Create: docs/AlarmProbeFindings.md (findings record, or the explicit "blocked" record)
  • Modify: src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs — comments ONLY, and only if a finding confirms/refutes a documented assumption (no behavior change in this task)

Spec: Two questions only a live rig answers:

  1. GUID identity semantics: is the alarm record GUID stable for one alarm instance across polls and across state transitions (UNACK→ACK→RTN)? ComputeTransitions keys the diff on GUID (latestSnapshot: Dictionary<Guid, ...>); if wnwrap mints a new GUID on a state change, a transition would read as clear+new instead.
  2. ALARM_RECORDS/@COUNT: when the fetch is capped (maxAlmCnt < actives), does the reply's COUNT attribute carry the total active count (a usable "more available" signal) or just the records-in-reply count? If it carries the total, IsTruncatedFetch can become exact instead of the ≥cap heuristic — feed that finding to Task 8.

Steps:

  1. ssh windev (lands in PowerShell; CI clone C:\build\mxaccessgw-ci; first build after a pull may fail on stale Contracts obj — clear src\ZB.MOM.WW.MxGateway.Contracts\obj,bin and rebuild, it is not a regression). Pull the branch.
  2. Determine whether live alarms exist or can be raised: check provider state; try driving a known alarmed attribute over its limit via a gateway write or a Galaxy test object. Timebox 15 minutes. If no alarm can be made active: write docs/AlarmProbeFindings.md recording exactly what was tried and that both questions remain open, commit, STOP.
  3. With ≥1 live alarm: run the probe (WnWrapConsumerProbeTests, Skip flipped locally, cap maxAlmCnt low, e.g. 12, to force truncation), ack the alarm, let it RTN, capture the XML across the transitions. Record: GUID per state, COUNT vs records-in-reply under the forced cap.
  4. Write findings + implications (for ComputeTransitions and IsTruncatedFetch) into docs/AlarmProbeFindings.md; adjust WnWrapAlarmConsumer comments where an assumption is now confirmed/refuted. Any behavioral fix the findings demand is reported to the orchestrator (feeds Task 8, or a follow-on task if it's transition-identity surgery) — not done here.
  5. Commit docs(alarms): wnwrap live-probe findings — GUID identity, ALARM_RECORDS COUNT .

Task 4: SEC-25 groundwork — DashboardTags on the API key, Tags on the session

Classification: high-risk Estimated implement time: ~8 min Parallelizable with: Task 2, Task 3, Task 5 (NOT Task 1 — both touch GatewaySession.cs and session fakes; run after Task 1 lands)

Files:

  • Modify: src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraints.cs (add IReadOnlyList<string> DashboardTags, default empty)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraintSerializer.cs (round-trip the new field; absent-in-JSON → empty — old rows keep deserializing)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCommandLineParser.cs + ApiKeyAdminCliRunner.cs + ApiKeyAdminCommand.cs + ApiKeyAdminListedKey.cs (CLI --dashboard-tags team-a,team-b on create-key — there is no update subcommand; shown in list output)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs (new public IReadOnlySet<string> Tags { get; }, set at construction from the owner key's effective constraints; empty = untagged)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs (OpenSession path copies ApiKeyIdentity.EffectiveConstraints.DashboardTags onto the session)
  • Test: serializer round-trip incl. legacy-JSON-without-field; CLI parse; session tag inheritance via the fake-worker harness
  • Modify: docs/Authorization.md (DashboardTags is dashboard-visibility-only, never a data-access constraint)

Spec: Implements §3 of docs/plans/2026-07-10-dashboard-session-acl-tst15.md. The §9 open call is settled per the design's own recommendation: the tag rides in the existing ApiKeyConstraints JSON blob — no SQLite schema migration. Tags are immutable per session, assigned once at OpenSession from the owning key. No enforcement in this task — the field and its plumbing only. Case handling: preserve tag strings as entered; comparisons later (Task 6) are ordinal-ignore-case — note that on the property doc.

Steps: edit → build NonWindows.slnx → targeted tests (~ApiKeyConstraint, ~ApiKeyAdmin, ~SessionManager) → commit feat(security): DashboardTags on API-key constraints; sessions inherit owner tags (SEC-25 groundwork).


Task 5: SEC-25 config — GroupToTag, UntaggedSessionVisibility, validator, mapper

Classification: standard Estimated implement time: ~6 min Parallelizable with: Task 1, Task 2, Task 3, Task 4

Files:

  • Modify: src/ZB.MOM.WW.MxGateway.Server/Configuration/DashboardOptions (Dictionary<string,string[]> GroupToTag ordinal-ignore-case; UntaggedSessionVisibility enum AdminOnly(default)|AllViewers) + GatewayOptionsValidator (keys non-empty, tag arrays non-null/non-empty entries, enum known — mirror the GroupToRole validation shape)
  • Create: group→tag mapper as a sibling of DashboardGroupRoleMapping (union of tags over the principal's LDAP groups; unknown group → nothing)
  • Test: validator accept/reject cases; mapper union/unknown-group/case-insensitivity
  • Modify: docs/GatewayConfiguration.md (Dashboard:GroupToTag, Dashboard:UntaggedSessionVisibility rows)

Spec: §3.2 of the design doc, exactly. No coupling to GroupToRole — a group may appear in one, the other, or both.

Steps: edit → build → --filter "FullyQualifiedName~GatewayOptionsValidator" + mapper tests → commit feat(dashboard): GroupToTag / UntaggedSessionVisibility config (SEC-25).


Task 6: SEC-25 enforcement — ACL service, token/cookie tag claims, both subscribe seams gated

Classification: high-risk Estimated implement time: ~10 min Parallelizable with: none (needs Tasks 4 + 5)

Files:

  • Create: src/ZB.MOM.WW.MxGateway.Server/Dashboard/ IDashboardSessionAcl + implementation (CanViewSession(ClaimsPrincipal, string sessionId); DI singleton)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs (payload gains string[]? Tags; mint stamps resolved granted tags as zb:dashboardtag claims; validate rehydrates them)
  • Modify: dashboard cookie principal creation (DashboardAuthenticator.CreatePrincipal per the design doc — grep for the exact site) so cookie-authenticated circuits carry the same tag claims
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/EventsHub.cs:42-60 (SubscribeSession checks the ACL; deny → HubException, no group join; remove the TODO(per-session-acl))
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/SessionDetailsPage.razor (in-process seam gate: resolve the circuit principal via AuthenticationStateProvider, check IDashboardSessionAcl.CanViewSession before Subscribe(SessionId); deny → render "Not authorized for this session's events." in place of the event panel, no subscription created)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs (register the ACL)
  • Test: DashboardSessionAclTests (admin bypass; session-not-found deny; untagged under both visibility values; tag intersection match/non-match; principal-with-no-claims = empty-grant Viewer); HubTokenService tag round-trip; EventsHub deny-does-not-join; SessionDetailsPage gate (deny renders banner, allow subscribes — follow the existing bUnit/component test idiom in Gateway/Dashboard/)
  • Modify: docs/plans/2026-07-10-dashboard-session-acl-tst15.md — add an as-built note: the 2026-08 in-process feed refactor created a second seam (IDashboardSessionEventSubscriber used by SessionDetailsPage); "gating at join is sufficient — no second seam" (§4) no longer held, so the same ACL now gates both.

Spec: §§2, 4, 4.1 of the design doc, with one deviation the doc predates: the dashboard now also consumes session events in-process (SessionDetailsPage → Subscribe(sessionId)), so enforcement lands in two places — the hub SubscribeSession (remote surface) and the page-side check before the in-process subscribe. ACL decision order: Admin role → allow; session unknown → deny; untagged → UntaggedSessionVisibility == AllViewers; else session.Tags ∩ grantedTags ≠ ∅ (ordinal-ignore-case). Anonymous localhost = Viewer with empty grant (§4.1) — this tightens SEC-02's loopback posture by design; DisableLogin auto-login carries both roles → admin bypass, unchanged. Fail closed everywhere. Never log tag grants alongside credentials.

Steps: edit → build → --filter "~DashboardSessionAcl", ~EventsHub, ~HubTokenService, ~SessionDetailsPage → commit feat(dashboard): per-session event ACL on both subscribe seams (SEC-25 / TST-15).


Task 7: SEC-25 closure — live-LDAP tests, docs sweep, design-doc status

Classification: standard Estimated implement time: ~7 min Parallelizable with: Task 8, Task 9

Files:

  • Test: extend DashboardLdapLiveTests (gated MXGATEWAY_RUN_LIVE_LDAP_TESTS=1): map the existing GLAuth groups via config — gw-viewer's group granted team-a through Dashboard:GroupToTag, a team-a-tagged session allows, a team-b-tagged session denies; multi-role (Admin) allows both. Use the existing GLAuth users/groups (glauth.md) — no GLAuth server change.
  • Modify: docs/Sessions.md (session-tag model, dashboard event visibility), gateway.md dashboard section, CLAUDE.md dashboard-auth paragraph (Viewer is tag-scoped; Admin sees all; anonymous localhost = empty-grant Viewer), glauth.md (test-grant mapping used by the live tests — config-side only), docs/GatewayDashboardDesign.md (remove/replace the two "SEC-25 outstanding" passages at ~:277 and ~:699 — the ACL now exists; describe it)
  • Modify: docs/plans/2026-07-10-dashboard-session-acl-tst15.md header — Status: Implemented (this plan, date)
  • Modify: archreview tracking rows for TST-15/SEC-25 if a Not started row exists (mark Done with commit ref)

Spec: §§78 of the design doc. Live tests are opt-in-gated exactly like the existing LiveLdapFactAttribute suite; they must skip cleanly where GLAuth is unreachable.

Steps: edit → build → run unit portions + (if GLAuth reachable from this Mac, MXGATEWAY_RUN_LIVE_LDAP_TESTS=1 run; else record skipped per docs/GatewayTesting.md) → commit test(dashboard)+docs: SEC-25 live-LDAP ACL coverage; design marked implemented.


Task 8: Alarm-truncation degraded-status signal — proto + worker + gateway + dashboard

Classification: high-risk Estimated implement time: ~10 min Parallelizable with: Task 7 (after Task 3's findings are in hand, or Task 3 recorded "blocked")

Files:

  • Modify: src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.protoActiveAlarmSnapshot gains bool from_truncated_snapshot (next free field number); QueryActiveAlarmsReplyPayload gains bool snapshot_truncated. Additive only; comment style per the file.
  • Regenerate: dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj (never hand-edit Generated/)
  • Modify: src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs (expose the retained truncated state, e.g. bool LastSnapshotTruncated, maintained where ApplySnapshotUpdate already receives truncated) + IMxAccessAlarmConsumer.cs + the QueryActiveAlarms reply builder in MxAccessCommandExecutor.cs/AlarmDispatcher.cs (stamp both new fields)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs + IGatewayAlarmService.cs (+ its implementation) — propagate degraded state to the dashboard snapshot model (DashboardActiveAlarm.cs or a sibling flag on the alarm snapshot the service exposes)
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs — the public QueryActiveAlarms stream carries from_truncated_snapshot through
  • Modify: src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/AlarmsPage.razor — degraded banner ("Alarm snapshot may be incomplete — provider returned a capped fetch") driven by the service flag; reuse the existing alert-banner idiom, do not disturb the poll-loop structure landed in dcbec97
  • Test: worker XML tests (WnWrapAlarmConsumerXmlTests idiom) for the exposed state; gateway fake-worker test that a truncated reply surfaces the flag end-to-end; AlarmsPage banner test
  • Modify: gateway.md (alarm surface paragraph), docs/DesignDecisions.md (one entry: why per-record flag + reply-payload flag, wire-compatibility, absence-inference already suppressed worker-side since the truncation cliff fix)

Spec: The truncation cliff fix (perf plan Task 23) made transitions safe but silent: only a worker-stderr warning says the snapshot is degraded. Give it a structural signal: worker stamps truncation state into the QueryActiveAlarms reply; gateway propagates it to the public stream (per-record flag — the RPC returns a bare stream ActiveAlarmSnapshot with no envelope, so a per-record boolean is the only additive carrier) and to the dashboard alarm service; AlarmsPage shows the degraded banner. If Task 3 found that ALARM_RECORDS/@COUNT carries the true total, also replace the IsTruncatedFetch ≥cap heuristic with the exact comparison in the same commit (parse @COUNT, compare to records delivered; keep the heuristic as fallback when the attribute is absent) — cite the findings doc. If Task 3 was blocked, keep the heuristic untouched and say so in the commit body. MXAccess parity note: this flag describes our fetch mechanics, not provider behavior — it is additive gateway metadata, not a parity deviation.

Steps: proto edit → regenerate → worker+gateway edits → build NonWindows.slnx + gateway tests (~Alarm, ~AlarmsPage) — worker compile/tests defer to the Task 11 windev gate → commit feat(alarms): structural degraded-status signal for truncated alarm snapshots.


Task 9: Client regeneration + rebuild for the new alarm fields

Classification: standard Estimated implement time: ~8 min Parallelizable with: Task 7 (needs Task 8's proto committed)

Files:

  • Regenerate per each client's README: clients/dotnet, clients/python, clients/rust, clients/java, Go (clients/go / mxgw-go layout — follow its README)
  • Modify: each client's README alarm section IF it documents the snapshot shape (one line: from_truncated_snapshot means the provider fetch was capped and absent-implies-cleared inference was suspended)
  • Test: dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx + tests; python -m pytest in clients/python; cargo fmt && cargo check --workspace && cargo test --workspace && cargo clippy --all-targets -- -D warnings in clients/rust; gradle test in clients/java; gofmt/go build ./.../go test ./... in clients/go

Spec: Additive proto fields — codegen carries them; no typed wrapper work unless a client already wraps ActiveAlarmSnapshot in a typed model (then add the field there too, following how ReplayGap surfacing was done per-client). All five clients must build and test green. Use the build lock around each build/test.

Steps: regenerate → build/test each client → commit chore(clients): regenerate for alarm truncation fields; READMEs note the degraded flag.


Task 10: Phase gate — full gateway suite on macOS

Classification: high-risk (gate) Estimated implement time: ~5 min wall Parallelizable with: none (after Tasks 1, 49)

Full dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj (with the build lock) + dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx. Any failure: fix forward with a scoped commit, re-run the failed filter, then re-run the full suite once.


Task 11: windev gate — full Windows verification

Classification: high-risk (gate) Estimated implement time: ~15 min wall Parallelizable with: none (last verification)

On windev (ssh windev, PowerShell, clone C:\build\mxaccessgw-ci; expect the stale Contracts-obj first-build quirk — clear src\ZB.MOM.WW.MxGateway.Contracts\obj,bin and rebuild, not a regression):

git pull   # the feat/deferred-closeout branch
dotnet build src\ZB.MOM.WW.MxGateway.slnx
dotnet build src\ZB.MOM.WW.MxGateway.Worker\ZB.MOM.WW.MxGateway.Worker.csproj -p:Platform=x86
dotnet test src\ZB.MOM.WW.MxGateway.Worker.Tests\ZB.MOM.WW.MxGateway.Worker.Tests.csproj -p:Platform=x86
dotnet test src\ZB.MOM.WW.MxGateway.Tests\ZB.MOM.WW.MxGateway.Tests.csproj

This is the first verification of Task 2's writer restructure and Task 8's worker-side changes — expect iteration; fix on the Mac, commit, re-run the failed leg. Live MXAccess smoke (MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1) if provider state allows; else record why skipped.


Task 12: Wrap-up — closure notes, umbrella check, final review

Classification: small Estimated implement time: ~5 min Parallelizable with: none (last)

  • Append closure lines to docs/plans/2026-08-15-deferred-remediation.md's out-of-scope table (each row: resolved by this plan + date, or — for the probe row if Task 3 blocked — "probes attempted , blocked on live alarms, findings doc records the attempt").
  • Check ../scadaproj/CLAUDE.md's MxAccessGateway entry: the proto contents changed (additive fields) — update only if the index records a fact that changed (expected: no change needed; verify, don't assume).
  • Update .tasks.json; update auto-memory (perf-remediation-branch.md or successor).
  • Dispatch the final integration code review (Opus) over git diff main..feat/deferred-closeout. Merge remains the user's decision.

Explicitly out of scope

Item Why
gRPC "all-sessions admin scope" (epic Task 16's gRPC half) Stays with the session-resilience epic; the design doc itself scopes it out of TST-15.
Session-list row filtering by tag on SessionsPage The approved design gates event subscription only; list-metadata visibility is a separate call.
jti-denylist token revocation Rejected in the design (§10.3); 5-min token lifetime bounds grant staleness.
Behavioral rework of ComputeTransitions identity if the GUID probe refutes stability Needs its own reviewed design against real captures; Task 3 records the evidence, a follow-on plan acts on it.
MxAccessWriteCompletionCache clone Consciously kept (prior plan Task 12.5); unchanged posture.

As-built notes (execution record, 2026-08-17)

All 12 tasks completed on feat/deferred-closeout; every implement went through its classification's review chain to Approved. Verification: macOS build 0W/0E + gateway 1123/1123; windev full slnx 0W/0E, worker x86 523/523 (11 standing opt-in skips), gateway 1123/1123, live MXAccess smoke 8/8 — green first try, no stale-obj quirk.

  • Task 3 ended blocked, with evidence. The rig refuses the alarm-condition writes (SecurityError from the responding automation object — the test UDAs need AuthenticateUser+WriteSecured or the in-engine flip script that drove the 2026-05-01 capture). docs/AlarmProbeFindings.md records what was tried, the partial GUID answer (active→returned leg confirmed), and the unblock paths.
  • Commit 693a78d contains two tasks' work. A concurrent implementer's git commit without pathspecs swept Task 6's staged ACL files into Task 8's alarm commit. Nothing was lost; both halves were reviewed separately and their fix rounds (7ec0b35, b9fb0dd) are clean single-task commits. Process rule tightened mid-run: pathspecs on the commit itself, not just the add.
  • Task 8's atomicity fix went structural. Review found the flag/snapshot pairing relied on STA serialization while claiming a read-order guarantee; the fix made SnapshotActiveAlarms(out bool truncated) the only accessor, so the unpaired read is unexpressible.
  • Task 6 review caught a real regression (async attach re-entrancy under rapid navigation) — closed with a generation guard and a mutation-verified interleaving test.
  • Live-LDAP ACL tests ran green against the shared GLAuth (7/7, plus 7/7 clean-skip without the env gate). multi-role sits in both GLAuth groups, so the Admin-bypass test asserts the sessions a dropped bypass would actually lose (team-b + untagged).
  • Task 9 found and fixed pre-existing drift: clients/rust/protos/mxaccess_gateway.proto had diverged from Contracts (masked by the in-repo build path); refreshed byte-identical, and the client protoset descriptors were regenerated.

Follow-ups recorded, not started:

  • IGatewayAlarmService.StreamAsync / AlarmFeedMessage does not carry the truncation signal — live central-feed consumers (lmxopcua, ScadaBridge) cannot see snapshot degradation; add if those consumers need completeness reasoning.
  • No guard keeps clients/rust/protos/ in sync with Contracts (a diff check in scripts/check-codegen.ps1 would close it).
  • EffectiveDashboardConfiguration (dashboard settings page) doesn't display GroupToTag / UntaggedSessionVisibility, though it shows GroupToRole.
  • The alarm probes' remaining questions (ack-leg GUID stability, @COUNT semantics) unblock via the paths in docs/AlarmProbeFindings.md.

Execution notes for the orchestrator

  • Branch feat/deferred-closeout off main before Task 1.
  • Opus implementers per user instruction; reviewer chain per Classification (high-risk = spec-reviewer serial then code-reviewer; standard = parallel pair; small = code-reviewer only).
  • Waves: Wave 1: 1, 2, 3, 5 · Wave 2: 4 (after 1) · Wave 3: 6 (after 4+5), 8 (after 3 findings/blocked) · Wave 4: 7, 9 · then 10 → 11 → 12. Per-task Parallelizable with fields are the contract.
  • Each implementer gets its full task text + the ground rules block. Files: is the scope contract.
  • Task 3 and Task 11 run against windev over ssh windev (PowerShell); psbridge is available as fallback transport.