Phase 7 follow-up #246 — Phase7Composer + Program.cs wire-in #193

Merged
dohertj2 merged 1 commits from phase-7-fu-246-program-wireup into v2 2026-04-20 22:08:20 -04:00
Owner

Activates the Phase 7 engines in production. Loads Script + VirtualTag + ScriptedAlarm rows from the bootstrapped generation, wires the engines through the Phase7EngineComposer kernel (#243), starts the DriverSubscriptionBridge feed (#244), and late-binds the resulting IReadable sources to OpcUaApplicationHost before OPC UA server start.

Phase7Composer

Singleton orchestrator. PrepareAsync loads the three Phase 7 row sets in one DB scope, builds CachedTagUpstreamSource, calls Phase7EngineComposer.Compose, constructs DriverSubscriptionBridge with one DriverFeed per registered ISubscribable driver (path-to-fullRef map built from EquipmentNamespaceContent via MapPathsToFullRefs), starts the bridge.

DisposeAsync tears down in the right order: bridge first (no more events fired into the cache), then engines (cascades + timers stop), then any disposable sink.

MapPathsToFullRefs deterministic path convention: /{areaName}/{lineName}/{equipmentName}/{tagName} matching exactly what EquipmentNodeWalker emits into the OPC UA browse tree, so script literals against the operator-visible UNS tree work without translation. Tags missing EquipmentId or pointing at unknown Equipment are skipped silently.

OpcUaApplicationHost.SetPhase7Sources

New late-bind setter. Throws InvalidOperationException if called after StartAsync because OtOpcUaServer + DriverNodeManagers capture the field values at construction; mutation post-start would silently fail.

OpcUaServerService

After bootstrap loads the current generation, calls phase7Composer.PrepareAsync + applicationHost.SetPhase7Sources before applicationHost.StartAsync. StopAsync disposes Phase7Composer first so the bridge stops feeding the cache before the OPC UA server tears down its node managers.

Program.cs

Registers IAlarmHistorianSink as NullAlarmHistorianSink.Instance (task #247 swaps in the real Galaxy.Host-writer-backed SqliteStoreAndForwardSink), Serilog root logger, and Phase7Composer singleton.

Tests — 5 new Phase7ComposerMappingTests = 34 Phase 7 tests total

Maps tag → walker UNS path, skips null EquipmentId, skips unknown Equipment reference, multiple tags under same equipment map distinctly, empty content yields empty map. Pure functions; no DI/DB.

The real PrepareAsync DB query path can't be exercised without SQL Server in the test environment — exercised by the live E2E smoke (task #240) which unblocks once #247 lands.

Phase 7 production wiring chain

  • #243 composition kernel
  • #245 scripted-alarm IReadable adapter
  • #244 driver bridge
  • #246 this — Program.cs wire-in
  • 🟡 #247 — Galaxy.Host SqliteStoreAndForwardSink writer adapter (replaces NullSink)
  • 🟡 #240 — live E2E smoke (unblocks once #247 lands)
Activates the Phase 7 engines in production. Loads `Script` + `VirtualTag` + `ScriptedAlarm` rows from the bootstrapped generation, wires the engines through the `Phase7EngineComposer` kernel (#243), starts the `DriverSubscriptionBridge` feed (#244), and late-binds the resulting `IReadable` sources to `OpcUaApplicationHost` before OPC UA server start. ## `Phase7Composer` Singleton orchestrator. `PrepareAsync` loads the three Phase 7 row sets in one DB scope, builds `CachedTagUpstreamSource`, calls `Phase7EngineComposer.Compose`, constructs `DriverSubscriptionBridge` with one `DriverFeed` per registered `ISubscribable` driver (path-to-fullRef map built from `EquipmentNamespaceContent` via `MapPathsToFullRefs`), starts the bridge. `DisposeAsync` tears down in the right order: bridge first (no more events fired into the cache), then engines (cascades + timers stop), then any disposable sink. `MapPathsToFullRefs` deterministic path convention: `/{areaName}/{lineName}/{equipmentName}/{tagName}` matching exactly what `EquipmentNodeWalker` emits into the OPC UA browse tree, so script literals against the operator-visible UNS tree work without translation. Tags missing `EquipmentId` or pointing at unknown Equipment are skipped silently. ## `OpcUaApplicationHost.SetPhase7Sources` New late-bind setter. Throws `InvalidOperationException` if called after `StartAsync` because `OtOpcUaServer` + `DriverNodeManager`s capture the field values at construction; mutation post-start would silently fail. ## `OpcUaServerService` After bootstrap loads the current generation, calls `phase7Composer.PrepareAsync` + `applicationHost.SetPhase7Sources` before `applicationHost.StartAsync`. `StopAsync` disposes `Phase7Composer` first so the bridge stops feeding the cache before the OPC UA server tears down its node managers. ## Program.cs Registers `IAlarmHistorianSink` as `NullAlarmHistorianSink.Instance` (task #247 swaps in the real Galaxy.Host-writer-backed `SqliteStoreAndForwardSink`), Serilog root logger, and `Phase7Composer` singleton. ## Tests — 5 new `Phase7ComposerMappingTests` = **34 Phase 7 tests total** Maps tag → walker UNS path, skips null EquipmentId, skips unknown Equipment reference, multiple tags under same equipment map distinctly, empty content yields empty map. Pure functions; no DI/DB. The real `PrepareAsync` DB query path can't be exercised without SQL Server in the test environment — exercised by the live E2E smoke (task #240) which unblocks once #247 lands. ## Phase 7 production wiring chain - ✅ #243 composition kernel - ✅ #245 scripted-alarm IReadable adapter - ✅ #244 driver bridge - ✅ **#246 this — Program.cs wire-in** - 🟡 #247 — Galaxy.Host SqliteStoreAndForwardSink writer adapter (replaces NullSink) - 🟡 #240 — live E2E smoke (unblocks once #247 lands)
dohertj2 added 1 commit 2026-04-20 22:08:08 -04:00
Activates the Phase 7 engines in production. Loads Script + VirtualTag +
ScriptedAlarm rows from the bootstrapped generation, wires the engines through
the Phase7EngineComposer kernel (#243), starts the DriverSubscriptionBridge feed
(#244), and late-binds the resulting IReadable sources to OpcUaApplicationHost
before OPC UA server start.

## Phase7Composer (Server.Phase7)

Singleton orchestrator. PrepareAsync loads the three Phase 7 row sets in one
DB scope, builds CachedTagUpstreamSource, calls Phase7EngineComposer.Compose,
constructs DriverSubscriptionBridge with one DriverFeed per registered
ISubscribable driver (path-to-fullRef map built from EquipmentNamespaceContent
via MapPathsToFullRefs), starts the bridge.

DisposeAsync tears down in the right order: bridge first (no more events fired
into the cache), then engines (cascades + timers stop), then any disposable sink.

MapPathsToFullRefs: deterministic path convention is
  /{areaName}/{lineName}/{equipmentName}/{tagName}
matching exactly what EquipmentNodeWalker emits into the OPC UA browse tree, so
script literals against the operator-visible UNS tree work without translation.
Tags missing EquipmentId or pointing at unknown Equipment are skipped silently
(Galaxy SystemPlatform-style tags + dangling references handled).

## OpcUaApplicationHost.SetPhase7Sources

New late-bind setter. Throws InvalidOperationException if called after
StartAsync because OtOpcUaServer + DriverNodeManagers capture the field values
at construction; mutation post-start would silently fail.

## OpcUaServerService

After bootstrap loads the current generation, calls phase7Composer.PrepareAsync
+ applicationHost.SetPhase7Sources before applicationHost.StartAsync. StopAsync
disposes Phase7Composer first so the bridge stops feeding the cache before the
OPC UA server tears down its node managers (avoids in-flight cascades surfacing
as noisy shutdown warnings).

## Program.cs

Registers IAlarmHistorianSink as NullAlarmHistorianSink.Instance (task #247
swaps in the real Galaxy.Host-writer-backed SqliteStoreAndForwardSink), Serilog
root logger, and Phase7Composer singleton.

## Tests — 5 new Phase7ComposerMappingTests = 34 Phase 7 tests total

Maps tag → walker UNS path, skips null EquipmentId, skips unknown Equipment
reference, multiple tags under same equipment map distinctly, empty content
yields empty map. Pure functions; no DI/DB needed.

The real PrepareAsync DB query path can't be exercised without SQL Server in
the test environment — it's exercised by the live E2E smoke (task #240) which
unblocks once #247 lands.

## Phase 7 production wiring chain status
-  #243 composition kernel
-  #245 scripted-alarm IReadable adapter
-  #244 driver bridge
-  #246 this — Program.cs wire-in
- 🟡 #247 — Galaxy.Host SqliteStoreAndForwardSink writer adapter (replaces NullSink)
- 🟡 #240 — live E2E smoke (unblocks once #247 lands)
dohertj2 merged commit 42f3b17c4a into v2 2026-04-20 22:08:20 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#193