Files
lmxopcua/scripts/compliance/phase-7-compliance.ps1
Joseph Doherty fe91d42927 PR 7.2 — Retire legacy Galaxy projects + service
Matrix-gate satisfied (14 passed / 1 skipped / 0 failed on 2026-04-30
per docs/v2/Galaxy.ParityMatrix.md). Galaxy access flows through the
in-process GalaxyDriver → mxaccessgw exclusively. Legacy infrastructure
deleted in this commit:

Source projects (6):
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host         (.NET 4.8 x86 + MXAccess COM)
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy        (in-process pipe client)
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared       (pipe-IPC contracts)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests

Test projects with no consumer after legacy retired (3):
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E         (drove Galaxy.Host EXE)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests (drove both backends)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport (only consumed by Host/Proxy tests)

Edits:
- ZB.MOM.WW.OtOpcUa.slnx: drop nine project entries
- Server.csproj: drop Driver.Galaxy.Proxy ProjectReference
- Server/Program.cs: drop GalaxyProxyDriverFactoryExtensions.Register
  + the parallel-registration comment block; only GalaxyDriverFactoryExtensions
  registers now under DriverType "GalaxyMxGateway"
- Install-Services.ps1: rewrite to drop OtOpcUaGalaxyHost service install +
  the GalaxySharedSecret/ZbConnection/GalaxyClientName/GalaxyPipeName/
  AvevaServiceDependencies/MxAccessInitialConnect* parameters that only
  applied to the legacy host. Adds a closing note pointing operators at
  the separate mxaccessgw install
- Uninstall-Services.ps1: keep OtOpcUaGalaxyHost in the cleanup loop so
  pre-7.2 rigs upgrade-uninstall cleanly, plus add OtOpcUaWonderwareHistorian
- scripts/e2e/test-galaxy.ps1: deleted (drove the legacy E2E)
- scripts/e2e/e2e-config.sample.json: rewrite the galaxy section comment
  to reflect the GalaxyMxGateway-only path
- scripts/e2e/README.md: drop OtOpcUaGalaxyHost references
- scripts/compliance/phase-7-compliance.ps1: drop Galaxy.Shared
  HistorianAlarms* checks (those contracts moved to
  Driver.Historian.Wonderware.Client in PR 3.4)

Live state: OtOpcUaGalaxyHost Windows service stopped + removed via
NSSM before this commit. The dev box's Galaxy access is now exclusively
through the running mxaccessgw (separate repo).

Stays out of scope for PR 7.2 (PR 7.3 territory):
- CLAUDE.md Galaxy section rewrite
- mxaccess_documentation.md deletion
- Memory entries for the now-retired Galaxy.Host service

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:01:19 -04:00

152 lines
11 KiB
PowerShell

