diff --git a/docs/plans/2026-08-17-deferred-closeout.md b/docs/plans/2026-08-17-deferred-closeout.md new file mode 100644 index 0000000..817295d --- /dev/null +++ b/docs/plans/2026-08-17-deferred-closeout.md @@ -0,0 +1,398 @@ +# 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 `. 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.slnx` → +`dotnet 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`); 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. 1–2, 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 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/update; shown in list output) +- Modify: `src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs` (new `public IReadOnlySet 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 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:** §§7–8 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.proto` — `ActiveAlarmSnapshot` 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, 4–9) + +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): + +```powershell +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. | + +--- + +## 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. diff --git a/docs/plans/2026-08-17-deferred-closeout.md.tasks.json b/docs/plans/2026-08-17-deferred-closeout.md.tasks.json new file mode 100644 index 0000000..84d07f6 --- /dev/null +++ b/docs/plans/2026-08-17-deferred-closeout.md.tasks.json @@ -0,0 +1,18 @@ +{ + "planPath": "docs/plans/2026-08-17-deferred-closeout.md", + "tasks": [ + { "id": 1, "subject": "Task 1: Remove dead ISessionManager.ReadEventsAsync chain", "status": "pending" }, + { "id": 2, "subject": "Task 2: Frame-writer lock-parking — unpark awaited control-frame completion", "status": "pending" }, + { "id": 3, "subject": "Task 3: windev live-alarm probes — GUID identity, ALARM_RECORDS/@COUNT", "status": "pending" }, + { "id": 4, "subject": "Task 4: SEC-25 groundwork — DashboardTags on key, Tags on session", "status": "pending", "blockedBy": [1] }, + { "id": 5, "subject": "Task 5: SEC-25 config — GroupToTag, UntaggedSessionVisibility, validator, mapper", "status": "pending" }, + { "id": 6, "subject": "Task 6: SEC-25 enforcement — ACL, token/cookie tag claims, both seams gated", "status": "pending", "blockedBy": [4, 5] }, + { "id": 7, "subject": "Task 7: SEC-25 closure — live-LDAP tests, docs sweep, design-doc status", "status": "pending", "blockedBy": [6] }, + { "id": 8, "subject": "Task 8: Alarm-truncation degraded-status signal — proto + worker + gateway + dashboard", "status": "pending", "blockedBy": [3] }, + { "id": 9, "subject": "Task 9: Client regeneration + rebuild for new alarm fields", "status": "pending", "blockedBy": [8] }, + { "id": 10, "subject": "Task 10: Phase gate — full gateway suite on macOS", "status": "pending", "blockedBy": [1, 4, 5, 6, 7, 8, 9] }, + { "id": 11, "subject": "Task 11: windev gate — full Windows verification", "status": "pending", "blockedBy": [2, 10] }, + { "id": 12, "subject": "Task 12: Wrap-up — closure notes, umbrella check, final review", "status": "pending", "blockedBy": [11] } + ], + "lastUpdated": "2026-08-17T00:00:00Z" +}