Files
ScadaBridge/docs/plans/2026-09-11-mes-reportable-alarm-flag.md
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

43 KiB
Raw Permalink Blame History

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.

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:

    /// <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 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):

                    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 (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;:

        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 — 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:

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 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:

    /// <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 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:

    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 — 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

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 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:

<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 — 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:

<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.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):

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:
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:

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.