docs(plan): re-scope script-log Layer 2 inbound-ack into T17-T24
The original single T17 (inbound method dispatch + ack plumbing) proved on a 2026-06-11 deep dive to be four hard problems: roles on the session identity (T17), node-manager command router + AlarmAck veto + alarm-commands DPS topic (T18), host-actor inbound handler (T19), and delta-gate double-emit (T20). Old T18->T21 (AdminUI), old T19 split into T22 (Client.CLI feature) + T23 (verify), old T20->T24. Adds the Layer 2 design-decisions preamble.
This commit is contained in:
@@ -406,58 +406,264 @@ mapping/coverage where feasible; behaviour proven in T19.
|
||||
|
||||
---
|
||||
|
||||
### Task 17: Inbound method dispatch + ack plumbing
|
||||
# LAYER 2 — inbound client ack/shelve (re-scoped 2026-06-11)
|
||||
|
||||
**Classification:** high-risk · **~5 min** · **Parallelizable with:** Task 15 (depends T14)
|
||||
> **Status:** T0–T16 are **merged to `master`** (Layers 0+1 live-verified; Layer 2 Part-9
|
||||
> nodes/state/events done). The original single **T17 "Inbound method dispatch + ack plumbing"**
|
||||
> (`high-risk · ~5 min`) proved to be **four separate hard problems**, each its own task. After a
|
||||
> 2026-06-11 deep dive into the real code, T17 is split into **T17–T20** and the old T18–T20 shift
|
||||
> to **T21–T24** (old T19's Client.CLI work also grew a feature half, T22). This is the deferred
|
||||
> "fresh piece": branch off the **current** `master` (`git switch -c feat/scriptlog-alarm-ack`),
|
||||
> not the old `feat/scriptlog-alarm-runtime` base.
|
||||
|
||||
**Files:** `OtOpcUaNodeManager.cs` (wire Acknowledge/Confirm/AddComment/OneShotShelve/
|
||||
TimedShelve/Unshelve handlers → route to a control-plane message → `ScriptedAlarmHostActor`
|
||||
→ `engine.<Op>Async(conditionId, principal, comment, ct)`); the security gate at the
|
||||
`AlarmAck` tier (reuse the LDAP-group→OPC-UA-permission map). Tests: method→engine routing
|
||||
with a fake engine; permission gate allows/denies by tier.
|
||||
### Layer 2 design decisions (resolved in the re-scope deep dive)
|
||||
|
||||
These are the load-bearing findings the new tasks rest on — verified against the current code, not
|
||||
the original recon's assumptions:
|
||||
|
||||
1. **Topology — same-node co-location, multi-node ownership.** The OPC UA SDK server (+
|
||||
`OtOpcUaNodeManager`) and the `ScriptedAlarmHostActor` are **both spawned on every
|
||||
driver-role node** in the same `ActorSystem` (`OtOpcUaServerHostedService` +
|
||||
`DriverHostActor.SpawnScriptedAlarmHost`). So per node they're co-located and an in-process
|
||||
`Tell` would reach the local host. **But** in a multi-driver-node cluster each node owns a
|
||||
**disjoint** subset of alarms (its resident equipment, via the T6 artifact equipment-filter),
|
||||
and a client connects to **one** node's server. Whether that node owns the alarm the client
|
||||
acks is **not guaranteed**. **Decision:** route inbound commands over a new DPS topic
|
||||
`alarm-commands` (mirrors `alerts`/`script-logs`), and have each `ScriptedAlarmHostActor`
|
||||
**ignore commands for alarmIds its engine doesn't own**. This works same-node and cross-node
|
||||
with one mechanism. (Open item to confirm in T18: whether each node's address space is
|
||||
partitioned to its own equipment or replicated — if partitioned, a client can only ever ack
|
||||
local alarms and the DPS broadcast is still correct, just always locally satisfied.)
|
||||
|
||||
2. **The node manager has no Akka handle and must stay that way.** `OtOpcUaNodeManager(server,
|
||||
configuration)` (`OtOpcUaSdkServer.CreateMasterNodeManager`) holds **no** `IActorRef` /
|
||||
`ActorSystem` / DI. The existing **forward** seam is `OpcUaPublishActor → IOpcUaAddressSpaceSink
|
||||
(DeferredAddressSpaceSink → SdkAddressSpaceSink → node manager)`. For the **reverse** path the
|
||||
node manager gets a **settable command-router delegate** (`Action<AlarmCommand>`), wired at
|
||||
boot by `OtOpcUaServerHostedService` (which *does* have the DPS mediator) to publish onto
|
||||
`alarm-commands`. The node manager itself never touches Akka.
|
||||
|
||||
3. **No explicit re-projection after an engine op.** Every `ScriptedAlarmEngine` op
|
||||
(`AcknowledgeAsync`/`ConfirmAsync`/`OneShotShelveAsync`/`TimedShelveAsync`/`UnshelveAsync`/
|
||||
`EnableAsync`/`DisableAsync`/`AddCommentAsync` — all exist, signatures verified) raises the
|
||||
engine's `OnEvent`, which the host's **existing** `OnEngineEmission` already projects to the
|
||||
node. So the inbound handler just calls the op and awaits — the ack visibly updates the node
|
||||
for free. This makes T19 small.
|
||||
|
||||
4. **Roles are dropped at the impersonation seam.** `OpcUaApplicationHost.cs:292` does
|
||||
`args.Identity = new UserIdentity(token)` and **discards** `result.Roles` (only logs them at
|
||||
:293). `OpcUaUserAuthResult.Roles` is `IReadOnlyList<string>` (`ReadOnly`/`WriteOperate`/
|
||||
`WriteTune`/`WriteConfigure`/`AlarmAck`); there is an `OpcUaOperation` enum
|
||||
(`Core.Abstractions`) with `AlarmAcknowledge`/`AlarmConfirm`/`AlarmShelve`, but **no role is
|
||||
consulted anywhere post-auth today** (writes aren't gated either — this is greenfield, not a
|
||||
pattern to copy). **Risk (drives T17 being its own task):** it is **unconfirmed** that a custom
|
||||
`UserIdentity` subclass survives the SDK round-trip back to `context.UserIdentity` inside a
|
||||
method handler. T17 must *prove* the round-trip (integration assertion); fallback is populating
|
||||
`GrantedRoleIds` (`NodeIdCollection`) by mapping role strings → role NodeIds, which is more work.
|
||||
|
||||
5. **Double-emit is real, and delta-gating resolves it.** `WriteAlarmCondition` calls
|
||||
`ReportConditionEvent` **unconditionally** (`OtOpcUaNodeManager.cs:156`); the node manager keeps
|
||||
**no** previous snapshot. Once inbound acks route through the engine, the SDK's own
|
||||
`OnAcknowledgeCalled` auto-fires event E2 (applying acked state to the node) **and** the engine
|
||||
round-trip re-projects → would fire E3. Because the SDK applies the acked state *before* the
|
||||
async engine round-trip completes, **delta-gating `WriteAlarmCondition` against the node's
|
||||
current state** suppresses E3 (no delta) while still firing on genuine engine-driven
|
||||
transitions. That's T20. (Fallback if it proves racy: the correlation-suppression option already
|
||||
sketched in the `:190-198` in-code note — skip engine re-projection for inbound-originated
|
||||
transitions.)
|
||||
|
||||
---
|
||||
|
||||
### Task 18: AdminUI ack/shelve control
|
||||
### Task 17: Carry LDAP roles onto the OPC UA session identity
|
||||
|
||||
**Classification:** standard · **~4 min** · **Parallelizable with:** none (depends T17)
|
||||
**Classification:** high-risk · **~5 min** · **Parallelizable with:** Task 22
|
||||
|
||||
**Files:** `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/ScriptedAlarms.razor`
|
||||
(+ a control-plane command service) — ack/shelve buttons route to the same engine ops as
|
||||
T17. Tests: the control-plane command service (no bUnit; live-verify the razor in T19).
|
||||
**Files:**
|
||||
- Create: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/RoleCarryingUserIdentity.cs`
|
||||
(`: UserIdentity`, adds `IReadOnlyList<string> Roles`).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs:292`
|
||||
(`args.Identity = new RoleCarryingUserIdentity(token, result.Roles)`).
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostImpersonationTests.cs`
|
||||
(existing home for `HandleImpersonation`).
|
||||
|
||||
**Steps:**
|
||||
1. **Round-trip spike FIRST (de-risk the whole task).** Before building anything, confirm the SDK
|
||||
preserves a custom `IUserIdentity` instance: in a booted in-process server test (mirror
|
||||
`SdkAddressSpaceSinkTests`' server fixture), set `args.Identity` to a sentinel subclass during
|
||||
impersonation and assert a method handler reads it back via
|
||||
`(context as ISessionOperationContext)?.UserIdentity` **as that subclass**. If it does NOT
|
||||
survive (SDK wraps/strips it), STOP and switch to the `GrantedRoleIds` approach — surface this
|
||||
as a scope change, don't silently expand.
|
||||
2. Failing unit test: `HandleImpersonation` on a successful auth sets `args.Identity` to a
|
||||
`RoleCarryingUserIdentity` whose `Roles` equals `result.Roles` (and the existing
|
||||
identity/denial/anonymous tests still pass).
|
||||
3. Implement `RoleCarryingUserIdentity` + the one-line `:292` swap.
|
||||
4. Run (`OpcUaServer.Tests` + the impersonation tests) + commit by path.
|
||||
|
||||
> Security-path change → high-risk. Touches only the identity construction; no auth-decision logic
|
||||
> changes (roles were already resolved, just discarded). Do **not** change `IOpcUaUserAuthenticator`
|
||||
> or the LDAP bind.
|
||||
|
||||
---
|
||||
|
||||
### Task 19: Live-verify Layer 2 (Client.CLI)
|
||||
### Task 18: Node-manager command router + `AlarmAck` veto gate + `alarm-commands` topic
|
||||
|
||||
**Classification:** verification · **Parallelizable with:** none (depends T16, T17, T18)
|
||||
**Classification:** high-risk · **~5 min** · **Parallelizable with:** none (depends T17)
|
||||
|
||||
**Steps:** with docker-dev up, use `Client.CLI` `alarms` + `subscribe` to confirm a real
|
||||
condition appears, fires events on transition, and a client `Acknowledge` round-trips
|
||||
(state flips, event fires, persists). Confirm AdminUI ack does the same. User drives.
|
||||
**Files:**
|
||||
- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/AlarmCommand.cs`
|
||||
(`record AlarmCommand(string AlarmId, string Operation, string User, string? Comment, DateTime? UnshelveAtUtc)`;
|
||||
`Operation` ∈ Acknowledge/Confirm/OneShotShelve/TimedShelve/Unshelve/Enable/Disable/AddComment).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs` — add a settable
|
||||
`Action<AlarmCommand>? AlarmCommandRouter`; in `MaterialiseAlarmCondition` (after `Create` +
|
||||
initial state, before `AddChild`) wire `alarm.OnAcknowledge`/`OnConfirm`/`OnAddComment`/
|
||||
`OnShelve`/`OnTimedUnshelve`. Each delegate: (a) read principal via
|
||||
`(context as ISessionOperationContext)?.UserIdentity as RoleCarryingUserIdentity`, **gate on
|
||||
`AlarmAck`** → return `StatusCodes.BadUserAccessDenied` if absent; (b) invoke `AlarmCommandRouter`
|
||||
with the mapped `AlarmCommand` (so the engine updates the **domain** store + audit + alerts
|
||||
historization); (c) return `ServiceResult.Good` so the SDK applies node state + auto-fires (the
|
||||
engine re-projection is de-duped in T20).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaSdkServer.cs` (pass-through to expose
|
||||
the router setter on the node manager).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs` (after the server
|
||||
starts + node manager exists: resolve the DPS mediator, set the router to
|
||||
`mediator.Tell(new Publish(ScriptedAlarmHostActor.AlarmCommandsTopic, cmd))`).
|
||||
- Add the topic const `AlarmCommandsTopic = "alarm-commands"` on `ScriptedAlarmHostActor` (used here
|
||||
and in T19).
|
||||
- Test: `OpcUaServer.Tests` — veto gate allows with `AlarmAck` / denies without (drive a wired
|
||||
condition's `OnAcknowledge` with a `RoleCarryingUserIdentity` context); router invoked with the
|
||||
correctly-mapped `AlarmCommand` (fake `Action`).
|
||||
|
||||
**Steps:** TDD the gate + router-mapping in the node manager; then the SDK-server pass-through; then
|
||||
the hosted-service wiring (no unit test for the boot wiring — exercised by T23 live-verify). Commit
|
||||
by path. **Serialize with T20** (both touch `OtOpcUaNodeManager.cs`).
|
||||
|
||||
---
|
||||
|
||||
### Task 20: Docs + finish
|
||||
### Task 19: `ScriptedAlarmHostActor` inbound command handler
|
||||
|
||||
**Classification:** standard · **~4 min** · **Parallelizable with:** Task 20 (depends T18)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs` —
|
||||
subscribe to `AlarmCommandsTopic` in `PreStart` (alongside the existing `_mediator` use);
|
||||
`Receive<AlarmCommand>(OnAlarmCommand)`; `OnAlarmCommand` is `async void`, switches on
|
||||
`Operation` → the matching `engine.<Op>Async(AlarmId, User, …, CancellationToken.None)`.
|
||||
**Ownership filter:** if the engine doesn't own `AlarmId`, no-op (multi-node broadcast). Catch +
|
||||
log op failures (mirror `OnLoadFailed`). **No explicit re-projection** — the engine's `OnEvent`
|
||||
drives the existing `OnEngineEmission` → node update.
|
||||
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmHostActorTests.cs`
|
||||
(extend) — TestKit: an `AlarmCommand{Operation="Acknowledge"}` for a loaded alarm calls the
|
||||
engine's `AcknowledgeAsync` (fake/probe engine seam); an unknown `AlarmId` is ignored;
|
||||
`TimedShelve` without `UnshelveAtUtc` is rejected/logged, not thrown.
|
||||
|
||||
**Steps:** TDD via the existing host-actor test seam; run `dotnet test --filter ScriptedAlarmHostActor`;
|
||||
commit by path.
|
||||
|
||||
---
|
||||
|
||||
### Task 20: Delta-gate event firing (kill the inbound double-emit)
|
||||
|
||||
**Classification:** high-risk · **~4 min** · **Parallelizable with:** Task 19 (depends T18)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs` — keep a
|
||||
`ConcurrentDictionary<string, AlarmConditionSnapshot> _lastAlarmState`; in `WriteAlarmCondition`,
|
||||
after projecting, compare the new `state` to the stored snapshot and **only call
|
||||
`ReportConditionEvent` when it differs** (then store it). Replace the now-stale `:151-156`
|
||||
"fire exactly one event" comment + tighten the `:190-198` double-emit note to "resolved by
|
||||
delta-gate".
|
||||
- Test: `tests/.../OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs` (extend) — two identical
|
||||
`WriteAlarmCondition` calls fire the condition event **once**; a changed call fires again.
|
||||
(Assert via an event-count probe / monitored-item on the booted server fixture.)
|
||||
|
||||
**Steps:** TDD the delta-gate; run `OpcUaServer.Tests`; commit by path. If the booted-server test
|
||||
can't cleanly count events, fall back to asserting the gate's decision via a seam and prove
|
||||
end-to-end in T23. **Serialize after T18** (same file).
|
||||
|
||||
---
|
||||
|
||||
### Task 21: AdminUI ack/shelve control
|
||||
|
||||
**Classification:** standard · **~5 min** · **Parallelizable with:** none (depends T19)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Admin/AcknowledgeAlarmCommand.cs` +
|
||||
`ShelveAlarmCommand.cs` (control-plane messages, mirror `StartDeployment`'s shape with a
|
||||
`CorrelationId`).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs`
|
||||
(the existing admin-pinned cluster singleton) — `ReceiveAsync` handlers that publish onto
|
||||
`alarm-commands` (reusing T18's topic + the host's ownership filter → the singleton solves
|
||||
cross-node routing for the AdminUI path too).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminOperationsClient.cs`
|
||||
(+ its interface) — `AcknowledgeAlarmAsync` / `ShelveAlarmAsync` (mirror `StartDeploymentAsync`).
|
||||
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Alerts.razor` — per-row
|
||||
Acknowledge / Shelve buttons → `IAdminOperationsClient`; operator from
|
||||
`AuthState … User.Identity?.Name`.
|
||||
- Test: the control-plane command service + the new `AdminOperationsActor` handlers (TestKit / Ask).
|
||||
**No bUnit** — the razor is proven in T23.
|
||||
|
||||
**Steps:** TDD the messages + actor handlers + client; wire the razor; run + commit by path.
|
||||
|
||||
---
|
||||
|
||||
### Task 22: Client.CLI ack / confirm / shelve commands
|
||||
|
||||
**Classification:** standard · **~5 min** · **Parallelizable with:** Task 17–T21 (disjoint `Client.*`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs` +
|
||||
`OpcUaClientService.cs` — `AcknowledgeAlarmAsync` already declared (no command wires it yet); add
|
||||
`ConfirmAlarmAsync` + `ShelveAlarmAsync` (call the SDK `Confirm`/`OneShotShelve`/`TimedShelve`/
|
||||
`Unshelve` methods on the condition).
|
||||
- Create: `src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/{Acknowledge,Confirm,Shelve}Command.cs`
|
||||
(`--node`, `--event-id`, `--comment`; shelve adds `--kind OneShot|Timed --unshelve-at`).
|
||||
- Test: unit-test what's pure (arg→request mapping); the live round-trip is T23.
|
||||
|
||||
**Steps:** add the service methods + CLI commands; build; commit by path. This is **net-new client
|
||||
feature work** (the reason old "T19 verification" couldn't just be a verify pass).
|
||||
|
||||
---
|
||||
|
||||
### Task 23: Live-verify Layer 2 end-to-end
|
||||
|
||||
**Classification:** verification · **Parallelizable with:** none (depends T18, T19, T20, T21, T22)
|
||||
|
||||
**Steps:** docker-dev up. Use the **already-deployed `t12-overheat`** alarm (rig state, below) as the
|
||||
live condition. With `Client.CLI`: `alarms --refresh` shows the real condition; drive
|
||||
`TestMachine_002.TestChangingInt` past the predicate so it fires an event on transition; call the new
|
||||
`acknowledge` command → confirm the ack **round-trips** (node `AckedState` flips, exactly **one** ack
|
||||
event fires — no double-emit, T20 — state **persists** across a node restart). Repeat the ack from
|
||||
the AdminUI `/alerts` buttons (T21) and confirm parity. Verify the `AlarmAck` gate: a user **without**
|
||||
`AlarmAck` is denied (`BadUserAccessDenied`). **Agent does not sign in** — user drives. Defects → new
|
||||
fix tasks.
|
||||
|
||||
---
|
||||
|
||||
### Task 24: Docs + cleanup + finish branch
|
||||
|
||||
**Classification:** small · **~5 min** · **Parallelizable with:** none (depends all)
|
||||
|
||||
**Files:** update `docs/ScriptedAlarms.md`, `docs/VirtualTags.md`, `docs/v2/Runtime.md`
|
||||
(F8/F9 now wired), **correct the stale `docs/v2/phase-7-status.md` alarm-runtime status**,
|
||||
add a CLAUDE.md note for the script-log emit + scripted-alarm runtime. Delete/condense
|
||||
`pending.md` (its content now lives in the design + these docs). Then run
|
||||
superpowers-extended-cc:finishing-a-development-branch (full `dotnet test`, merge to
|
||||
master).
|
||||
**Files:** update `docs/ScriptedAlarms.md`, `docs/VirtualTags.md`, `docs/v2/Runtime.md`,
|
||||
`docs/AlarmTracking.md` (the inbound-ack + `AlarmAck`-gate flow is now real); **correct the stale
|
||||
`docs/v2/phase-7-status.md` alarm-runtime status**; CLAUDE.md note. **Clean up the deliberately-left
|
||||
rig artifacts** (`t12-overheat`, script `SC-ba675b168a85`, the `layer0-logcheck` vtag, and revert
|
||||
filler-02's inert `cycle-time-s` logger line — redeploy). Delete/condense `resume.md` + `pending.md`.
|
||||
Then run superpowers-extended-cc:finishing-a-development-branch (full `dotnet test`, merge to master).
|
||||
|
||||
---
|
||||
|
||||
## Execution notes
|
||||
|
||||
- **Parallel dispatch:** Layer 0 is serial (T1→T2→T3→T4). Layer 1: **T5→T6** serial
|
||||
(composer→artifact parity); **T7, T8 parallel** with T5/T6 (disjoint files); T9 waits
|
||||
on T6/T7/T8; T10→T11→T12 serial. Layer 2: T13 first; **T15 ∥ T17** after T14; T16 after
|
||||
T15; T18 after T17; T19/T20 last.
|
||||
- **One writer at a time** within a shared file (Program.cs touched by T2/T3/T11;
|
||||
OtOpcUaNodeManager by T14/T15/T16/T17 — serialize those).
|
||||
- **Layer boundaries are natural checkpoints** — Layer 0 is independently shippable; pause
|
||||
for review after T4 and after T12 before committing to the Layer 2 SDK epic.
|
||||
- **Parallel dispatch (Layers 0+1, done):** Layer 0 serial (T1→T2→T3→T4). Layer 1: **T5→T6** serial;
|
||||
**T7, T8 parallel** with T5/T6; T9 waits on T6/T7/T8; T10→T11→T12 serial.
|
||||
- **Parallel dispatch (Layer 2 remainder, T17–T24):**
|
||||
- **T17** first (roles) — its **step-1 round-trip spike is a go/no-go gate** for the gate design.
|
||||
- **T18** after T17 (the veto gate needs the roles). **T19 ∥ T20** after T18 (disjoint files:
|
||||
`ScriptedAlarmHostActor.cs` vs `OtOpcUaNodeManager.cs`).
|
||||
- **T22 runs in parallel with the whole T17–T21 server chain** (only `Client.*` files).
|
||||
- **T21** after T19. **T23** after T18/T19/T20/T21/T22. **T24** last.
|
||||
- **One writer at a time** within a shared file: `OtOpcUaNodeManager.cs` is touched by **T18 and
|
||||
T20 — serialize T18 → T20**. (Layers 0/1 already merged, so Program.cs/T14-T16 contention is moot.)
|
||||
- **Layer boundaries are natural checkpoints** — Layers 0+1 shipped; the T17 round-trip spike is the
|
||||
next gate before committing to the rest of the Layer 2 inbound epic.
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"baseBranch": "master",
|
||||
"baseSha": "df4c2657",
|
||||
"status": "partial-merged-T0-T16",
|
||||
"note": "Layers 0+1 complete + live-verified; Layer 2 PARTIAL — T13-T16 (Part 9 nodes/state/events) done + reviewed, merged to master. T17-T20 (inbound ack + security gate + AdminUI control + Client.CLI live-verify + docs) DEFERRED to a fresh piece (T17 reconned; see memory project-scriptlog-alarm-runtime).",
|
||||
"note": "Layers 0+1 complete + live-verified; Layer 2 PARTIAL — T13-T16 (Part 9 nodes/state/events) done + reviewed, merged to master. RE-SCOPED 2026-06-11: the single underscoped T17 ('Inbound method dispatch + ack plumbing', ~5min) was a deep dive shown to be FOUR hard problems — now T17 (roles on session identity), T18 (node-manager router + AlarmAck veto + alarm-commands DPS topic), T19 (host-actor inbound handler), T20 (delta-gate double-emit). Old T18->T21 (AdminUI), old T19 split into T22 (Client.CLI ack/confirm/shelve feature work) + T23 (live-verify), old T20->T24 (docs+cleanup+finish). All DEFERRED; start a fresh branch feat/scriptlog-alarm-ack off CURRENT master. T17 step-1 SDK-identity-round-trip spike is the go/no-go gate.",
|
||||
"rescopeBranch": "feat/scriptlog-alarm-ack",
|
||||
"tasks": [
|
||||
{"id": 200, "planTask": 0, "subject": "T0: Branch + test-project check", "status": "completed"},
|
||||
{"id": 201, "planTask": 1, "subject": "T1: IScriptLogPublisher + ScriptLogTopicSink", "status": "completed", "commit": "14fe88fc"},
|
||||
@@ -24,10 +25,14 @@
|
||||
{"id": 214, "planTask": 14, "subject": "T14: Real condition-node materialisation", "status": "completed", "commit": "60d48a2a, b31d7cb0"},
|
||||
{"id": 215, "planTask": 15, "subject": "T15: Richer alarm-state bridge", "status": "completed", "commit": "4eb1d65e, ab5d0752"},
|
||||
{"id": 216, "planTask": 16, "subject": "T16: Event firing on transition", "status": "completed", "commit": "295bb55d, 4c417f7f"},
|
||||
{"id": 217, "planTask": 17, "subject": "T17: Inbound method dispatch + ack plumbing", "status": "deferred", "note": "reconned, NOT built — needs cross-node DPS routing + LDAP-roles-on-session security gate + SDK veto delegates + delta-gated event firing"},
|
||||
{"id": 218, "planTask": 18, "subject": "T18: AdminUI ack/shelve control", "status": "deferred"},
|
||||
{"id": 219, "planTask": 19, "subject": "T19: Live-verify Layer 2 (Client.CLI)", "status": "deferred"},
|
||||
{"id": 220, "planTask": 20, "subject": "T20: Docs + finish branch", "status": "deferred"}
|
||||
{"id": 217, "planTask": 17, "subject": "T17: Carry LDAP roles onto the OPC UA session identity", "status": "deferred", "classification": "high-risk", "blockedBy": [], "note": "RoleCarryingUserIdentity : UserIdentity + OpcUaApplicationHost.cs:292 swap. STEP 1 = SDK-identity-round-trip spike (go/no-go: does a custom IUserIdentity survive back to context.UserIdentity in a method handler? fallback = GrantedRoleIds). Parallelizable with T22."},
|
||||
{"id": 218, "planTask": 18, "subject": "T18: Node-manager command router + AlarmAck veto gate + alarm-commands topic", "status": "deferred", "classification": "high-risk", "blockedBy": [217], "note": "AlarmCommand record (Commons); settable Action<AlarmCommand> router on OtOpcUaNodeManager; wire OnAcknowledge/OnConfirm/OnAddComment/OnShelve/OnTimedUnshelve veto delegates in MaterialiseAlarmCondition (gate on AlarmAck from RoleCarryingUserIdentity, route to engine, return Good); pass-through in OtOpcUaSdkServer; publish onto alarm-commands DPS topic from OtOpcUaServerHostedService. Serialize with T20 (same file)."},
|
||||
{"id": 219, "planTask": 19, "subject": "T19: ScriptedAlarmHostActor inbound command handler", "status": "deferred", "classification": "standard", "blockedBy": [218], "parallelizableWith": [220], "note": "Subscribe alarm-commands in PreStart; Receive<AlarmCommand> async-void switch -> engine.<Op>Async; ownership-filter unknown AlarmIds (multi-node broadcast); NO explicit re-projection (engine OnEvent -> existing OnEngineEmission)."},
|
||||
{"id": 220, "planTask": 20, "subject": "T20: Delta-gate event firing (kill inbound double-emit)", "status": "deferred", "classification": "high-risk", "blockedBy": [218], "parallelizableWith": [219], "note": "ConcurrentDictionary<string,AlarmConditionSnapshot> _lastAlarmState; WriteAlarmCondition fires ReportConditionEvent only on a delta. Resolves SDK-auto-fire (E2) + engine-re-projection (E3) double-emit. Serialize after T18 (same file OtOpcUaNodeManager.cs)."},
|
||||
{"id": 221, "planTask": 21, "subject": "T21: AdminUI ack/shelve control", "status": "deferred", "classification": "standard", "blockedBy": [219], "note": "Acknowledge/ShelveAlarmCommand (Commons); AdminOperationsActor singleton handlers publish onto alarm-commands (singleton solves cross-node for AdminUI); AdminOperationsClient methods; Alerts.razor per-row buttons. No bUnit."},
|
||||
{"id": 222, "planTask": 22, "subject": "T22: Client.CLI ack/confirm/shelve commands", "status": "deferred", "classification": "standard", "blockedBy": [], "parallelizableWith": [217, 218, 219, 220, 221], "note": "NET-NEW client feature (why old T19 wasn't just a verify): IOpcUaClientService Confirm/Shelve (+ wire existing AcknowledgeAlarmAsync); Acknowledge/Confirm/Shelve CLI commands. Only Client.* files -> parallel with the whole server chain."},
|
||||
{"id": 223, "planTask": 23, "subject": "T23: Live-verify Layer 2 end-to-end", "status": "deferred", "classification": "verification", "blockedBy": [218, 219, 220, 221, 222], "note": "Use deployed t12-overheat. Client.CLI + AdminUI ack round-trip (AckedState flips, ONE event, persists across restart); AlarmAck gate denies without role. User drives sign-in."},
|
||||
{"id": 224, "planTask": 24, "subject": "T24: Docs + cleanup + finish branch", "status": "deferred", "classification": "small", "blockedBy": [223], "note": "Docs (ScriptedAlarms/VirtualTags/Runtime/AlarmTracking) + fix stale phase-7-status + CLAUDE.md; clean up rig artifacts (t12-overheat, SC-ba675b168a85, layer0-logcheck, revert filler-02 cycle-time-s); delete resume.md+pending.md; finishing-a-development-branch merge."}
|
||||
],
|
||||
"lastUpdated": "2026-06-11"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user