Audits every Phase 7 plan stream (A-H) against the repo, confirms the exit gate is fully closed, and records the five genuine remaining gaps: OPC UA method-call dispatch for alarm Ack/Confirm/Shelve, the /virtual-tags and /scripted-alarms Admin UI tabs, the script log viewer, and the missing production IHistoryWriter for virtual-tag historization. Also notes that docs/v2/v2-release-readiness.md carries a stale "out of scope" label — Phase 7 shipped completely after that doc was last updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
Phase 7 Status — Scripting Runtime, Virtual Tags, Scripted Alarms, Historian Sink
Reconciliation date: 2026-05-18 Based on:
docs/v2/implementation/phase-7-scripting-and-alarming.md(the plan) anddocs/v2/implementation/exit-gate-phase-7.md(the exit-gate audit) cross-checked against the actual repository files. See "Evidence sources" at the bottom.
Summary verdict
Phase 7 core is fully shipped and the exit gate is closed. All eight plan streams
(A–H, where H = exit gate) plus the three deferred follow-ups (tasks #239 / #240 / #241)
landed before the 2026-04-23 exit-gate audit. The v2-release-readiness.md note
"Phase 7 — out of scope for v2 GA" is a stale label: the work shipped after that doc
was last updated. The four Core.* Phase 7 projects exist, have tests, and are wired
into the running server. Five targeted gaps remain open (see section below).
Work-item status by plan stream
Stream A — Core.Scripting (Roslyn engine, sandbox, AST inference, logger)
| Plan item | Status | Evidence |
|---|---|---|
A.1 — Project scaffold + ScriptContext base class (GetTag / SetVirtualTag / Logger / Now / Deadband) |
Done | src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs, ScriptGlobals.cs |
A.2 — DependencyExtractor : CSharpSyntaxWalker — literal-only path check, Inputs + Outputs sets |
Done | DependencyExtractor.cs; literal-reject logic exercised by 7 test files in Core.Scripting.Tests |
A.3 — Compile cache keyed on SHA-256(source) |
Done | CompiledScriptCache.cs (ConcurrentDictionary<string, Lazy<ScriptEvaluator<...>>>) |
| A.4 — Per-evaluation timeout (250 ms default) | Done | TimedScriptEvaluator.cs; TimedScriptEvaluatorTests.cs |
A.5 — Serilog sink wiring; scripts-*.log companion mirror to main log at WARN on ERROR |
Done | ScriptLoggerFactory.cs, ScriptLogCompanionSink.cs; ScriptLogCompanionSinkTests.cs |
| A.6 — Tests (AST extraction, sandbox escape, exception isolation, timeout, logger binding) | Done | ScriptSandboxTests.cs, DependencyExtractorTests.cs, CompiledScriptCacheTests.cs, ScriptLoggerFactoryTests.cs, TimedScriptEvaluatorTests.cs — 7 test files |
Shipped as PRs #177–#179 (63 tests).
Stream B — Virtual tag engine
| Plan item | Status | Evidence |
|---|---|---|
B.1 — VirtualTagEngine + DependencyGraph |
Done | VirtualTagEngine.cs, DependencyGraph.cs |
B.2 — ChangeTriggerDispatcher (subscribe to referenced driver tags via ITagUpstreamSource) |
Done | VirtualTagEngine.OnUpstreamChange internal subscriber path |
B.3 — TimerTriggerDispatcher (per-tag IntervalMs via timer-wheel) |
Done | TimerTriggerScheduler.cs |
B.4 — EvaluationPipeline (serial, per-tag isolation, _evalGate semaphore) |
Done | VirtualTagEngine.EvaluateInternalAsync; _evalGate SemaphoreSlim(1,1) |
B.5 — IVirtualTagSource implementing IReadable + ISubscribable |
Done | VirtualTagSource.cs |
B.6 — History routing (IHistoryWriter.Record when Historize=true) |
Partial | IHistoryWriter.cs + NullHistoryWriter present; no production writer is wired into the virtual-tag path. docs/VirtualTags.md §"Upstream reads + history" explicitly notes: "no production writer is currently wired for virtual tags". Virtual-tag historization is functional at the engine level but has no live sink. |
| B.7 — Tests: dependency graph, cascade, timer, change+timer combined, error propagation, historize | Done | DependencyGraphTests.cs, VirtualTagEngineTests.cs, TimerTriggerSchedulerTests.cs, VirtualTagSourceTests.cs — 5 test files |
Shipped as PR #180 (36 tests).
Stream C — Scripted alarm engine + Part 9 state machine + template messages
| Plan item | Status | Evidence |
|---|---|---|
C.1 — ScriptedAlarmEngine skeleton + alarm config model |
Done | ScriptedAlarmEngine.cs, ScriptedAlarmDefinition.cs |
C.2 — Part9StateMachine (Enable/Disable/Active/Ack/Confirm/Shelve/Unshelve/Comment/ShelvingCheck) |
Done | Part9StateMachine.cs; Part9StateMachineTests.cs |
| C.3 — Predicate evaluation on input change; activate/clear transitions | Done | ScriptedAlarmEngine.ReevaluateAsync; _alarmsReferencing inverse index |
C.4 — Startup recovery (ActiveState re-derived; Enabled/Ack/Confirm/Shelve loaded from store) |
Done | ScriptedAlarmEngine.LoadAsync; IAlarmStateStore.LoadAsync |
C.5 — Template substitution ({TagPath} tokens resolved at emission time) |
Done | MessageTemplate.cs; MessageTemplateTests.cs |
| C.6 — OPC UA method binding (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve) | Partial | Engine methods exist and are tested. ScriptedAlarmSource.AcknowledgeAsync defaults the user to "opcua-client". The plan's Stream G wiring of these methods to OPC UA MethodCall dispatch on the condition nodes (so OPC UA client method calls reach the engine with the authenticated principal) is noted in the e2e smoke doc as "not yet wired through DriverNodeManager.MethodCall dispatch." Operators acknowledge through Admin UI today; the Part 9 method-call path is a follow-up. |
C.7 — IAlarmSource implementation / fan-out registration |
Done | ScriptedAlarmSource.cs |
| C.8 — Tests: all state transitions, startup recovery, template substitution, shelving timer expiry | Done | Part9StateMachineTests.cs, ScriptedAlarmEngineTests.cs, ScriptedAlarmSourceTests.cs, MessageTemplateTests.cs — 5 test files |
Shipped as PR #181 (47 tests).
Stream D — Historian alarm sink (SQLite store-and-forward + Wonderware IPC)
| Plan item | Status | Evidence |
|---|---|---|
D.1 — Core.AlarmHistorian project; IAlarmHistorianSink; SqliteStoreAndForwardSink (backoff, dead-letter, capacity) |
Done | IAlarmHistorianSink.cs, SqliteStoreAndForwardSink.cs; SqliteStoreAndForwardSinkTests.cs |
| D.2 — Live-historian smoke against dev box Aveva Historian; document the exact SDK entry point | Partial | The smoke (docs/v2/implementation/phase-7-e2e-smoke.md) ran but the IPC path via Galaxy.Host to aahClientManaged was the original plan. That path changed: the production implementation uses Driver.Historian.Wonderware.Client (WonderwareHistorianClient.WriteBatchAsync) over a named-pipe sidecar, not Galaxy.Host. There is no separate docs/v2/historian-alarm-api.md artifact documenting the SDK entry point as the plan called for; the implementation detail is in WonderwareHistorianClient.cs inline. |
D.3 — Driver.Galaxy.Shared contract additions (HistorianAlarmEventRequest / Response / ConnectivityStatusNotification) |
Changed | The plan routed alarm writes through Galaxy.Host IPC. The shipped implementation uses Driver.Historian.Wonderware.Client (a standalone sidecar project) instead. HistorianAlarmEventRequest / HistorianAlarmEventResponse as named protos never shipped; the equivalent contract is the AlarmHistorianEventDto / WriteAlarmEventsRequest / WriteAlarmEventsReply MessagePack DTOs in Driver.Historian.Wonderware.Client/Ipc/. Galaxy.Host still exists as the mxaccessgw entry point but does not carry historian writes. |
D.4 — Driver.Galaxy.Host handler for alarm writes |
Changed | Not shipped via Galaxy.Host. The sidecar (Driver.Historian.Wonderware.Client) is the production path. IAlarmHistorianWriter is implemented by WonderwareHistorianClient, not by a Galaxy.Host frame handler. |
| D.5 — Drain worker in main server (poll SQLite queue, batch 100 events, exponential backoff) | Done | SqliteStoreAndForwardSink.StartDrainLoop; backoff ladder 1s → 2s → 5s → 15s → 60s; Phase7Composer.ResolveHistorianSink starts it with a 2-second drain cadence |
D.6 — Per-alarm HistorizeToAveva toggle; AlarmHistorizationPolicy per source |
Done | ScriptedAlarm.HistorizeToAveva column (default true); Phase7EngineComposer.RouteToHistorianAsync checks it; Galaxy defaults false |
D.7 — /alarms/historian diagnostics view in Admin (queue depth, drain rate, last error, retry dead-lettered) |
Done | AlarmsHistorian.razor; HistorianDiagnosticsService.cs |
| D.8 — Tests | Done | SqliteStoreAndForwardSinkTests.cs; Phase7ComposerWriterSelectionTests.cs covers historian-writer resolution |
Shipped as PR #182 (14 tests). Architecture deviated from the plan (Wonderware sidecar instead of Galaxy.Host IPC) but the functional goals are met.
Stream E — Config DB schema + generation-sealed cache extensions
| Plan item | Status | Evidence |
|---|---|---|
E.1 — EF migration for Script / VirtualTag / ScriptedAlarm / ScriptedAlarmState tables |
Done | Migration 20260420231641_AddPhase7ScriptingTables.cs; entities in Configuration/Entities/ |
E.2 — sp_PublishGeneration extension (sealed-cache snapshot includes Phase 7 rows) |
Done | Migration 20260420232000_ExtendComputeGenerationDiffWithPhase7.cs |
E.3 — CRUD services: VirtualTagService, ScriptedAlarmService, ScriptService, ScriptedAlarmStateService |
Done | All four exist in Admin/Services/; GetStateAsync on ScriptedAlarmService serves the state query |
| E.4 — Tests: migration up/down; publish atomicity; audit trail | Done | Phase7ServicesTests.cs (13 tests covering CRUD + hash behavior + harness) |
Shipped as PR #183 (12 tests in configuration; 13 more in Admin.Tests).
Stream F — Admin UI scripting tab
| Plan item | Status | Evidence |
|---|---|---|
| F.1 — Monaco editor Razor component (CDN bundle + textarea fallback) | Done | ScriptEditor.razor (textarea with Monaco JS interop, otOpcUaScriptEditor.attach) |
F.2 — /virtual-tags tab (list view, edit pane, dependency preview, publish gate) |
Partial | The ScriptsTab.razor is the single tab covering script CRUD, dependency preview, and harness. There is no separate /virtual-tags tab UI — virtual tags are managed through the script service alone; no VirtualTag list/edit form exists in the Admin UI. The per-tag fields (EquipmentId, DataType, ChangeTriggered, TimerIntervalMs, Historize) are accessible via the VirtualTagService backend but have no corresponding UI form. |
F.3 — /scripted-alarms tab (alarm type, severity, message template, HistorizeToAveva, detail page with shelve/ack state read-only) |
Partial | No dedicated scripted-alarms tab razor page exists (confirmed by Glob + Grep searches). Scripted alarm CRUD (ScriptedAlarmService) exists as a service but has no Admin UI page. |
| F.4 — Test harness (modal, synthetic inputs, output + logger display) | Partial | ScriptTestHarnessService.cs is complete and tested. ScriptsTab.razor calls Harness.RunVirtualTagAsync with zero-value synthetic inputs derived from the extractor. A full interactive input-form modal was not shipped — the harness zeroes all inputs automatically rather than prompting the operator per-tag. |
F.5 — Script log viewer (SignalR tail of scripts-*.log filtered by ScriptName, load-more) |
Not started | No SignalR stream of the scripts log is wired in the Admin UI. The AlertHub / FleetStatusHub exist but there is no ScriptLogHub. |
F.6 — /alarms/historian diagnostics view |
Done | AlarmsHistorian.razor + HistorianDiagnosticsService.cs |
F.7 — Playwright smoke (author calc tag, verify in equipment tree; author alarm, verify in AlarmsAndConditions) |
Not started | tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/ exists but its UnsTabDragDropE2ETests.cs is the only Playwright test; no Phase 7 Admin UI playwright scenario. |
Shipped as PR #185 (13 Admin service tests; UI completeness is partial — see gaps section).
Stream G — Address-space integration
| Plan item | Status | Evidence |
|---|---|---|
G.1 — EquipmentNodeWalker extension emits NodeSourceKind.Virtual + NodeSourceKind.ScriptedAlarm variables |
Done | PR #184; NodeSourceKind discriminator confirmed in exit gate |
G.2 — DriverNodeManager dispatch routes reads by source; writes to non-Driver rejected with BadUserAccessDenied |
Done | PR #186 follow-up; OpcUaApplicationHost.SetPhase7Sources threads _virtualReadable + _scriptedAlarmReadable into the node manager |
G.3 — AlarmTracker composition (ScriptedAlarmEngine registers as additional IAlarmSource) |
Done | ScriptedAlarmSource adapts engine to IAlarmSource; Phase7EngineComposer.Compose wires it |
| G.4 — Tests: mixed equipment folder browsable via Client.CLI; read/subscribe round-trip; alarm transitions in event stream | Done | Phase7ComposerMappingTests.cs, Phase7EngineComposerTests.cs, ScriptedAlarmReadableTests.cs, CachedTagUpstreamSourceTests.cs, DriverSubscriptionBridgeTests.cs — 6 test files in Server.Tests/Phase7/ |
| OPC UA method binding for alarm Ack/Confirm/Shelve | Not started | Noted explicitly in phase-7-e2e-smoke.md §"Known limitations": DriverNodeManager.MethodCall dispatch for scripted alarm methods is not wired. Engine has the methods; the OPC UA call path does not reach them. |
Shipped across PRs #184 + #186 (5 + 7 tests).
Stream H — Exit gate
| Plan item | Status | Evidence |
|---|---|---|
| H.1 — Compliance script real-checks | Done | scripts/compliance/phase-7-compliance.ps1 |
H.2 — Full solution dotnet test baseline |
Done | Exit gate records ~197 new tests + solution baseline |
H.3 — plan.md Migration Strategy §6 update |
Not verified | Not explicitly confirmed; minor — the plan doc is not the primary status artifact |
| H.4 — Phase-status memory update | Done | Memory updated (see project_alarms_over_gateway_epic.md + project_server_history_alarm_subsystems.md) |
H.5 — Merge v2/phase-7-scripting-and-alarming → v2 |
Done | All PRs (#177–#186) merged |
Post-gate follow-ups (tasks #239 / #240 / #241)
All three are verified closed in the 2026-04-23 exit-gate audit:
| Task | Item | Status |
|---|---|---|
| #239 | SealedBootstrap composition root — Phase7Composer.PrepareAsync + OpcUaServerService wiring |
Done |
| #240 | Live OPC UA e2e smoke — scripts/e2e/test-phase7-virtualtags.ps1 |
Done (partial pass: 3/7 stages reach PASS; writer/subscribe/alarm stages blocked by live Galaxy attribute activity + Historian SDK environment) |
| #241 | sp_ComputeGenerationDiff extension for Script / VirtualTag / ScriptedAlarm diff sections |
Done — migration 20260420232000_ExtendComputeGenerationDiffWithPhase7 |
What genuinely remains
These are real open items, not issues with the plan reconciliation.
Gap 1 — OPC UA method-call dispatch for scripted alarm Ack/Confirm/Shelve (Stream G / C.6)
DriverNodeManager.MethodCall does not route OPC UA Acknowledge / Confirm / OneShotShelve / TimedShelve / Unshelve / AddComment method invocations to the ScriptedAlarmEngine. Operators can acknowledge scripted alarms through the Admin UI today; OPC UA HMI clients expecting to use Part 9 method nodes directly cannot. Explicit in phase-7-e2e-smoke.md §"Known limitations".
Gap 2 — Admin UI: no /virtual-tags tab or form (Stream F.2)
VirtualTagService CRUD is fully tested but no razor page exposes it. Operators must author virtual tags through direct SQL or Admin API calls. ScriptsTab.razor covers script CRUD only; virtual-tag fields (EquipmentId, DataType, trigger config, Historize) have no UI form.
Gap 3 — Admin UI: no /scripted-alarms tab or form (Stream F.3)
ScriptedAlarmService CRUD is fully tested but no razor page exists. Only ScriptsTab.razor under the cluster detail view is present; there is no ScriptedAlarmsTab.razor or equivalent.
Gap 4 — Script log viewer not shipped (Stream F.5)
The SignalR tail of scripts-*.log filtered by ScriptName was not implemented. ScriptsTab.razor shows script output from the in-process harness but has no live-log panel for production emissions.
Gap 5 — Virtual-tag historization has no production sink (Stream B.6)
IHistoryWriter + NullHistoryWriter are present; VirtualTagEngine calls IHistoryWriter.Record per evaluation when Historize=true. Phase7EngineComposer.Compose passes NullHistoryWriter — no live writer is wired. Virtual-tag values are computed and served correctly but never persisted to any historian. Explicitly documented in docs/VirtualTags.md §"Upstream reads + history".
What is definitely done
- All four
Core.*projects (Core.Scripting,Core.VirtualTags,Core.ScriptedAlarms,Core.AlarmHistorian) ship with full implementation and test coverage. - Roslyn sandbox (allow-list +
ForbiddenTypeAnalyzerdefense-in-depth + 250 ms timeout + per-script Serilog sink + compile cache) is complete. - Virtual tag engine: dependency graph with iterative Tarjan SCC, topo-sort, change-trigger cascade, timer trigger,
IReadable+ISubscribableadapter, per-tag error isolation. - Scripted alarm engine: full Part 9 state machine, startup recovery, template substitution,
IAlarmSourcefan-out, 5-second shelving timer,IAlarmStateStore(in-memory default; DB-backed via Config DB entities). - SQLite store-and-forward historian sink: drain loop with exponential backoff, dead-letter retention, bounded capacity,
RetryDeadLetteredoperator action. - Config DB schema:
Script,VirtualTag,ScriptedAlarm,ScriptedAlarmStatetables with EF migrations and generation-diff extension. - Admin services:
ScriptService,VirtualTagService,ScriptedAlarmService,ScriptTestHarnessService,HistorianDiagnosticsService— all backed by unit tests. - Admin UI:
ScriptsTab.razor(Monaco-backed editor, dependency preview, test harness),AlarmsHistorian.razor(queue depth, drain state, retry dead-lettered). - Server-side composition:
Phase7Composer,Phase7EngineComposer,CachedTagUpstreamSource,DriverSubscriptionBridge,ScriptedAlarmReadable— fully wired intoOpcUaServerServicestartup sequence beforeOpcUaApplicationHost.StartAsync. EquipmentNodeWalkeremitsNodeSourceKind.VirtualandNodeSourceKind.ScriptedAlarmvariables;DriverNodeManagerdispatches reads and rejects writes to virtual nodes.WonderwareHistorianClient.WriteBatchAsyncimplementsIAlarmHistorianWriteras the alarm-event write path (deviation from plan's Galaxy.Host route, but functionally equivalent).- Compliance script
scripts/compliance/phase-7-compliance.ps1and e2e smokescripts/e2e/test-phase7-virtualtags.ps1both present.
Evidence sources
| Source | Path |
|---|---|
| Phase 7 plan | docs/v2/implementation/phase-7-scripting-and-alarming.md |
| Phase 7 exit gate | docs/v2/implementation/exit-gate-phase-7.md |
| E2E smoke runbook | docs/v2/implementation/phase-7-e2e-smoke.md |
| Virtual tags reference doc | docs/VirtualTags.md |
| Scripted alarms reference doc | docs/ScriptedAlarms.md |
Core.Scripting sources |
src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ |
Core.VirtualTags sources |
src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ |
Core.ScriptedAlarms sources |
src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ |
Core.AlarmHistorian sources |
src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ |
| Server Phase7 composition | src/Server/ZB.MOM.WW.OtOpcUa.Server/Phase7/ |
| Admin services | src/Server/ZB.MOM.WW.OtOpcUa.Admin/Services/Script*.cs, VirtualTagService.cs, HistorianDiagnosticsService.cs |
| Admin UI pages | src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor, AlarmsHistorian.razor |
| Historian sidecar writer | src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/WonderwareHistorianClient.cs |
| EF migrations | src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.cs, 20260420232000_ExtendComputeGenerationDiffWithPhase7.cs |