Files
ScadaBridge/docs/plans/2026-09-11-mes-reportable-alarm-flag.md
T
Joseph Doherty 73f2a032e0 docs: MES-reportable alarm flag — design + 16-task implementation plan
Moves the legacy BTDB MachineAlarm.FlaggedForMES lookup onto TemplateAlarm.IsMesReportable
(instance-overridable), surfaces it as ScriptAlarm.IsMesReportable / Alarms.MesReportableAsync(),
and adds an AlarmStatus inbound method demo on the docker rig. Native alarms stay on the
severity band (no per-alarm row).

Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
2026-09-11 09:50:54 -04:00

673 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MES-Reportable Alarm Flag Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
**Goal:** Replace the legacy BTDB `MachineAlarm.FlaggedForMES` lookup with an `IsMesReportable` checkbox on template alarms (instance-overridable), expose it to site scripts through `Alarms.CurrentAsync()` / `Alarms.MesReportableAsync()`, and prove it end-to-end on the docker rig through an `AlarmStatus` inbound API method.
**Architecture:** One additive `bool` on `TemplateAlarm` + one `bool?` on `InstanceAlarmOverride` (null = inherit), resolved by the flattener into `ResolvedAlarm.IsMesReportable`, which already ships to the site inside `deployed_configurations.config_json`. The site `InstanceActor` stamps the flag on its alarm snapshot in ONE place (`BuildAlarmStatesSnapshot`), so `ScriptAlarm.IsMesReportable` is correct for fired and unfired alarms. No proto/wire change; native alarms (source bindings, no per-alarm row) are out of scope and stay on the 900–999 severity band. The inbound method and site script are deployed config seeded by `docker/seed-mes-alarm-demo.sh`.
**Tech Stack:** .NET 10, EF Core (SQL Server migrations), Akka.NET, Roslyn scripting, Blazor Server + Bootstrap, System.CommandLine CLI, bash + `scadabridge` CLI on the docker rig.
**Design:** `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md`. Mockup: artifact "MES Reportable Alarms".
**Conventions for every task** (from `CLAUDE.md` + memory):
- Work in a dedicated worktree (Task 0). `EnterWorktree` is broken here — use `git worktree add`.
- Run **targeted** tests per task (`dotnet test <proj> --filter …`), the affected-project build, and the full solution build only in Task 15.
- Never `git stash` (shared across worktrees). Commit with pathspecs. End every commit message with
`Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB`.
- Every public member needs an XML doc comment (`checkdocs` enforces it).
- Message contracts and Transport DTOs evolve **additively with trailing defaulted parameters** — never reorder.
---
### Task 0: Worktree
**Classification:** trivial
**Estimated implement time:** ~2 min
**Parallelizable with:** none
**Files:** none (git only)
**Step 1:** Create the worktree and confirm a clean build baseline.
```bash
cd /Users/dohertj2/Desktop/ScadaBridge
git worktree add -b feat/mes-reportable-alarm-flag ../ScadaBridge-mes-reportable main
cd ../ScadaBridge-mes-reportable
dotnet build ZB.MOM.WW.ScadaBridge.slnx 2>&1 | tail -3
```
Expected: `0 Error(s)`. All later tasks run from `/Users/dohertj2/Desktop/ScadaBridge-mes-reportable` (absolute paths in every command — Bash cwd drifts).
---
### Task 1: Commons — entity, override and resolved-alarm fields
**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** none (everything depends on it)
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateAlarm.cs:18` (after `IsLocked`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceAlarmOverride.cs:44` (after `PriorityLevelOverride`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs:125` (after `IsLocked` in `ResolvedAlarm`)
- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/EntityConventionTests.cs` (existing sweep, must stay green)
**Step 1: Add the three properties**
`TemplateAlarm.cs`:
```csharp
/// <summary>
/// When true, the alarm is reported to MES by the alarm-status API
/// (<c>IsFlaggedForMES</c> on the legacy contract). Default false. Overridable per
/// instance via <see cref="Instances.InstanceAlarmOverride.IsMesReportableOverride"/>
/// unless the alarm is locked.
/// </summary>
public bool IsMesReportable { get; set; }
```
`InstanceAlarmOverride.cs`:
```csharp
/// <summary>
/// Replaces the alarm's <c>IsMesReportable</c> flag when set. <c>null</c> = keep inherited.
/// </summary>
public bool? IsMesReportableOverride { get; set; }
```
`FlattenedConfiguration.cs` (`ResolvedAlarm`):
```csharp
/// <summary>Gets whether the alarm is reported to MES (template value after any instance override).</summary>
public bool IsMesReportable { get; init; }
```
**Step 2: Build + run the convention sweeps**
```bash
dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter "FullyQualifiedName~EntityConventionTests|FullyQualifiedName~MessageConventionTests" 2>&1 | tail -3
```
Expected: all pass.
**Step 3: Commit**
```bash
git add src/ZB.MOM.WW.ScadaBridge.Commons
git commit -m "feat(commons): IsMesReportable on TemplateAlarm, ResolvedAlarm; IsMesReportableOverride on InstanceAlarmOverride"
```
---
### Task 2: Configuration Database — migration
**Classification:** high-risk (migration)
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 3, Task 6, Task 8 (after Task 1)
**Files:**
- Create: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/<ts>_AddAlarmMesReportable.cs` (+ `.Designer.cs`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ScadaBridgeDbContextModelSnapshot.cs` (generated)
- Test: `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SchemaConfigurationTests.cs:144-157`
**Step 1: Write the failing test** — extend the existing TemplateAlarm persist test (or add beside it):
```csharp
[Fact]
public async Task TemplateAlarm_And_Override_PersistMesReportableFlags()
{
// arrange as the sibling test does (in-memory/SQLite context + a template + an instance)
var alarm = new TemplateAlarm("HighTemp") { TemplateId = template.Id, TriggerType = AlarmTriggerType.HiLo, PriorityLevel = 500, IsMesReportable = true };
var ovr = new InstanceAlarmOverride("HighTemp") { InstanceId = instance.Id, IsMesReportableOverride = false };
ctx.TemplateAlarms.Add(alarm); ctx.InstanceAlarmOverrides.Add(ovr);
await ctx.SaveChangesAsync();
ctx.ChangeTracker.Clear();
Assert.True((await ctx.TemplateAlarms.SingleAsync(a => a.Id == alarm.Id)).IsMesReportable);
Assert.False((await ctx.InstanceAlarmOverrides.SingleAsync(o => o.Id == ovr.Id)).IsMesReportableOverride);
}
```
**Step 2: Scaffold the migration — BUILD FIRST, no `--no-build`** (memory: `--no-build` scaffolds an empty migration):
```bash
dotnet build src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase 2>&1 | tail -1
dotnet ef migrations add AddAlarmMesReportable \
--project src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase \
--startup-project src/ZB.MOM.WW.ScadaBridge.Host
```
Verify the `Up()` matches the `20260517000628_AddDerivedAlarmFields.cs` shape:
```csharp
migrationBuilder.AddColumn<bool>(name: "IsMesReportable", table: "TemplateAlarms", type: "bit", nullable: false, defaultValue: false);
migrationBuilder.AddColumn<bool>(name: "IsMesReportableOverride", table: "InstanceAlarmOverrides", type: "bit", nullable: true);
```
If the file is empty, `rm` the two generated files and re-run the build + add.
**Step 3: Run the tests**
```bash
dotnet test tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests --filter "FullyQualifiedName~SchemaConfigurationTests" 2>&1 | tail -3
```
Expected: PASS. No explicit EF mapping change is needed (`bool`/`bool?` map by convention).
**Step 4: Commit**
```bash
git add src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests
git commit -m "feat(config-db): AddAlarmMesReportable migration (TemplateAlarms.IsMesReportable, InstanceAlarmOverrides.IsMesReportableOverride)"
```
---
### Task 3: Template Engine — flattening
**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 2, Task 6, Task 8
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:450-461` (projection) and `:358-363` (override merge)
- Test: `tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/FlatteningServiceTests.cs` (model on `Flatten_Alarms_ResolvedFromChain` at :221)
**Step 1: Write three failing tests** (copy the arrange block of `Flatten_Alarms_ResolvedFromChain`):
- `Flatten_MesReportable_TemplateValueCarried` — template alarm `IsMesReportable = true` → resolved `IsMesReportable == true`; a second alarm left default → `false`.
- `Flatten_MesReportable_OverrideFalseWins` — template `true`, override `IsMesReportableOverride = false` → resolved `false`, `Source == "Override"`.
- `Flatten_MesReportable_OverrideNullInherits` — template `true`, override with only `PriorityLevelOverride = 7` → resolved `IsMesReportable == true`.
**Step 2: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests --filter "FullyQualifiedName~Flatten_MesReportable"` → 3 FAIL.
**Step 3: Implement**
In the projection (`:450`):
```csharp
IsLocked = alarm.IsLocked,
IsMesReportable = alarm.IsMesReportable,
```
In `ApplyInstanceAlarmOverrides` (`:358`):
```csharp
PriorityLevel = ovr.PriorityLevelOverride ?? existing.PriorityLevel,
IsMesReportable = ovr.IsMesReportableOverride ?? existing.IsMesReportable,
```
**Step 4: Run** the same filter → 3 PASS; then the whole `Flattening` folder filter to confirm no regression.
**Step 5: Commit**
```bash
git add src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening
git commit -m "feat(template-engine): flatten IsMesReportable with null-inherits override semantics"
```
---
### Task 4: Template Engine — service write paths
**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** none (Task 5, 10, 11 depend on the new signature)
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:~619-627` (`UpdateAlarmAsync` field copy block: `existing.PriorityLevel = proposed.PriorityLevel; …`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs:218-280` (`SetAlarmOverrideAsync`)
- Test: `tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/` (the existing `TemplateService`/`InstanceService` alarm tests — find with `grep -rn "UpdateAlarmAsync\|SetAlarmOverrideAsync" tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests`)
**Step 1: Failing tests**
- `UpdateAlarmAsync_CopiesIsMesReportable`: existing alarm `false`, proposed `true` → stored `true`.
- `SetAlarmOverrideAsync_StoresIsMesReportableOverride`: call with `isMesReportableOverride: true` → repo receives an override with `IsMesReportableOverride == true`; a second call with `null` clears it.
**Step 2: Implement**
`TemplateService.UpdateAlarmAsync` — beside `existing.IsLocked = proposed.IsLocked;`:
```csharp
existing.IsMesReportable = proposed.IsMesReportable;
```
`InstanceService.SetAlarmOverrideAsync` — new trailing optional parameter **after `user`** (all current callers pass five positional args; grep `SetAlarmOverrideAsync(` across `src/` and `tests/` to confirm):
```csharp
string user,
bool? isMesReportableOverride = null,
CancellationToken cancellationToken = default)
```
and in both branches:
```csharp
existingOverride.IsMesReportableOverride = isMesReportableOverride;
…
PriorityLevelOverride = priorityLevelOverride,
IsMesReportableOverride = isMesReportableOverride
```
Update the XML `<param>` docs. `AddAlarmAsync` needs nothing — it persists the entity as given.
**Step 3: Run** the TemplateEngine test project filtered to `Alarm` → PASS.
**Step 4: Commit** `feat(template-engine): persist IsMesReportable on alarm update; IsMesReportableOverride on SetAlarmOverrideAsync`.
---
### Task 5: Management commands + ManagementActor
**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** none
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs:45-46`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs:39-43`
- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:3173-3201`, `:1171-1182`
- Test: `tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs`
**Step 1: Failing tests** (model on the existing add-alarm / set-override tests in `ManagementActorTests`):
- `AddTemplateAlarm_CarriesIsMesReportable` — send `new AddTemplateAlarmCommand(1, "HighTemp", "HiLo", 500, null, null, false, IsMesReportable: true)`; assert the `TemplateAlarm` handed to `TemplateService.AddAlarmAsync` has `IsMesReportable == true`.
- `SetInstanceAlarmOverride_PassesIsMesReportableOverride` — `new SetInstanceAlarmOverrideCommand(10, "HighTemp", null, null, IsMesReportableOverride: false)` → `InstanceService.SetAlarmOverrideAsync` receives `false`.
**Step 2: Implement** — trailing optionals, never reorder:
```csharp
public record AddTemplateAlarmCommand(int TemplateId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked, bool IsMesReportable = false);
public record UpdateTemplateAlarmCommand(int AlarmId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked, bool IsMesReportable = false);
```
```csharp
public record SetInstanceAlarmOverrideCommand(
int InstanceId,
string AlarmCanonicalName,
string? TriggerConfigurationOverride,
int? PriorityLevelOverride,
bool? IsMesReportableOverride = null);
```
`HandleAddAlarm` / `HandleUpdateAlarm`: add `IsMesReportable = cmd.IsMesReportable` to both initializers. `HandleSetInstanceAlarmOverride`: pass `cmd.IsMesReportableOverride` after `user.Username`.
Note the PLAN-08 T9 finding: Newtonsoft ignores C# ctor optional-parameter defaults on the wire → an old CLI that omits the field yields `default(T)` = `false` / `null`, which is exactly the intended inherit/off default.
**Step 3: Run**
```bash
dotnet test tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests --filter "FullyQualifiedName~ManagementActorTests|FullyQualifiedName~RequiredRoleMatrixTests" 2>&1 | tail -3
```
Expected: PASS. `RequiredRoleMatrixTests` is untouched — no new command names.
**Step 4: Commit** `feat(management): IsMesReportable on alarm commands, IsMesReportableOverride on SetInstanceAlarmOverride`.
---
### Task 6: Site Runtime — stamp the flag on the alarm snapshot
**Classification:** high-risk (actor)
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 2, Task 3, Task 8
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Streaming/AlarmStateChanged.cs:109` (append after `IsConfiguredPlaceholder`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:53` (field), `:1982` (spawn), `:1569-1613` (`BuildAlarmStatesSnapshot`)
- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorNativeAlarmTests.cs` (has the `GetAlarmSnapshotRequest` harness — add beside it)
**Step 1: Failing test** `AlarmSnapshot_StampsIsMesReportable_OnFiredAndUnfiredAlarms`: deploy a `FlattenedConfiguration` with two computed alarms (`HighTemp` `IsMesReportable=true`, `TempSpike` `false`); drive `HighTemp` active (write the attribute past the setpoint, as the sibling tests do); Ask `GetAlarmSnapshotRequest`; assert the `HighTemp` row (retained event) has `IsMesReportable == true` and the `TempSpike` row (fallback) `false`.
**Step 2: Implement**
`AlarmStateChanged.cs`:
```csharp
/// <summary>
/// True when the alarm's deployed configuration flags it as MES-reportable
/// (<c>ResolvedAlarm.IsMesReportable</c>). Stamped by the Instance Actor on its alarm
/// snapshot; always false for native conditions, which have no per-alarm config row.
/// NOT carried on the gRPC stream in this slice (no proto field) — central sees the
/// default. Additive; default false.
/// </summary>
public bool IsMesReportable { get; init; }
```
`InstanceActor.cs`:
```csharp
private readonly Dictionary<string, bool> _alarmMesReportable = new();
…
_alarmPriorities[alarm.CanonicalName] = alarm.PriorityLevel;
_alarmMesReportable[alarm.CanonicalName] = alarm.IsMesReportable;
```
`BuildAlarmStatesSnapshot()` — stamp every row in one place (records support `with`):
```csharp
var states = _latestAlarmEvents.Values
.Select(e => e with { IsMesReportable = _alarmMesReportable.GetValueOrDefault(e.AlarmName) })
.ToList();
foreach (var name in _alarmActors.Keys)
{
if (_latestAlarmEvents.ContainsKey(name)) continue;
states.Add(new AlarmStateChanged(/* unchanged five args */)
{
IsMesReportable = _alarmMesReportable.GetValueOrDefault(name)
});
}
```
Native placeholder rows are untouched (`false`). Also clear `_alarmMesReportable` wherever `_alarmPriorities` is cleared on undeploy/redeploy (grep `_alarmPriorities.Clear`).
**Step 3: Run**
```bash
dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests --filter "FullyQualifiedName~InstanceActorNativeAlarmTests|FullyQualifiedName~InstanceActorTests" 2>&1 | tail -3
dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter "FullyQualifiedName~MessageConventionTests|FullyQualifiedName~AlarmStateChanged" 2>&1 | tail -3
```
Expected: PASS (the Commons additive/back-compat tests for `AlarmStateChanged` still deserialize a pre-flag JSON to `false`).
**Step 4: Commit** `feat(site-runtime): stamp IsMesReportable on the Instance Actor alarm snapshot`.
---
### Task 7: Script surface — `ScriptAlarm.IsMesReportable` + `Alarms.MesReportableAsync()`
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 8, Task 9 (after Task 6)
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptAlarm.cs:58-79` (trailing positional `bool IsMesReportable = false` + `<param>` doc)
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs:1263-1304` (`GetAlarmsAsync`, `ToScriptAlarm`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs:372-401` (`AlarmsAccessor`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs:150-157` (`CompileAlarmsAccessor`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs:151-164` (`SandboxAlarmsAccessor`)
- Test: `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/AlarmsAccessorTests.cs`, `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/CompileSurfaceParityTests.cs`, the ScriptAnalysis compile-surface test that compiles an `Alarms.CurrentAsync()` script (grep `CurrentAsync` under `tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests`), and the CentralUI `SandboxScriptHost` diagnose-clean test (grep `Alarms.CurrentAsync` under `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests`).
**Step 1: Failing tests**
- `AlarmsAccessorTests.CurrentAsync_ProjectsIsMesReportable` — respond with a row `{ IsMesReportable = true }` → `ScriptAlarm.IsMesReportable == true`.
- `AlarmsAccessorTests.MesReportableAsync_FiltersToFlaggedRows` — respond with one flagged + one unflagged row → exactly the flagged one.
- Compile-surface + sandbox tests: extend the existing script literal to `var r = await Alarms.MesReportableAsync(); var f = r.Where(a => a.IsMesReportable);` → compiles with zero diagnostics.
- `CompileSurfaceParityTests` already pairs `AlarmsAccessor` ↔ `CompileAlarmsAccessor` (`:112`) — it fails automatically until the mirror gains the member.
**Step 2: Implement**
`ScriptAlarm`: append `bool IsMesReportable = false` as the last positional parameter (existing 21-arg call sites keep compiling).
`ScriptRuntimeContext`:
```csharp
internal async Task<IReadOnlyList<ScriptAlarm>> GetMesReportableAlarmsAsync(CancellationToken cancellationToken = default)
=> (await GetAlarmsAsync(cancellationToken)).Where(a => a.IsMesReportable).ToList();
…
IsConfiguredPlaceholder: a.IsConfiguredPlaceholder,
IsMesReportable: a.IsMesReportable);
```
`AlarmsAccessor`:
```csharp
/// <summary>
/// <see cref="CurrentAsync"/> restricted to alarms whose deployed configuration flags
/// them MES-reportable (template checkbox, or an instance override). Native conditions
/// are never included — they carry no per-alarm flag. Placeholder rows are excluded.
/// </summary>
public Task<IReadOnlyList<ScriptAlarm>> MesReportableAsync(CancellationToken cancellationToken = default)
=> _ctx.GetMesReportableAlarmsAsync(cancellationToken);
```
`CompileAlarmsAccessor`: same signature → `throw new NotSupportedException(CompileOnly)`.
`SandboxAlarmsAccessor`: same signature → `throw new ScriptSandboxException("Alarms.MesReportableAsync() reads the site Instance Actor's live alarm conditions, which aren't available in the central Test Run sandbox — deploy to a site to exercise alarm-reading scripts.")`.
**Step 3: Run** the four test projects filtered to `Alarms|CompileSurfaceParity|SandboxScriptHost` → PASS.
**Step 4: Commit** `feat(scripts): ScriptAlarm.IsMesReportable + Alarms.MesReportableAsync() mirrored on both compile surfaces`.
---
### Task 8: Transport bundles
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 2, Task 3, Task 6, Task 7
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs:212-221` (`TemplateAlarmDto` — append `bool IsMesReportable = false`), `:395-398` (`InstanceAlarmOverrideDto` — append `bool? IsMesReportableOverride = null`)
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs:66-74`, `:285-288`, `:346-359`, `:603-611`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/TemplateChildEquality.cs:57-65`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs:488-496`
- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:2168-2178`, `:2392-2416`, `:4357-4364`, `:4904-4916`
- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/RoundTripEquivalenceTests.cs:233-241`, `:808-812`; `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/ArtifactDiffTests.cs`; `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs`
**Step 1: Failing tests**
- Round-trip guard: set `IsMesReportable = true` on the seeded `High` alarm (`:233`) and `IsMesReportableOverride = false` on the seeded `HiAlarm` override (`:808`). The reflection sweep now fails until the DTO carries both.
- `ArtifactDiffTests`: an alarm differing only in `IsMesReportable` classifies as Changed (not Identical); same for an override differing only in `IsMesReportableOverride`.
- `EntitySerializerTests`: a bundle JSON lacking the two new keys deserializes to `false` / `null`.
**Step 2: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests --filter RoundTripEquivalence` → FAIL naming the two properties.
**Step 3: Implement** — carry the field at every listed site:
- serializer export: `IsMesReportable: a.IsMesReportable` / `IsMesReportableOverride: o.IsMesReportableOverride`
- serializer import + importer entity builds (`:346`, `:603`, `:2168`, `:4357`): `IsMesReportable = al.IsMesReportable` / `IsMesReportableOverride = o.IsMesReportableOverride`
- `SyncTemplateAlarmsAsync` (`:2392`): `current.IsMesReportable = alarmDto.IsMesReportable;` and add `current.IsMesReportable` to the audit payload object
- `TemplateChildEquality.AlarmsEqual`: `&& e.IsMesReportable == i.IsMesReportable`
- `ArtifactDiff` override lambda: `&& e.IsMesReportableOverride == i.IsMesReportableOverride`
- `BundleImporter:4904` (`ResolvedAlarm` for validation): `IsMesReportable = al.IsMesReportable`
- `TemplateChildEquality` also has an override-equality helper if one exists — grep `PriorityLevelOverride` in `Transport/Import` and extend every comparison you find.
**Step 4: Run**
```bash
dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.Tests 2>&1 | tail -3
dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests --filter "RoundTripEquivalence|BundleImporterApply|ArtifactDiff" 2>&1 | tail -3
```
Expected: PASS.
**Step 5: Commit** `feat(transport): carry IsMesReportable / IsMesReportableOverride through bundles (round-trip guard extended)`.
---
### Task 9: CLI flags + README
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 7, Task 8, Task 10, Task 11
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs:683-684` (add option), `:709-730` (register on add), `:747-754` (pass), `:758-770` (update option), `:790-797` (pass)
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs:551-568`
- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/README.md:263-313` (alarm add/update option tables), `:808-821` (alarm-override set)
- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/EditingParityTests.cs:31-36`
**Step 1: Failing tests** — add `InlineData` rows (exact envelope, camelCase, all fields):
```csharp
[InlineData("template alarm add --template-id 12 --name HighTemp --trigger-type HiLo --priority 500 --mes-reportable", "AddTemplateAlarm", "{\"templateId\":12,\"name\":\"HighTemp\",\"triggerType\":\"HiLo\",\"priorityLevel\":500,\"description\":null,\"triggerConfiguration\":null,\"isLocked\":false,\"isMesReportable\":true}")]
[InlineData("instance alarm-override set --instance-id 6 --alarm HighTemp --mes-reportable false", "SetInstanceAlarmOverride", "{\"instanceId\":6,\"alarmCanonicalName\":\"HighTemp\",\"triggerConfigurationOverride\":null,\"priorityLevelOverride\":null,\"isMesReportableOverride\":false}")]
```
(If the existing serializer emits `triggerConfiguration` differently for a no-config add, copy the exact string the failing assertion prints.)
**Step 2: Implement**
Template add + update (mirror `--locked`):
```csharp
var mesReportableOption = new Option<bool>("--mes-reportable") { Description = "Report this alarm to MES (IsFlaggedForMES on the alarm-status API)" };
mesReportableOption.DefaultValueFactory = _ => false;
```
register on the command, pass `result.GetValue(mesReportableOption)` as the new last ctor arg (a separate `updateMesReportableOption` for `update`, like `updateLockedOption`).
Instance override — nullable means inherit (the `api-method update --trace-all-calls` `Option<bool?>` idiom, `ApiMethodCommands.cs:98-102`):
```csharp
var setMesReportableOption = new Option<bool?>("--mes-reportable") { Description = "MES-reportable override: true|false; omit to inherit the template value" };
```
README: add the `--mes-reportable` row to the three option tables and the synopsis lines; note "update replaces the whole entity — omit `--mes-reportable` and it resets to false" on `template alarm update` (that is the existing `--locked` behaviour too).
**Step 3: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CLI.Tests --filter "EditingParity|UpdateCommandContract"` → PASS.
**Step 4: Commit** `feat(cli): --mes-reportable on template alarm add/update and instance alarm-override set`.
---
### Task 10: Central UI — template alarm dialog + Alarms table
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 9, Task 11
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateAlarmDialog.razor:38-43` (markup), `:68`, `:84`, `:94`, `:106`, `:128-133`
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor:906-934` (table), `:1436-1469` (open/show), `:1518-1540` (save)
- Test: create `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/TemplateAlarmDialogMesReportableTests.cs` (structural, copy the shape of `TemplateNativeAlarmSourceEditorTests.cs:16-67`)
**Step 1: Failing test** — reads `TemplateAlarmDialog.razor` from disk and asserts it contains `@bind="_isMesReportable"`, `id="alarmMesReportable"`, and the label text `MES reportable`; reads `TemplateEdit.razor` and asserts the Alarms table has a `<th>MES</th>` header and a `data-test="alarm-mes-badge"` cell.
**Step 2: Implement**
Dialog markup, directly after the Locked block:
```razor
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="_isMesReportable" id="alarmMesReportable" />
<label class="form-check-label" for="alarmMesReportable">MES reportable</label>
<div class="form-text">Returned by the MES alarm-status API as <code>IsFlaggedForMES</code>. Instances can override unless the alarm is locked.</div>
</div>
</div>
```
Code: `[Parameter] public bool InitialIsMesReportable { get; set; }`, `private bool _isMesReportable;`, seed in `OnInitialized`, pass in `Submit`, and extend the record:
```csharp
public sealed record AlarmDraft(string Name, AlarmTriggerType TriggerType, int Priority, string? TriggerConfig, bool IsLocked, bool IsMesReportable);
```
`TemplateEdit.razor`: default draft gets `false`; edit draft passes `alarm.IsMesReportable`; `ShowAlarmDialogAsync` passes `InitialIsMesReportable="initial.IsMesReportable"`; both `proposed`/`alarm` initializers in `SaveAlarmDraftAsync` set `IsMesReportable = draft.IsMesReportable`. Table: header `<th>MES</th>` between Config and Lock; cell
```razor
<td data-test="alarm-mes-badge">
@if (alarm.IsMesReportable) { <span class="badge bg-success-subtle text-success-emphasis border" aria-label="MES reportable">MES</span> }
else { <span class="text-muted">—</span> }
</td>
```
**Step 3: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter "TemplateAlarmDialogMesReportable|TemplateEdit"` → PASS. `dotnet build src/ZB.MOM.WW.ScadaBridge.CentralUI` → 0 errors.
**Step 4: Commit** `feat(ui): MES reportable checkbox on the template alarm dialog + MES column on the Alarms tab`.
---
### Task 11: Central UI — instance override modal (tri-state)
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 9, Task 10
**Files:**
- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor:441-499` (table), `:531-541` (modal field), `:1532` (`OverrideSummary`), `:1564-1583` (`BeginEditOverride`), `:1628-1660` (delta check + save)
- Test: create `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/InstanceConfigureMesReportableTests.cs` (structural on the razor source, plus a unit test of the delta rule if you extract it into a static helper — recommended: `static bool HasOverrideDelta(string? json, int? priority, bool? mes)`)
- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Deployment/InstanceConfigureTests.cs` — add `AlarmOverride_MesReportable_RoundTrips` beside `AlarmOverride_SetPriority_ThenClear_RoundTrips` (`:195-249`); runs against the rig in Task 14.
**Step 1: Failing tests** — structural: `data-test="alarm-mes-select"` exists with three `<option>`s; helper test: `(null, null, true)` → delta, `(null, null, null)` → no delta.
**Step 2: Implement**
Modal, a second `col-md-6` beside Priority override:
```razor
<div class="col-md-6">
<label class="form-label small text-uppercase text-muted fw-semibold mb-1">MES reportable</label>
<select class="form-select form-select-sm" data-test="alarm-mes-select" @bind="_editingMesReportableText">
<option value="">Inherited (@(_editingAlarm.IsMesReportable ? "reportable" : "not reportable"))</option>
<option value="true">Reportable</option>
<option value="false">Not reportable</option>
</select>
</div>
```
Code: `private string _editingMesReportableText = "";` seeded in `BeginEditOverride` from `existing?.IsMesReportableOverride?.ToString().ToLowerInvariant() ?? ""`; in `SaveOverrideFromModal`:
```csharp
bool? mes = _editingMesReportableText switch { "true" => true, "false" => false, _ => null };
if (string.IsNullOrWhiteSpace(overrideJson) && !priority.HasValue && mes is null) // ← the delta rule now counts the flag
```
and pass `mes` to `SetAlarmOverrideAsync(Id, alarmName, overrideJson, priority, currentUser, mes)`.
`OverrideSummary`: when `ovr.IsMesReportableOverride is bool m` add `parts.Add(m ? "mesReportable" : "notMesReportable")`. Overrides table: in the *Inherited Config* cell append the `MES` badge when `alarm.IsMesReportable`; in the *Override* cell the summary already renders the new part.
**Step 3: Run** `dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests --filter InstanceConfigure` → PASS; `dotnet build src/ZB.MOM.WW.ScadaBridge.CentralUI` → 0 errors. Playwright test compiles (`dotnet build tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests`) but runs in Task 14.
**Step 4: Commit** `feat(ui): tri-state MES reportable override on Instance Configure (reportable-only override is a delta)`.
---
### Task 12: Documentation
**Classification:** trivial
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 13
**Files:**
- Modify: `docs/requirements/Component-TemplateEngine.md` (alarm fields + override granularity), `Component-SiteRuntime.md` (Alarms accessor), `Component-ScriptAnalysis.md` (surface list), `Component-InboundAPI.md` (AlarmStatus routing example), `Component-CentralUI.md`, `Component-ConfigurationDatabase.md` (migration list), `Component-CLI.md` if it lists alarm flags
- Modify: `CLAUDE.md` — the native-alarm bullet under *Data & Communication*: add one sentence "Computed alarms carry an `IsMesReportable` flag (template checkbox, instance-overridable) exposed as `ScriptAlarm.IsMesReportable` / `Alarms.MesReportableAsync()`; native alarms have no per-alarm row and keep the 900–999 band."
- Modify: `docs/plans/2026-06-30-mes-alarm-status-api.md` §6.1 — prefix "**Superseded for COMPUTED alarms 2026-09-11** by `2026-09-11-mes-reportable-alarm-flag-design.md`; still the rule for native alarms."
- Modify: `docs/plans/2026-07-08-deferred-work-register.md` — rows 121 (source-level `IsMesReportable` on `TemplateNativeAlarmSource` + override) and 122 (proto field 25 / `DebugAlarmStateDto` field 20 + Alarm Summary badge).
- Modify: `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md` — Status → IMPLEMENTED once Task 15 lands.
**Step 1:** Make the edits. **Step 2:** `grep -rn "severity band\|900–999\|900-999" docs/requirements CLAUDE.md` and make sure each hit now says "native only". **Step 3: Commit** `docs: MES-reportable alarm flag — component docs, CLAUDE.md, register rows 121–122`.
---
### Task 13: Rig — rebuild + seed script
**Classification:** standard
**Estimated implement time:** ~5 min (plus deploy time)
**Parallelizable with:** Task 12
**Files:**
- Create: `docker/seed-mes-alarm-demo.sh` (mirror `docker/seed-flow-log-demo.sh`: header comment, `CLI=` prebuilt binary, `A=(…)`, `find_id`, heredoc bodies, idempotent by name)
- Modify: `docker/README.md` (add to the seed-script list, if one exists — `grep -n seed-flow-log docker/README.md`)
**Step 1: Rebuild the rig** (server-side changes + migration; the rig auto-applies migrations at boot):
```bash
cd /Users/dohertj2/Desktop/ScadaBridge-mes-reportable
dotnet build src/ZB.MOM.WW.ScadaBridge.CLI 2>&1 | tail -1
bash docker/deploy.sh 2>&1 | tail -5
until curl -sf http://localhost:9000/health/ready >/dev/null; do sleep 2; done
docker logs scadabridge-central-a 2>&1 | grep -i "AddAlarmMesReportable" | head -2
```
Expected: the migration name appears in the central log once.
**Step 2: Write the seed** — sequence (all via `"$CLI" … "${A[@]}"`):
1. `template create --name MesAlarmDemo --description "MES-reportable alarm flag demo"` (skip if `template list | find_id name MesAlarmDemo` succeeds).
2. `template attribute add --template-id $T --name Temperature --data-type Double --value 20` (static — no `--data-source`).
3. `template alarm add --template-id $T --name HighTemp --trigger-type HiLo --priority 500 --attribute Temperature --hi 80 --hihi 95 --mes-reportable`
`template alarm add --template-id $T --name TempSpike --trigger-type ValueMatch --priority 300 --attribute Temperature --match-value 100`
`template alarm add --template-id $T --name SensorFault --trigger-type ValueMatch --priority 700 --attribute Temperature --match-value -1`
4. `template script add --template-id $T --name AlarmStatus --trigger-type Call --parameters '<schema §2.5 of the design, flat: MachineCode, FlaggedOnly, IncludeAcked, MinSeverity, MaxSeverity, NameFilter>' --return-def '<AlarmStatusResponse schema>' --code "$SITE_CODE"` — body = the script in the design mockup step 3 (`Alarms.MesReportableAsync()` when `FlaggedOnly`, else `CurrentAsync()`; legacy filter order; `IsFlaggedForMES = a.IsMesReportable`). On re-run use `template script update`.
5. `instance create --name MesAlarmDemo01 --template-id $T --site-id 1`; `instance alarm-override set --instance-id $I --alarm SensorFault --mes-reportable true`; `instance deploy --id $I`.
6. `api-method create --name AlarmStatus --timeout 30 --parameters '{"type":"object","properties":{"MachineCode":{"type":"string"},"AlarmFilter":{"type":"object","properties":{"FlaggedOnly":{"type":"boolean"},"IncludeAcked":{"type":"boolean"},"MinSeverity":{"type":"integer"},"MaxSeverity":{"type":"integer"},"NameFilter":{"type":"string"}}}},"required":["MachineCode"]}' --return-def '<same AlarmStatusResponse schema>' --script "$ROUTER_CODE"` where `$ROUTER_CODE`:
```csharp
var code = (Parameters.ContainsKey("MachineCode") ? Parameters["MachineCode"]?.ToString() : null) ?? "";
var f = Parameters.ContainsKey("AlarmFilter") ? Parameters["AlarmFilter"] as System.Collections.Generic.Dictionary<string, object?> : null;
object? G(string k) => f != null && f.TryGetValue(k, out var v) ? v : null;
try {
return await Route.To(code).Call("AlarmStatus", new {
MachineCode = code,
FlaggedOnly = G("FlaggedOnly") as bool? ?? false,
IncludeAcked = G("IncludeAcked") as bool? ?? true,
MinSeverity = G("MinSeverity") is long mn ? (int?)mn : null,
MaxSeverity = G("MaxSeverity") is long mx ? (int?)mx : null,
NameFilter = G("NameFilter")?.ToString() });
} catch (Exception ex) when (ex.Message.Contains("not found on instance")) {
return new { WasSuccessful = false, ErrorText = $"Alarm status is not supported on machine '{code}'", Alarms = Array.Empty<object>() };
} catch (Exception ex) {
return new { WasSuccessful = false, ErrorText = "AlarmStatus failed: " + ex.Message, Alarms = Array.Empty<object>() };
}
```
and `api-method create --name SimpleAlarmStatus …` with params `{MachineCode}` and a body that calls the same site script with `FlaggedOnly = true, IncludeAcked = true`.
Verify in the api-method design page / `flow-debug run-method` that both compile — memory `inbound-noncompiling-update-keeps-old-handler`: a bad body is silently ignored on update.
7. `security api-key create --name mes-alarm-demo --methods AlarmStatus,SimpleAlarmStatus` → print `MES_ALARM_DEMO_KEY=sbk_…` once (mirror `seed-delmia-api.sh:45-46`).
8. `site deploy-artifacts --site-id 1` so the site has the method's dependencies, then `instance deploy --id $I` again if the script was updated.
**Step 3: Run it** `bash docker/seed-mes-alarm-demo.sh` → prints the ids and the key. Run it twice — second run must succeed (idempotent).
**Step 4: Commit** `feat(docker): seed-mes-alarm-demo.sh — MesAlarmDemo template/instance, AlarmStatus + SimpleAlarmStatus inbound methods`.
---
### Task 14: Rig — verify script + live gate + Playwright
**Classification:** standard
**Estimated implement time:** ~5 min (plus run time)
**Parallelizable with:** none
**Files:**
- Create: `docker/verify-mes-alarm-demo.sh` (mirror `verify-delmia-notifier.sh` assertions: python3 one-liners over CLI JSON / curl JSON; `set -euo pipefail`; a `fail()` helper; PASS/FAIL summary)
- Modify: `docs/plans/2026-09-11-mes-reportable-alarm-flag-design.md` — add §8 *Rig live gate results* with the measured output.
**Step 1: Write the verify script** — steps, each asserting on JSON:
```bash
KEY="${MES_ALARM_DEMO_KEY:?export the key printed by seed-mes-alarm-demo.sh}"
I=$("$CLI" instance list "${A[@]}" | find_id uniqueName MesAlarmDemo01)
call() { curl -sS -X POST "http://localhost:9000/api/$1" -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d "$2"; }
trip() { "$CLI" flow-debug run-value-change --instance-id "$I" --attribute Temperature --value "$1" "${A[@]}" >/dev/null; sleep 2; }
# A 96 → AlarmStatus: exactly HighTemp, IsFlaggedForMES true
# B 100 → AlarmStatus FlaggedOnly=false: HighTemp + TempSpike (TempSpike false); FlaggedOnly=true: TempSpike absent
# C -1 → SimpleAlarmStatus: SensorFault present, IsFlaggedForMES true (instance override wins over template false)
# D MinSeverity=600 on AlarmStatus at -1 → SensorFault only
# E 20 → AlarmStatus: WasSuccessful true, Alarms []
# F MachineCode=DelmiaSim01 → WasSuccessful false, ErrorText contains "not supported on machine"
# G MachineCode=NoSuchMachine → WasSuccessful false, ErrorText contains "not found"
# H debug snapshot --id $I at step A shows HighTemp Active (cross-check)
```
Watch the trigger semantics while writing B: a HiLo alarm at 100 is still active (HiHi), so step B expects two rows.
**Step 2: Run** `MES_ALARM_DEMO_KEY=sbk_… bash docker/verify-mes-alarm-demo.sh` → `PASS 8/8`. If a step fails, use `superpowers-extended-cc:systematic-debugging` before touching code — likely suspects: `Parameters.Get<bool?>` vs `long` coercion in the router (`ParameterValidator.Materialize` yields `long`/`bool`), the 2 s settle after `run-value-change`, or the instance not redeployed after a script update.
**Step 3: Playwright override round-trip**
```bash
dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests --filter "AlarmOverride_MesReportable_RoundTrips" 2>&1 | tail -3
```
Expected: PASS (rig login is disabled → fixture login still works, per the cluster-ops skill).
**Step 4: Record results** in the design doc §8 (commands, row counts, any surprise) and commit `test(docker): verify-mes-alarm-demo.sh — live gate PASS; design §8 results`.
---
### Task 15: Finish — sweep, review, merge, memory
**Classification:** standard
**Estimated implement time:** ~5 min (plus test time)
**Parallelizable with:** none
**Step 1: Full build + targeted sweep of every touched project**
```bash
dotnet build ZB.MOM.WW.ScadaBridge.slnx 2>&1 | grep -E "error|Warn.*CS" | head; \
for p in Commons ConfigurationDatabase TemplateEngine ManagementService SiteRuntime ScriptAnalysis Transport Transport.IntegrationTests CLI CentralUI; do \
dotnet test tests/ZB.MOM.WW.ScadaBridge.$p.Tests 2>&1 | tail -1; done
```
Expected: 0 errors; every project `Passed!` (the known env-only Playwright role-negative failures excepted).
**Step 2: `git diff main --stat` review**, then `git log --oneline main..HEAD` — one commit per task above.
**Step 3: Merge** via `superpowers-extended-cc:finishing-a-development-branch` (fast-forward `main`, delete the worktree). Do NOT push unless the user says so; QA/prod (`vq03`/`vq04`/`vd03`) are NOT deployed (remote exec is on hold — CLAUDE.md 2026-09-11 note).
**Step 4: Memory** — write `mes-reportable-alarm-flag-state.md` (status, merged SHA, rig seeded, key name, QA/prod not deployed, follow-ups = register rows 121/122) and add its line to `MEMORY.md`.