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
43 KiB
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).
EnterWorktreeis broken here — usegit 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 withClaude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB. - Every public member needs an XML doc comment (
checkdocsenforces 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.
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(afterIsLocked) - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Instances/InstanceAlarmOverride.cs:44(afterPriorityLevelOverride) - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/FlattenedConfiguration.cs:125(afterIsLockedinResolvedAlarm) - Test:
tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/EntityConventionTests.cs(existing sweep, must stay green)
Step 1: Add the three properties
TemplateAlarm.cs:
/// <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:
/// <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):
/// <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
dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter "FullyQualifiedName~EntityConventionTests|FullyQualifiedName~MessageConventionTests" 2>&1 | tail -3
Expected: all pass.
Step 3: Commit
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):
[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):
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:
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
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
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 onFlatten_Alarms_ResolvedFromChainat :221)
Step 1: Write three failing tests (copy the arrange block of Flatten_Alarms_ResolvedFromChain):
Flatten_MesReportable_TemplateValueCarried— template alarmIsMesReportable = true→ resolvedIsMesReportable == true; a second alarm left default →false.Flatten_MesReportable_OverrideFalseWins— templatetrue, overrideIsMesReportableOverride = false→ resolvedfalse,Source == "Override".Flatten_MesReportable_OverrideNullInherits— templatetrue, override with onlyPriorityLevelOverride = 7→ resolvedIsMesReportable == 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):
IsLocked = alarm.IsLocked,
IsMesReportable = alarm.IsMesReportable,
In ApplyInstanceAlarmOverrides (:358):
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
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(UpdateAlarmAsyncfield 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 existingTemplateService/InstanceServicealarm tests — find withgrep -rn "UpdateAlarmAsync\|SetAlarmOverrideAsync" tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests)
Step 1: Failing tests
UpdateAlarmAsync_CopiesIsMesReportable: existing alarmfalse, proposedtrue→ storedtrue.SetAlarmOverrideAsync_StoresIsMesReportableOverride: call withisMesReportableOverride: true→ repo receives an override withIsMesReportableOverride == true; a second call withnullclears it.
Step 2: Implement
TemplateService.UpdateAlarmAsync — beside existing.IsLocked = proposed.IsLocked;:
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):
string user,
bool? isMesReportableOverride = null,
CancellationToken cancellationToken = default)
and in both branches:
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— sendnew AddTemplateAlarmCommand(1, "HighTemp", "HiLo", 500, null, null, false, IsMesReportable: true); assert theTemplateAlarmhanded toTemplateService.AddAlarmAsynchasIsMesReportable == true.SetInstanceAlarmOverride_PassesIsMesReportableOverride—new SetInstanceAlarmOverrideCommand(10, "HighTemp", null, null, IsMesReportableOverride: false)→InstanceService.SetAlarmOverrideAsyncreceivesfalse.
Step 2: Implement — trailing optionals, never reorder:
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);
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
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 afterIsConfiguredPlaceholder) - 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 theGetAlarmSnapshotRequestharness — 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:
/// <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:
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):
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
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 positionalbool 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 anAlarms.CurrentAsync()script (grepCurrentAsyncundertests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests), and the CentralUISandboxScriptHostdiagnose-clean test (grepAlarms.CurrentAsyncundertests/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. CompileSurfaceParityTestsalready pairsAlarmsAccessor↔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:
internal async Task<IReadOnlyList<ScriptAlarm>> GetMesReportableAlarmsAsync(CancellationToken cancellationToken = default)
=> (await GetAlarmsAsync(cancellationToken)).Where(a => a.IsMesReportable).ToList();
…
IsConfiguredPlaceholder: a.IsConfiguredPlaceholder,
IsMesReportable: a.IsMesReportable);
AlarmsAccessor:
/// <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— appendbool IsMesReportable = false),:395-398(InstanceAlarmOverrideDto— appendbool? 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 = trueon the seededHighalarm (:233) andIsMesReportableOverride = falseon the seededHiAlarmoverride (:808). The reflection sweep now fails until the DTO carries both. ArtifactDiffTests: an alarm differing only inIsMesReportableclassifies as Changed (not Identical); same for an override differing only inIsMesReportableOverride.EntitySerializerTests: a bundle JSON lacking the two new keys deserializes tofalse/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 addcurrent.IsMesReportableto the audit payload objectTemplateChildEquality.AlarmsEqual:&& e.IsMesReportable == i.IsMesReportableArtifactDiffoverride lambda:&& e.IsMesReportableOverride == i.IsMesReportableOverrideBundleImporter:4904(ResolvedAlarmfor validation):IsMesReportable = al.IsMesReportableTemplateChildEqualityalso has an override-equality helper if one exists — grepPriorityLevelOverrideinTransport/Importand extend every comparison you find.
Step 4: Run
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):
[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):
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):
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 ofTemplateNativeAlarmSourceEditorTests.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:
<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:
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
<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— addAlarmOverride_MesReportable_RoundTripsbesideAlarmOverride_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:
<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:
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.mdif it lists alarm flags - Modify:
CLAUDE.md— the native-alarm bullet under Data & Communication: add one sentence "Computed alarms carry anIsMesReportableflag (template checkbox, instance-overridable) exposed asScriptAlarm.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 by2026-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-levelIsMesReportableonTemplateNativeAlarmSource+ override) and 122 (proto field 25 /DebugAlarmStateDtofield 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(mirrordocker/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):
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[@]}"):
template create --name MesAlarmDemo --description "MES-reportable alarm flag demo"(skip iftemplate list | find_id name MesAlarmDemosucceeds).template attribute add --template-id $T --name Temperature --data-type Double --value 20(static — no--data-source).template alarm add --template-id $T --name HighTemp --trigger-type HiLo --priority 500 --attribute Temperature --hi 80 --hihi 95 --mes-reportabletemplate alarm add --template-id $T --name TempSpike --trigger-type ValueMatch --priority 300 --attribute Temperature --match-value 100template alarm add --template-id $T --name SensorFault --trigger-type ValueMatch --priority 700 --attribute Temperature --match-value -1template 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()whenFlaggedOnly, elseCurrentAsync(); legacy filter order;IsFlaggedForMES = a.IsMesReportable). On re-run usetemplate script update.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.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:
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(mirrorverify-delmia-notifier.shassertions: python3 one-liners over CLI JSON / curl JSON;set -euo pipefail; afail()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:
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
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
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.