diff --git a/docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime.md b/docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime.md new file mode 100644 index 00000000..02f6fc21 --- /dev/null +++ b/docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime.md @@ -0,0 +1,463 @@ +# Script-log Engine Emit + Scripted-Alarm Runtime — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: use superpowers-extended-cc:subagent-driven-development +> (or executing-plans) to implement this plan task-by-task. + +**Goal:** Make the Script-log page tail real script output, and stand up scripted +alarms end-to-end including real OPC UA Part 9 condition nodes + client ack. + +**Architecture:** Three sequenced layers off one shared seam (a root script logger +fanning to file + companion + a new DPS topic sink). Layer 0 = emit (F8 live). Layer 1 += F9 engine runtime on the Akka equipment-namespace runtime. Layer 2 = F14b real Part 9 +nodes + events + inbound ack. Design: `docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime-design.md`. +Verified gap analysis: `pending.md`. + +**Tech:** .NET 10, Akka.NET, EF Core (SQL prod / InMemory tests), Serilog, OPC +Foundation UA .NET Standard, xUnit + Shouldly, Akka TestKit. No bUnit. + +**Hard rules (every task):** stage by explicit path — never `git add .`; never stage +`sql_login.txt` or `src/Server/ZB.MOM.WW.OtOpcUa.Host/pki/`; never echo the gateway API +key into a new tracked file; no force-push, no `--no-verify`. **No Configuration entity +/ EF migration change** (`ScriptedAlarmState` table already exists). Agent does **not** +sign in to the AdminUI — the user drives live `/run`. + +**Branch:** `feat/scriptlog-alarm-runtime` off `master @ df4c2657` (design committed there). + +**Reference patterns to mirror:** `VirtualTagHostActor` (host actor shape), +`EfAlarmActorStateStore` (EF store shape), the `{{equip}}` two-seam parity work +(`Phase7Composer` ↔ `DeploymentArtifact`), `RoslynVirtualTagEvaluator` (evaluator). + +--- + +# LAYER 0 — Shared script-log emit + F8 live + +### Task 0: Branch + test-project check + +**Classification:** small · **~2 min** · **Parallelizable with:** none + +**Files:** none created (branch + verification only) + +**Steps:** +1. `git switch -c feat/scriptlog-alarm-runtime` (off `master @ df4c2657`). +2. Confirm `tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/` exists and is in the + `.slnx` (it does — `ScriptLoggerFactoryTests.cs` lives there). New Layer-0 tests land + here. Confirm `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/` for Layer-1 tests. +3. `dotnet build ZB.MOM.WW.OtOpcUa.slnx` — green baseline. Commit nothing. + +--- + +### Task 1: `IScriptLogPublisher` + `ScriptLogTopicSink` + +**Classification:** standard · **~4 min** · **Parallelizable with:** none + +**Files:** +- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/IScriptLogPublisher.cs` +- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogTopicSink.cs` +- Test: `tests/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests/ScriptLogTopicSinkTests.cs` +- Maybe modify: `Core.Scripting.csproj` (add ProjectReference to `Commons` for + `ScriptLogEntry` if not already referenced — verify first). + +**Step 1 — failing tests** (`ScriptLogTopicSinkTests`): +- A `LogEvent` (Information) with properties `ScriptId="S1"`, `VirtualTagId="V1"`, + `EquipmentId="EQ1"`, message `"hello"` → publisher receives one `ScriptLogEntry` with + those fields, `Level=="Information"`, `Message=="hello"`. +- `AlarmId` property maps to `ScriptLogEntry.AlarmId`; absent properties → null fields. +- A `Debug` event with default `minLevel=Information` → publisher receives **nothing**. +- Template message renders (`"v={V}"` + prop V=3 → `"v=3"`). +Use a fake `IScriptLogPublisher` capturing entries. + +**Step 2 — run, expect fail** (types don't exist). + +**Step 3 — implement:** +```csharp +public interface IScriptLogPublisher { void Publish(ScriptLogEntry entry); } + +public sealed class ScriptLogTopicSink : ILogEventSink +{ + private readonly IScriptLogPublisher _publisher; + private readonly LogEventLevel _min; + public ScriptLogTopicSink(IScriptLogPublisher publisher, + LogEventLevel min = LogEventLevel.Information) { _publisher = publisher; _min = min; } + public void Emit(LogEvent e) + { + if (e is null || e.Level < _min) return; + string? P(string k) => e.Properties.TryGetValue(k, out var v) + && v is ScalarValue { Value: string s } ? s : null; + _publisher.Publish(new ScriptLogEntry( + ScriptId: P("ScriptId") ?? P("ScriptName") ?? "unknown", + Level: e.Level.ToString(), + Message: e.RenderMessage(), + TimestampUtc: e.Timestamp.UtcDateTime, + VirtualTagId: P("VirtualTagId"), AlarmId: P("AlarmId"), EquipmentId: P("EquipmentId"))); + } +} +``` +(Property-name constants — reuse/extend `ScriptLoggerFactory`'s `ScriptNameProperty`; +add `ScriptIdProperty`/`VirtualTagIdProperty`/`AlarmIdProperty`/`EquipmentIdProperty`.) + +**Step 4 — run tests, expect pass. Step 5 — commit** (`git add` the 3 files by path). + +--- + +### Task 2: Root script logger + `DpsScriptLogPublisher` + Host wiring + +**Classification:** standard · **~5 min** · **Parallelizable with:** none (depends T1) + +**Files:** +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Scripting/DpsScriptLogPublisher.cs` + (or Host — wherever the `ActorSystem`/`Mediator` is reachable at construction). +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Logging/ScriptRootLoggerFactory.cs` + (builds the root `ILogger`: rolling `scripts-*.log` + `ScriptLogCompanionSink` + + `ScriptLogTopicSink`). +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs` (build + register root logger; + register `IScriptLogPublisher`). +- Test: `tests/.../Core.Scripting.Tests/ScriptRootLoggerFanoutTests.cs` (or Host.Tests). + +**Steps (TDD):** +1. Failing test: a logger built by `ScriptRootLoggerFactory` with a fake publisher + + in-memory companion → an `Error` event reaches the companion mirror AND the topic + publisher; a `Debug` event reaches neither topic nor companion (file only). (Assert + via fakes; don't assert the physical file.) +2. Implement `DpsScriptLogPublisher` — ctor takes the DPS mediator `IActorRef` (or + `ActorSystem`); `Publish` → `mediator.Tell(new Publish("script-logs", entry))` + (topic constant `VirtualTagActor.ScriptLogsTopic`). +3. Implement `ScriptRootLoggerFactory.Build(IScriptLogPublisher, config)` → + `LoggerConfiguration().WriteTo.File(...).WriteTo.Sink(new ScriptLogCompanionSink(Log.Logger)) + .WriteTo.Sink(new ScriptLogTopicSink(publisher, minLevel)).CreateLogger()`. +4. `Program.cs`: resolve the mediator after the ActorSystem is up; register + `IScriptLogPublisher` (singleton) + the root `ILogger` (keyed/named for scripts). + Min-level from config (`Scripting:LogTopicMinLevel`, default `Information`). +5. Run + commit by path. + +--- + +### Task 3: Rewire evaluators to the root script logger + +**Classification:** standard · **~5 min** · **Parallelizable with:** none (depends T1, T2) + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs` +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynScriptedAlarmEvaluator.cs` +- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLoggerFactory.cs` (bind the + full property set, not just `ScriptName`). +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs` (inject root logger into the + evaluators). +- Test: `tests/.../Core.Scripting.Tests/` — evaluator emits via fake publisher. + +**Steps:** +1. Failing test: a `RoslynVirtualTagEvaluator` built with a root logger wired to a fake + publisher; evaluate a script `ctx.Logger.Information("hi"); return 1;` → publisher + gets one entry with `ScriptId`/`VirtualTagId` bound and `Message=="hi"`. +2. Replace the static `ScriptLogger` field with a ctor-injected root `ILogger`. Per + evaluation, `var log = _root.ForContext("ScriptId", id).ForContext("VirtualTagId", virtualTagId)` + (+ `EquipmentId` when available) and pass into the `VirtualTagContext`. Same for the + alarm evaluator (binds `AlarmId`). +3. `ScriptLoggerFactory`: add a `Create(scriptId, virtualTagId?, alarmId?, equipmentId?)` + overload binding the standard properties (keep the old `Create(scriptName)` for + compatibility). +4. `Program.cs`: pass the root logger to both evaluator registrations. +5. Run + commit by path. + +> Note: `IVirtualTagEvaluator.Evaluate` carries `virtualTagId`; in the live path +> `scriptId == virtualTagId`, so Layer 0 binds both from it. Threading a distinct +> `EquipmentId` (nice-to-have on the page) is optional here — if it requires an +> interface change, defer it to a Layer-1 follow-up rather than expanding T3. + +--- + +### Task 4: Live-verify Layer 0 + +**Classification:** verification · **Parallelizable with:** none (depends T2, T3) + +**Steps:** +1. Rebuild docker-dev central nodes (user-driven `/run`). Author a virtual tag whose + script calls `ctx.Logger.Information(...)`. +2. Open `/script-log`; drive the dependency so the script evaluates; confirm the line + appears live with the right ScriptId/level. Confirm Debug stays off the page, + Information+ shows. +3. **Agent does not sign in** — user signs in and drives. Record outcome. No code unless + a defect surfaces (→ new fix task). + +--- + +# LAYER 1 — F9 engine runtime + +### Task 5: `EquipmentScriptedAlarmPlan` + Phase7Composer enrichment + +**Classification:** standard · **~5 min** · **Parallelizable with:** Task 7, Task 8 + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs` (new record + + build the enriched list from `ScriptedAlarm` + `Script` rows). +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/Phase7/` (or wherever Phase7Composer + is tested) — new `Phase7ComposerScriptedAlarmTests.cs`. + +**Steps:** +1. Failing test: compose two equipments each with a scripted alarm referencing a script; + assert each `EquipmentScriptedAlarmPlan` carries the resolved `PredicateSource`, + extracted `DependencyRefs` (via `DependencyExtractor`), `AlarmType`, `Severity`, + `MessageTemplate`, `HistorizeToAveva`, `Retain`, `Enabled`, `Name`. +2. Add `public sealed record EquipmentScriptedAlarmPlan(string ScriptedAlarmId, string + EquipmentId, string Name, string AlarmType, int Severity, string MessageTemplate, + string PredicateScriptId, string PredicateSource, IReadOnlyList DependencyRefs, + bool HistorizeToAveva, bool Retain, bool Enabled);` +3. In `Compose`: join `ScriptedAlarm.PredicateScriptId → Script.SourceCode`; run + `DependencyExtractor.Extract(source).Reads` (∪ `MessageTemplate` token paths) for + `DependencyRefs`; project into the new list on the composition result. Skip + `Enabled=false` alarms (or carry the flag — carry it; host decides). Drop alarms whose + script is missing with a structured warning (don't throw the whole compose). +4. Run + commit by path. + +--- + +### Task 6: DeploymentArtifact parity for the alarm plan + +**Classification:** standard · **~5 min** · **Parallelizable with:** Task 7, Task 8 (depends T5) + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs` + (encode/decode `EquipmentScriptedAlarmPlan`; add + `Phase7CompositionResult.EquipmentScriptedAlarms`; filter-by-equipment like + `EquipmentVirtualTags` at :263). +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/` — artifact round-trip + parity + with the Composer for the same input. + +**Steps:** +1. Failing test: build a composition via `Phase7Composer`, serialize to artifact, parse + back → `EquipmentScriptedAlarms` is byte-identical (same discipline as the `{{equip}}` + parity tests). Equipment-filter test (only alarms for resident equipment survive). +2. Add the field to `Phase7CompositionResult`; mirror the `EquipmentVirtualTags` + encode/decode/filter exactly (`:202`, `:263`). +3. Run + commit by path. + +--- + +### Task 7: `DependencyMuxTagUpstreamSource` + +**Classification:** standard · **~4 min** · **Parallelizable with:** Task 5, Task 6, Task 8 + +**Files:** +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/DependencyMuxTagUpstreamSource.cs` + (implements `Core.ScriptedAlarms`/`Core.VirtualTags` `ITagUpstreamSource`). +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/DependencyMuxTagUpstreamSourceTests.cs` + +**Steps:** +1. Failing tests: `Push(path, snapshot)` updates cache so `ReadTag(path)` returns it; + `SubscribeTag(path, obs)` → `obs` fires on the next `Push`; `ReadTag` for an unknown + path returns a Bad-quality snapshot; dispose removes the observer. +2. Implement: a thread-safe cache (`ConcurrentDictionary`) + + per-path observer list; `Push` updates cache then invokes observers; `ReadTag` reads + cache (Bad if absent); `SubscribeTag` returns an `IDisposable` that deregisters. The + host actor calls `Push` from its `DependencyValueChanged` handler. Value wrap: + `new DataValueSnapshot(value, StatusCode:0, ts, ts)`. +3. Run + commit by path. + +--- + +### Task 8: `EfAlarmConditionStateStore : IAlarmStateStore` + +**Classification:** standard · **~5 min** · **Parallelizable with:** Task 5, Task 6, Task 7 + +**Files:** +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/EfAlarmConditionStateStore.cs` +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/EfAlarmConditionStateStoreTests.cs` + (in-memory EF). + +**Steps:** +1. Failing tests (in-memory `OtOpcUaConfigDbContext`): `SaveAsync(state)` then + `LoadAsync(alarmId)` round-trips Enabled/Acked/Confirmed/Shelving(+UnshelveAtUtc)/ + LastAck*/LastConfirm*/Comments; `LoadAsync` of an unknown id → null; `ActiveState` + is **not** persisted (a saved state's Active is ignored on load — load returns the + stored operator state, Active defaults). Comments JSON round-trips. +2. Implement mapping `AlarmConditionState` ↔ `ScriptedAlarmState` entity (mirror + `EfAlarmActorStateStore`'s `IDbContextFactory` upsert pattern; serialize + `ImmutableList` ↔ `CommentsJson`). Map enum states ↔ the entity's string + columns. +3. Run + commit by path. + +--- + +### Task 9: `ScriptedAlarmHostActor` + +**Classification:** high-risk · **~5 min** · **Parallelizable with:** none (depends T6, T7, T8; needs Layer 0 T2/T3 root logger) + +**Files:** +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs` +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmHostActorTests.cs` + (Akka TestKit + a real engine with the fake upstream, or a fake engine seam). + +**Design:** mirrors `VirtualTagHostActor`. Owns one `ScriptedAlarmEngine` (built with the +`DependencyMuxTagUpstreamSource`, the `EfAlarmConditionStateStore`, a `ScriptLoggerFactory` +wrapping the **Layer 0 root logger**, and the engine's root logger). Message +`ApplyScriptedAlarms(IReadOnlyList Plans)`. + +**Steps:** +1. Failing TestKit tests: + - `ApplyScriptedAlarms` with one alarm → engine loaded (assert via a probe/seam); + registers interest with the (probe) mux for the alarm's dep refs. + - A `DependencyValueChanged` that makes the predicate true → the host tells the + (probe) `OpcUaPublishActor` a `WriteAlarmState(alarmId, active:true, …)`, tells the + (probe) historian an `AlarmHistorianEvent` (when `HistorizeToAveva`), and publishes + an `AlarmTransitionEvent` on `alerts`. + - Re-`ApplyScriptedAlarms` with a different set reloads the engine (LoadAsync replace). +2. Implement: on `ApplyScriptedAlarms`, build `ScriptedAlarmDefinition`s from the plans + (map `AlarmType`→`AlarmKind`, `Severity`→`AlarmSeverity`, `EquipmentId`→`EquipmentPath`), + `engine.LoadAsync`; register mux interest for `⋃ DependencyRefs`; on + `DependencyValueChanged` → `_upstream.Push(...)`. Subscribe `engine.OnEvent` once → + map `ScriptedAlarmEvent.Condition` to `(active, acknowledged)` → + `OpcUaPublishActor.WriteAlarmState`; map → `AlarmHistorianEvent` → historian (if + Historize); publish `AlarmTransitionEvent` on `alerts`. Dispose engine in `PostStop`. +3. Run targeted tests (`dotnet test --filter ScriptedAlarmHostActor`). Commit by path. + +--- + +### Task 10: Spawn + apply in DriverHostActor + +**Classification:** standard · **~4 min** · **Parallelizable with:** none (depends T9) + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs` + (spawn `ScriptedAlarmHostActor` next to `VirtualTagHostActor` ~:197; tell + `ApplyScriptedAlarms(composition.EquipmentScriptedAlarms)` next to the vtag apply ~:532; + add an override field for tests like `_virtualTagHostOverride`). +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs` if the + host needs new injected deps (EF store factory, root logger, historian ref). +- Test: extend `DriverHostActorTests` — apply pushes `ApplyScriptedAlarms`. + +**Steps:** mirror the VirtualTag spawn/apply exactly; thread `_opcUaPublishActor`, +`_dependencyMux`, the EF store, the root logger, the historian actor ref. Run + commit. + +--- + +### Task 11: Retire the orphaned actor + F9b evaluator + +**Classification:** small · **~3 min** · **Parallelizable with:** none (depends T9, T10) + +**Files:** +- Delete: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmActor.cs` + (+ its tests) and `src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynScriptedAlarmEvaluator.cs`. +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs` (remove F9b DI registration, + lines ~110-114) and any `IScriptedAlarmEvaluator` references. Keep + `EfAlarmActorStateStore` only if nothing else uses it — otherwise delete with the actor. + +**Steps:** delete, fix build, run full `dotnet test` for the touched projects, commit by +path. (If something unexpectedly depends on these, stop and surface — don't expand scope.) + +--- + +### Task 12: Live-verify Layer 1 + +**Classification:** verification · **Parallelizable with:** none (depends T10, T11) + +**Steps:** rebuild docker-dev; author a scripted alarm whose predicate references a live +tag; drive the tag; confirm the alarm node flips active/clear, the historian queue +advances (`/alarms/historian`), the `alerts`/Alerts page shows it, and predicate +`ctx.Logger` output appears on `/script-log`. User drives sign-in. Defects → new tasks. + +--- + +# LAYER 2 — F14b real Part 9 + client ack + +### Task 13: SDK research spike (DeepWiki) + +**Classification:** small (research) · **~5 min** · **Parallelizable with:** Layer-1 tasks + +**Steps:** Use the DeepWiki MCP (`OPCFoundation/UA-.NETStandard`) to confirm: how to +create + add an `AlarmConditionState` (and Limit/OffNormal/Discrete subtypes) under a +parent in a `CustomNodeManager2`; how to set ActiveState/AckedState/ConfirmedState/ +ShelvingState/Severity/Retain; how transitions fire events (`ReportEvent`); how inbound +`Acknowledge`/`Shelve`/`Confirm` method calls are dispatched + where to hook them. Write +findings to `docs/v2/f14b-part9-sdk-notes.md` (committed). This de-risks T14-T17. + +--- + +### Task 14: Real condition-node materialisation + +**Classification:** high-risk · **~5 min** · **Parallelizable with:** none (depends T13) + +**Files:** `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs` (replace the +placeholder `[active, ack]` variable in `WriteAlarmState` / add a `MaterialiseAlarmCondition` +path per `AlarmType`); `Phase7Applier.cs` (call the new materialiser); tests where the +SDK allows (node existence/type assertions). + +**Steps:** create real condition nodes on materialise; keep `WriteAlarmState` as a thin +shim during transition or replace its callers. Run + commit. (SDK threading: all via the +pinned `OpcUaPublishActor` dispatcher.) + +--- + +### Task 15: Richer alarm-state bridge + +**Classification:** standard · **~4 min** · **Parallelizable with:** Task 17 (depends T14, T9) + +**Files:** `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs` (new message +carrying the full `AlarmConditionState`, not 2 bools); `ScriptedAlarmHostActor` bridge +(send the richer message); `OtOpcUaNodeManager` (apply full state to the condition). +Tests: message mapping. + +--- + +### Task 16: Event firing on transition + +**Classification:** high-risk · **~5 min** · **Parallelizable with:** none (depends T14, T15) + +**Files:** `OtOpcUaNodeManager.cs` (`condition.ReportEvent(...)` on state change). Tests: +mapping/coverage where feasible; behaviour proven in T19. + +--- + +### Task 17: Inbound method dispatch + ack plumbing + +**Classification:** high-risk · **~5 min** · **Parallelizable with:** Task 15 (depends T14) + +**Files:** `OtOpcUaNodeManager.cs` (wire Acknowledge/Confirm/AddComment/OneShotShelve/ +TimedShelve/Unshelve handlers → route to a control-plane message → `ScriptedAlarmHostActor` +→ `engine.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. + +--- + +### Task 18: AdminUI ack/shelve control + +**Classification:** standard · **~4 min** · **Parallelizable with:** none (depends T17) + +**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). + +--- + +### Task 19: Live-verify Layer 2 (Client.CLI) + +**Classification:** verification · **Parallelizable with:** none (depends T16, T17, T18) + +**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. + +--- + +### Task 20: Docs + finish + +**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). + +--- + +## 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. diff --git a/docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime.md.tasks.json b/docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime.md.tasks.json new file mode 100644 index 00000000..fa8a0885 --- /dev/null +++ b/docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime.md.tasks.json @@ -0,0 +1,32 @@ +{ + "planPath": "docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime.md", + "designPath": "docs/plans/2026-06-10-script-log-and-scripted-alarm-runtime-design.md", + "branch": "feat/scriptlog-alarm-runtime", + "baseBranch": "master", + "baseSha": "df4c2657", + "status": "pending", + "tasks": [ + {"id": 200, "planTask": 0, "subject": "T0: Branch + test-project check", "classification": "small", "status": "pending"}, + {"id": 201, "planTask": 1, "subject": "T1: IScriptLogPublisher + ScriptLogTopicSink", "classification": "standard", "status": "pending", "blockedBy": [200]}, + {"id": 202, "planTask": 2, "subject": "T2: Root script logger + DpsScriptLogPublisher + Host wiring", "classification": "standard", "status": "pending", "blockedBy": [201]}, + {"id": 203, "planTask": 3, "subject": "T3: Rewire Roslyn evaluators to root logger", "classification": "standard", "status": "pending", "blockedBy": [202]}, + {"id": 204, "planTask": 4, "subject": "T4: Live-verify Layer 0", "classification": "verification", "status": "pending", "blockedBy": [202, 203]}, + {"id": 205, "planTask": 5, "subject": "T5: EquipmentScriptedAlarmPlan + Phase7Composer enrichment", "classification": "standard", "status": "pending", "blockedBy": [200], "parallelizableWith": [207, 208]}, + {"id": 206, "planTask": 6, "subject": "T6: DeploymentArtifact parity for alarm plan", "classification": "standard", "status": "pending", "blockedBy": [205], "parallelizableWith": [207, 208]}, + {"id": 207, "planTask": 7, "subject": "T7: DependencyMuxTagUpstreamSource", "classification": "standard", "status": "pending", "blockedBy": [200], "parallelizableWith": [205, 206, 208]}, + {"id": 208, "planTask": 8, "subject": "T8: EfAlarmConditionStateStore", "classification": "standard", "status": "pending", "blockedBy": [200], "parallelizableWith": [205, 206, 207]}, + {"id": 209, "planTask": 9, "subject": "T9: ScriptedAlarmHostActor", "classification": "high-risk", "status": "pending", "blockedBy": [206, 207, 208, 203]}, + {"id": 210, "planTask": 10, "subject": "T10: Spawn + apply in DriverHostActor", "classification": "standard", "status": "pending", "blockedBy": [209]}, + {"id": 211, "planTask": 11, "subject": "T11: Retire orphaned actor + F9b evaluator", "classification": "small", "status": "pending", "blockedBy": [209, 210]}, + {"id": 212, "planTask": 12, "subject": "T12: Live-verify Layer 1", "classification": "verification", "status": "pending", "blockedBy": [210, 211]}, + {"id": 213, "planTask": 13, "subject": "T13: SDK research spike (DeepWiki)", "classification": "small", "status": "pending", "blockedBy": [200]}, + {"id": 214, "planTask": 14, "subject": "T14: Real condition-node materialisation", "classification": "high-risk", "status": "pending", "blockedBy": [213]}, + {"id": 215, "planTask": 15, "subject": "T15: Richer alarm-state bridge", "classification": "standard", "status": "pending", "blockedBy": [214, 209], "parallelizableWith": [217]}, + {"id": 216, "planTask": 16, "subject": "T16: Event firing on transition", "classification": "high-risk", "status": "pending", "blockedBy": [214, 215]}, + {"id": 217, "planTask": 17, "subject": "T17: Inbound method dispatch + ack plumbing", "classification": "high-risk", "status": "pending", "blockedBy": [214], "parallelizableWith": [215]}, + {"id": 218, "planTask": 18, "subject": "T18: AdminUI ack/shelve control", "classification": "standard", "status": "pending", "blockedBy": [217]}, + {"id": 219, "planTask": 19, "subject": "T19: Live-verify Layer 2 (Client.CLI)", "classification": "verification", "status": "pending", "blockedBy": [216, 217, 218]}, + {"id": 220, "planTask": 20, "subject": "T20: Docs + finish branch", "classification": "small", "status": "pending", "blockedBy": [219]} + ], + "lastUpdated": "2026-06-10" +}