docs(plans): deferred-closeout plan — SEC-25, truncation signal, lock-parking, dead chain, alarm probes
This commit is contained in:
@@ -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 <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.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<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. 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<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/update; 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:** §§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 <date>, 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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user