<#
.SYNOPSIS
Phase 7 exit-gate compliance check. Each check either passes or records a failure;
non-zero exit = fail.
.DESCRIPTION
Validates Phase 7 (scripting runtime + virtual tags + scripted alarms + historian
alarm sink + Admin UI + address-space integration) per
`docs/v2/implementation/phase-7-scripting-and-alarming.md`.
.NOTES
Usage: pwsh ./scripts/compliance/phase-7-compliance.ps1
Exit: 0 = all checks passed; non-zero = one or more FAILs
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$script:failures = 0
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
function Assert-Pass { param([string]$C) Write-Host " [PASS] $C" -ForegroundColor Green }
function Assert-Fail { param([string]$C, [string]$R) Write-Host " [FAIL] $C - $R" -ForegroundColor Red; $script:failures++ }
function Assert-Deferred { param([string]$C, [string]$P) Write-Host " [DEFERRED] $C (follow-up: $P)" -ForegroundColor Yellow }
function Assert-FileExists {
param([string]$C, [string]$P)
if (Test-Path (Join-Path $repoRoot $P)) { Assert-Pass "$C ($P)" }
else { Assert-Fail $C "missing file: $P" }
}
function Assert-TextFound {
param([string]$C, [string]$Pat, [string[]]$Paths)
foreach ($p in $Paths) {
$full = Join-Path $repoRoot $p
if (-not (Test-Path $full)) { continue }
if (Select-String -Path $full -Pattern $Pat -Quiet) {
Assert-Pass "$C (matched in $p)"
return
}
}
Assert-Fail $C "pattern '$Pat' not found in any of: $($Paths -join ', ')"
}
Write-Host ""
Write-Host "=== Phase 7 compliance - scripting + virtual tags + scripted alarms + historian ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "Stream A - Core.Scripting (Roslyn + sandbox + AST inference + logger)"
Assert-FileExists "Core.Scripting project" "src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ZB.MOM.WW.OtOpcUa.Core.Scripting.csproj"
Assert-TextFound "ScriptSandbox allow-list anchored on ScriptContext assembly" "contextType\.Assembly" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs")
Assert-TextFound "ForbiddenTypeAnalyzer defense-in-depth (plan decision #6)" "class ForbiddenTypeAnalyzer" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs")
Assert-TextFound "DependencyExtractor rejects non-literal paths (plan decision #7)" "class DependencyExtractor" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs")
Assert-TextFound "Per-script log companion sink forwards Error+ to main log (plan decision #12)" "class ScriptLogCompanionSink" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogCompanionSink.cs")
Assert-TextFound "ScriptContext static Deadband helper" "static bool Deadband" @("src/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptContext.cs")
Write-Host ""
Write-Host "Stream B - Core.VirtualTags (dependency graph + change/timer + source)"
Assert-FileExists "Core.VirtualTags project" "src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ZB.MOM.WW.OtOpcUa.Core.VirtualTags.csproj"
Assert-TextFound "DependencyGraph iterative Tarjan SCC (no stack overflow on 10k chains)" "class DependencyGraph" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs")
Assert-TextFound "VirtualTagEngine SemaphoreSlim async-safe cascade" "SemaphoreSlim" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs")
Assert-TextFound "VirtualTagSource IReadable + ISubscribable per ADR-002" "class VirtualTagSource" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs")
Assert-TextFound "TimerTriggerScheduler groups by interval" "class TimerTriggerScheduler" @("src/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/TimerTriggerScheduler.cs")
Write-Host ""
Write-Host "Stream C - Core.ScriptedAlarms (Part 9 state machine + predicate engine + IAlarmSource)"
Assert-FileExists "Core.ScriptedAlarms project" "src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"
Assert-TextFound "Part9StateMachine pure functions" "class Part9StateMachine" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs")
Assert-TextFound "Alarm condition state with GxP audit Comments list" "Comments" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs")
Assert-TextFound "MessageTemplate {TagPath} substitution (plan decision #13)" "class MessageTemplate" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs")
Assert-TextFound "AlarmPredicateContext rejects SetVirtualTag (predicates must be pure)" "class AlarmPredicateContext" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmPredicateContext.cs")
Assert-TextFound "ScriptedAlarmSource implements IAlarmSource" "class ScriptedAlarmSource" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs")
Assert-TextFound "IAlarmStateStore abstraction + in-memory default" "class InMemoryAlarmStateStore" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs")
Write-Host ""
Write-Host "Stream D - Core.AlarmHistorian (SQLite store-and-forward; alarm-event sidecar IPC moved to Driver.Historian.Wonderware.Client in PR 3.4)"
Assert-FileExists "Core.AlarmHistorian project" "src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"
Assert-TextFound "SqliteStoreAndForwardSink backoff ladder (1s..60s cap)" "BackoffLadder" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
Assert-TextFound "Default 1M row capacity + 30-day dead-letter retention (plan decision #21)" "DefaultDeadLetterRetention" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
Assert-TextFound "Per-event outcomes (Ack/RetryPlease/PermanentFail)" "HistorianWriteOutcome" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs")
# Galaxy.Shared pipe-IPC contracts retired in PR 7.2 alongside the rest of the legacy
# Galaxy projects. Wonderware sidecar contracts live in Driver.Historian.Wonderware.Client.
Write-Host ""
Write-Host "Stream E - Config DB schema"
Assert-FileExists "Script entity" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs"
Assert-FileExists "VirtualTag entity" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs"
Assert-FileExists "ScriptedAlarm entity" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs"
Assert-FileExists "ScriptedAlarmState entity (logical-id keyed per plan decision #14)" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs"
Assert-TextFound "VirtualTag trigger check constraint (change OR timer)" "CK_VirtualTag_Trigger_AtLeastOne" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
Assert-TextFound "ScriptedAlarm severity range check" "CK_ScriptedAlarm_Severity_Range" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
Assert-TextFound "ScriptedAlarm type enum check" "CK_ScriptedAlarm_AlarmType" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
Assert-TextFound "ScriptedAlarmState.CommentsJson is ISJSON (GxP audit)" "CK_ScriptedAlarmState_CommentsJson_IsJson" @("src/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs")
Assert-FileExists "Phase 7 migration present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260420231641_AddPhase7ScriptingTables.cs"
Write-Host ""
Write-Host "Stream F - Admin UI (services + Monaco editor + test harness + historian diagnostics)"
Assert-FileExists "ScriptService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptService.cs"
Assert-FileExists "VirtualTagService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/VirtualTagService.cs"
Assert-FileExists "ScriptedAlarmService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptedAlarmService.cs"
Assert-FileExists "ScriptTestHarnessService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptTestHarnessService.cs"
Assert-FileExists "HistorianDiagnosticsService" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/HistorianDiagnosticsService.cs"
Assert-FileExists "ScriptEditor Razor component" "src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptEditor.razor"
Assert-FileExists "ScriptsTab Razor component" "src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ScriptsTab.razor"
Assert-FileExists "AlarmsHistorian diagnostics page" "src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/AlarmsHistorian.razor"
Assert-FileExists "Monaco loader (CDN progressive enhancement)" "src/ZB.MOM.WW.OtOpcUa.Admin/wwwroot/js/monaco-loader.js"
Assert-TextFound "Scripts tab wired into DraftEditor" "ScriptsTab " @("src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor")
Assert-TextFound "Harness enforces declared-inputs-only contract (plan decision #22)" "UnknownInputs" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/ScriptTestHarnessService.cs")
Write-Host ""
Write-Host "Stream G - Address-space integration"
Assert-TextFound "NodeSourceKind discriminator in DriverAttributeInfo" "enum NodeSourceKind" @("src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs")
Assert-TextFound "Walker emits VirtualTag variables with Source=Virtual" "AddVirtualTagVariable" @("src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
Assert-TextFound "Walker emits ScriptedAlarm variables with Source=ScriptedAlarm + IsAlarm" "AddScriptedAlarmVariable" @("src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
Assert-TextFound "EquipmentNamespaceContent carries VirtualTags + ScriptedAlarms" "VirtualTags" @("src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs")
Assert-TextFound "DriverNodeManager selects IReadable by source kind" "SelectReadable" @("src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
Assert-TextFound "Virtual/ScriptedAlarm writes rejected (plan decision #6)" "IsWriteAllowedBySource" @("src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs")
Write-Host ""
Write-Host "Deferred surfaces"
Assert-Deferred "SealedBootstrap composition root wiring (VirtualTagEngine + ScriptedAlarmEngine + SqliteStoreAndForwardSink)" "task #239"
Assert-Deferred "Live OPC UA end-to-end test (virtual-tag Read + scripted-alarm Ack via method node)" "task #240"
Assert-Deferred "sp_ComputeGenerationDiff extension for Script/VirtualTag/ScriptedAlarm sections" "task #241"
Write-Host ""
Write-Host "Cross-cutting"
Write-Host " Running full solution test suite..." -ForegroundColor DarkGray
$prevPref = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
$testOutput = & dotnet test (Join-Path $repoRoot 'ZB.MOM.WW.OtOpcUa.slnx') --nologo 2>&1
$ErrorActionPreference = $prevPref
$passLine = $testOutput | Select-String 'Passed:\s+(\d+)' -AllMatches
$failLine = $testOutput | Select-String 'Failed:\s+(\d+)' -AllMatches
$passCount = 0; foreach ($m in $passLine.Matches) { $passCount += [int]$m.Groups[1].Value }
$failCount = 0; foreach ($m in $failLine.Matches) { $failCount += [int]$m.Groups[1].Value }
# Phase 6.4 exit-gate baseline was 1137; Phase 7 adds ~197 across 7 streams.
$baseline = 1300
if ($passCount -ge $baseline) { Assert-Pass "No test-count regression ($passCount >= $baseline pre-Phase-7-exit baseline)" }
else { Assert-Fail "Test-count regression" "passed $passCount < baseline $baseline" }
if ($failCount -le 1) { Assert-Pass "No new failing tests (pre-existing CLI flake tolerated)" }
else { Assert-Fail "New failing tests" "$failCount failures > 1 tolerated" }
Write-Host ""
if ($script:failures -eq 0) {
Write-Host "Phase 7 compliance: PASS" -ForegroundColor Green
exit 0
}
Write-Host "Phase 7 compliance: $script:failures FAIL(s)" -ForegroundColor Red
exit 1