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
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
# MES-Reportable Alarm Flag — Design
|
||||
|
||||
**Date:** 2026-09-11
|
||||
**Status:** DRAFT — awaiting user review. Nothing implemented; nothing committed.
|
||||
**Supersedes (partially):** the severity-band decision in
|
||||
[`2026-06-30-mes-alarm-status-api.md`](2026-06-30-mes-alarm-status-api.md) §6.1 for **computed**
|
||||
alarms only (native alarms keep the band — see §2.3).
|
||||
**Component touchpoints:** Template Engine (#1), Configuration Database, Site Runtime (#3),
|
||||
Script Analysis (#25), Central UI, CLI, Transport (#24), Inbound API (#14, deployed config only).
|
||||
**Mockup:** https://claude.ai/code/artifact/02ffcbf9-b87f-4e12-852d-38e2910c6056 (artifact "MES Reportable Alarms").
|
||||
**Implementation plan:** [`2026-09-11-mes-reportable-alarm-flag.md`](2026-09-11-mes-reportable-alarm-flag.md) (16 tasks).
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
The legacy WWSupport / APIServer MES alarm endpoints decided which alarms were MES-relevant by a
|
||||
per-machine row in the BTDB `MachineAlarm` table (`Name`, `Severity`, `FlaggedForMES`). ScadaBridge
|
||||
already owns the alarm definition on the template, so the flag belongs there too:
|
||||
|
||||
1. Add a **`IsMesReportable` boolean (checkbox, default `false`)** to template alarms, and a
|
||||
matching **tri-state override** (`inherited` / `true` / `false`) on instance alarm overrides.
|
||||
2. Carry the flag through flattening to the site so the script engine's alarm read
|
||||
(`Alarms.CurrentAsync()`) exposes it and a script can **filter by it**.
|
||||
3. Ship an **inbound API method that routes to the instance and performs the alarm check**,
|
||||
returning the legacy `AlarmStatusResponse` shape with `IsFlaggedForMES` driven by the flag.
|
||||
4. Prove all of it on the **local docker rig** (`docker/`) — seed template, instance, method, key,
|
||||
trip an alarm, call the endpoint.
|
||||
|
||||
No BTDB dependency anywhere. Everything the check needs lives on the deployed configuration.
|
||||
|
||||
---
|
||||
|
||||
## 2. Decisions and scope
|
||||
|
||||
### 2.1 Where the flag lives (data model)
|
||||
|
||||
| Layer | Change | Semantics |
|
||||
|---|---|---|
|
||||
| `TemplateAlarm` (Commons entity) | `bool IsMesReportable` | Default `false`. Inherits down the template chain like `PriorityLevel`. |
|
||||
| `InstanceAlarmOverride` (Commons entity) | `bool? IsMesReportableOverride` | `null` = inherit from template (the existing nullable-means-inherited convention, same as `PriorityLevelOverride`). |
|
||||
| `ResolvedAlarm` (flattened DTO) | `bool IsMesReportable` | Effective value after override; rides `deployed_configurations.config_json` unchanged — **no site schema change**. |
|
||||
| Config DB | migration `AddAlarmMesReportable` | `TemplateAlarms.IsMesReportable bit NOT NULL DEFAULT 0`; `InstanceAlarmOverrides.IsMesReportableOverride bit NULL`. |
|
||||
|
||||
Naming follows the entity idiom already in the file (`IsLocked`, `IsInherited`). The legacy JSON
|
||||
name `IsFlaggedForMES` is preserved **only on the API response**, where MES clients depend on it.
|
||||
|
||||
**Locking:** the flag is an ordinary overridable field. A `Locked` alarm cannot be overridden at all
|
||||
(existing rule, `LockEnforcer.ValidateAlarmOverride`); an unlocked alarm's flag can be flipped per
|
||||
instance. `Name`/`TriggerType` stay the only fixed fields.
|
||||
|
||||
### 2.2 How the site exposes it (script engine)
|
||||
|
||||
The flag is stamped onto the alarm snapshot the script engine already reads:
|
||||
|
||||
- `InstanceActor` records `IsMesReportable` per alarm at Alarm Actor spawn (beside the existing
|
||||
`_alarmPriorities` map) and **stamps it in `BuildAlarmStatesSnapshot()`** on every row
|
||||
(`with { IsMesReportable = … }`), retained or fallback. One place, so it cannot drift from an emit
|
||||
site. `AlarmActor` itself is untouched.
|
||||
- `AlarmStateChanged` gains an additive init-only `bool IsMesReportable` (default `false`) — the
|
||||
same additive convention as `AckTime`. **It is deliberately NOT added to the gRPC protos in this
|
||||
slice**; central never sees it (see §2.4).
|
||||
- `ScriptAlarm` gains a trailing `bool IsMesReportable` positional parameter. Native rows are
|
||||
always `false`.
|
||||
- `AlarmsAccessor` gains **`MesReportableAsync(ct)`** = `CurrentAsync` filtered to
|
||||
`IsMesReportable`. `CurrentAsync()` is unchanged (an optional bool before the `CancellationToken`
|
||||
would break positional callers). Scripts can equally `.Where(a => a.IsMesReportable)`.
|
||||
- The accessor stub is mirrored on both hand-maintained compile surfaces — `ScriptCompileSurface`
|
||||
(`CompileAlarmsAccessor`) and the Central UI `SandboxScriptHost` (`SandboxAlarmsAccessor`) — and
|
||||
pinned by `CompileSurfaceParityTests`. `FlowShapePredictor` does not name accessor members, so
|
||||
it needs no change.
|
||||
|
||||
### 2.3 Native alarms are OUT of scope (and why)
|
||||
|
||||
Native alarms (OPC UA A&C / MxGateway mirrors) have **no per-alarm configuration row** anywhere:
|
||||
only the *source binding* (`TemplateNativeAlarmSource`) is configured, and the individual conditions
|
||||
are discovered at runtime. There is nowhere to put a per-alarm checkbox.
|
||||
|
||||
Consequences, stated plainly:
|
||||
|
||||
- This feature covers **computed `TemplateAlarm`s**. The production CvdReactor endpoints, whose
|
||||
alarms are all native sources, are **not changed** by it and keep the severity-band convention
|
||||
(900–999) from the 2026-06-30 plan. `IsFlaggedForMES` for a native row therefore stays
|
||||
`Severity in band`; for a computed row it becomes the flag.
|
||||
- Follow-up options, recorded in the deferred-work register: (a) a source-level
|
||||
`IsMesReportable` on `TemplateNativeAlarmSource` + its override ("every condition under this
|
||||
binding is reportable"), or (b) a new per-condition config entity — the first of its kind. Neither
|
||||
is designed here.
|
||||
|
||||
### 2.4 Central visibility is a follow-up
|
||||
|
||||
The Alarm Summary / Debug View rows are `AlarmStateChanged` values that arrive over gRPC. Showing a
|
||||
"MES" badge there needs additive proto fields (`AlarmStateUpdate` field 25, `DebugAlarmStateDto`
|
||||
field 20, manual `regen-proto.sh`) plus mapper changes. Not needed for the script check or the
|
||||
inbound API, so it is **deferred** (register row) to keep this slice free of wire changes.
|
||||
Design-time surfaces (template Alarms tab, Instance Configure overrides) DO show the flag.
|
||||
|
||||
### 2.5 The inbound API method (deployed config, rig)
|
||||
|
||||
`AlarmStatus` — the full-filter endpoint from the legacy spec, minus the BTDB machine lookup
|
||||
(the rig has no BTDB; `MachineCode` is the instance `UniqueName`, which is exactly what
|
||||
`Route.To(code)` resolves). Production adds its `SAPID → Code` lookup in front, as `MesMoveIn` does.
|
||||
|
||||
Request (`--parameters`, JSON Schema):
|
||||
```json
|
||||
{ "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"] }
|
||||
```
|
||||
Response (`--return-def`): `{ WasSuccessful, ErrorText, Alarms: AlarmInfo[] }` with
|
||||
`AlarmInfo = { Name, HierarchicalName, Description, IsFlaggedForMES, Severity, StatusCode,
|
||||
TriggeredDT, AckDT, AckComment }` — byte-compatible with the legacy contract.
|
||||
|
||||
Router body: `Route.To(MachineCode).Call("AlarmStatus", { MachineCode, ...AlarmFilter })`,
|
||||
catching `"not found on instance"` → `Alarms status is not supported on machine '<code>'`, any
|
||||
other exception → `WasSuccessful=false` with `Alarms` cleared (legacy all-or-nothing).
|
||||
|
||||
Site script `AlarmStatus` (Call, root of the demo template): `Alarms.CurrentAsync()` →
|
||||
`Active && !IsConfiguredPlaceholder` → `FlaggedOnly ? IsMesReportable : true` → severity bounds →
|
||||
name substring → `IncludeAcked` → project to `AlarmInfo` with `IsFlaggedForMES = a.IsMesReportable`.
|
||||
Filter order matches the legacy spec.
|
||||
|
||||
A `SimpleAlarmStatus` variant (flagged-only, acked always included) is one extra `api-method create`
|
||||
with the same site script and `FlaggedOnly=true` hard-wired; included in the seed as the second
|
||||
endpoint so both legacy routes are demonstrated.
|
||||
|
||||
### 2.6 Rig demo (all local)
|
||||
|
||||
`docker/seed-mes-alarm-demo.sh` (idempotent by name lookup, same style as `seed-flow-log-demo.sh`):
|
||||
|
||||
| Object | Detail |
|
||||
|---|---|
|
||||
| Template `MesAlarmDemo` | static `Double` attribute `Temperature` = 20 (static ⇒ a value-change trips alarms synchronously, no device round-trip, no OPC UA String-only trap). |
|
||||
| Alarm `HighTemp` | HiLo on `Temperature`, hi 80 / hihi 95, priority 500, **`--mes-reportable`**. |
|
||||
| Alarm `TempSpike` | ValueMatch `Temperature == 100`, priority 300, not reportable. |
|
||||
| Alarm `SensorFault` | ValueMatch `Temperature == -1`, priority 700, not reportable **on the template**. |
|
||||
| Script `AlarmStatus` | Call, root; body per §2.5. |
|
||||
| Instance `MesAlarmDemo01` | site 1; **alarm override `SensorFault` → `IsMesReportableOverride = true`** to prove the override path; deployed. |
|
||||
| Inbound `AlarmStatus`, `SimpleAlarmStatus` | per §2.5; one API key `mes-alarm-demo` scoped to both. |
|
||||
|
||||
`docker/verify-mes-alarm-demo.sh`:
|
||||
|
||||
1. `flow-debug run-value-change --instance-id … --attribute Temperature --value 96` → HighTemp active.
|
||||
2. `curl POST /api/AlarmStatus {"MachineCode":"MesAlarmDemo01"}` → 1 row, `IsFlaggedForMES=true`.
|
||||
3. `--value 100` → TempSpike active; `AlarmStatus` with `FlaggedOnly=true` → still only
|
||||
HighTemp-style rows (TempSpike excluded); `FlaggedOnly=false` → both, TempSpike `false`.
|
||||
4. `--value -1` → SensorFault active; `SimpleAlarmStatus` → SensorFault present with
|
||||
`IsFlaggedForMES=true` (override wins over the template's `false`).
|
||||
5. `--value 20` → all clear; `AlarmStatus` → empty `Alarms`, `WasSuccessful=true`.
|
||||
6. `AlarmStatus {"MachineCode":"DelmiaSim01"}` (no script) → `WasSuccessful=false`, "not
|
||||
supported on machine".
|
||||
7. Cross-check step 2 against `debug snapshot --id …` and the Flow Debugger replay.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data flow
|
||||
|
||||
```
|
||||
Central UI / CLI ──AddTemplateAlarm(IsMesReportable)──► ManagementActor ► TemplateService ► TemplateAlarms
|
||||
Central UI / CLI ──SetInstanceAlarmOverride(IsMesReportableOverride?)──► InstanceService ► InstanceAlarmOverrides
|
||||
│
|
||||
FlatteningService.ResolveInheritedAlarms → ResolvedAlarm.IsMesReportable
|
||||
FlatteningService.ApplyInstanceAlarmOverrides (ovr ?? existing)
|
||||
│ deploy (config_json, unchanged schema)
|
||||
▼
|
||||
Site: InstanceActor spawns AlarmActors; _alarmMesReportable[name] = alarm.IsMesReportable
|
||||
BuildAlarmStatesSnapshot() stamps IsMesReportable on every AlarmStateChanged row
|
||||
GetAlarmSnapshotResponse ─► ScriptRuntimeContext.ToScriptAlarm ─► ScriptAlarm.IsMesReportable
|
||||
│
|
||||
site script AlarmStatus: Alarms.CurrentAsync() / MesReportableAsync() → AlarmInfo[]
|
||||
▲
|
||||
Central inbound method AlarmStatus ── Route.To(MachineCode).Call("AlarmStatus", filter) ──┘
|
||||
◄── X-API-Key ── MES client (curl on the rig)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Alternatives considered
|
||||
|
||||
| Option | Verdict |
|
||||
|---|---|
|
||||
| **Keep the severity band (900–999) as the flag** (2026-06-30 decision) | Rejected for computed alarms: it couples MES relevance to priority, which operators tune for other reasons, and it is invisible in the template editor. Kept for native alarms only because there is no per-alarm row. |
|
||||
| **Central lookup table** (a `MesReportableAlarms` table joined at request time, i.e. re-creating BTDB `MachineAlarm`) | Rejected: the check runs at the site (that is the whole point of `Route.To().Call`) and the central table would be a second source of truth beside the template. |
|
||||
| **Stamp the flag in `AlarmActor` emits** instead of the snapshot | Rejected: three emit sites to keep in sync; the snapshot builder already owns the "fallback row for a quiet alarm" case, so stamping there covers fired and unfired alarms in one place. |
|
||||
| **`CurrentAsync(bool reportableOnly = false, ct)`** | Rejected: an optional bool before the token breaks existing positional `CurrentAsync(ct)` callers on three mirrored surfaces. A separate `MesReportableAsync` is additive. |
|
||||
| **Put `IsMesReportable` on the protos now** | Deferred (§2.4): no consumer in this slice; proto regen is manual and adds review surface. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Error handling
|
||||
|
||||
- **Override on a locked alarm** → existing `LockEnforcer` error; UI hides locked alarms from the
|
||||
override list already.
|
||||
- **Override with no delta** — Instance Configure deletes an override when neither config nor
|
||||
priority is set; the delta check must include the tri-state so a reportable-only override is
|
||||
**not** silently deleted (known trap, `InstanceConfigure.razor` ~line 1628).
|
||||
- **Old bundles / old artifacts**: the Transport DTO fields are trailing with defaults; a bundle
|
||||
without them imports as `false`/`null`. A `FlattenedConfiguration` serialized before the change
|
||||
deserializes with `IsMesReportable=false`.
|
||||
- **Mixed version**: a NEW central deploying to an OLD site — the site ignores the unknown JSON
|
||||
property; scripts there see `false` everywhere. Upgrade sites with the central, as always.
|
||||
- **Inbound method**: unknown machine → `Instance '…' not found` text passed through as
|
||||
`ErrorText`; missing script → the "not supported" mapping; everything else → `WasSuccessful=false`
|
||||
with `Alarms` cleared. Never an HTTP 500 for a handled case.
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing
|
||||
|
||||
| Layer | Tests |
|
||||
|---|---|
|
||||
| Commons | entity + message convention sweeps pass unchanged (plain `bool`, record init-prop); `ScriptAlarm` positional back-compat test. |
|
||||
| Config DB | migration applies on SQL Server + SQLite test double; `SchemaConfigurationTests` persist round-trip of the two new columns. |
|
||||
| Template Engine | `FlatteningServiceTests`: template `true` → resolved `true`; override `false` wins; override `null` inherits; inherited (derived-template) alarm carries base value; `LockEnforcerTests` unchanged. |
|
||||
| Site Runtime | `InstanceActor` snapshot stamps the flag on fired AND unfired alarms; `AlarmsAccessorTests`: `MesReportableAsync` filters; `CompileSurfaceParityTests` sees the new member on all mirrors. |
|
||||
| Script Analysis / Central UI | compile-surface + `SandboxScriptHost` compile a script using `MesReportableAsync()` and `a.IsMesReportable`. |
|
||||
| Management | `ManagementActorTests` add/update/override carry the field; authz matrix unchanged (no new command names). |
|
||||
| CLI | `EditingParityTests` rows for `template alarm add --mes-reportable` and `instance alarm-override set --mes-reportable false`; README updated. |
|
||||
| Transport | `RoundTripEquivalenceTests` seeds non-default values (the reflection sweep fails otherwise); `TemplateChildEquality`/`ArtifactDiff` tests see the flag as a change. |
|
||||
| Central UI | structural test on `TemplateAlarmDialog.razor` (`@bind="_isMesReportable"`); bUnit/Playwright override round-trip `AlarmOverride_MesReportable_RoundTrips` beside the priority one. |
|
||||
| Rig | `docker/verify-mes-alarm-demo.sh` steps 1–7 (§2.6). |
|
||||
|
||||
---
|
||||
|
||||
## 7. Documents to update in the same change
|
||||
|
||||
`Component-TemplateEngine.md` (alarm fields, override granularity table), `Component-SiteRuntime.md`
|
||||
(`Alarms` accessor + `MesReportableAsync`), `Component-ScriptAnalysis.md` (surface list),
|
||||
`Component-InboundAPI.md` (routing example: AlarmStatus), `Component-CentralUI.md` (dialog + override
|
||||
editor), `Component-ConfigurationDatabase.md` (migration), CLI `README.md`, `CLAUDE.md` native-alarm
|
||||
bullet (flag vs band), `2026-06-30-mes-alarm-status-api.md` §6.1 (note: superseded for computed
|
||||
alarms), deferred-work register (native source-level flag; central badge/proto).
|
||||
@@ -0,0 +1,672 @@
|
||||
# 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`.
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"planPath": "docs/plans/2026-09-11-mes-reportable-alarm-flag.md",
|
||||
"tasks": [
|
||||
{"id": 0, "subject": "Task 0: Worktree", "status": "pending"},
|
||||
{"id": 1, "subject": "Task 1: Commons — entity, override and resolved-alarm fields", "status": "pending", "blockedBy": [0]},
|
||||
{"id": 2, "subject": "Task 2: Configuration Database — migration", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 3, "subject": "Task 3: Template Engine — flattening", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 4, "subject": "Task 4: Template Engine — service write paths", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 5, "subject": "Task 5: Management commands + ManagementActor", "status": "pending", "blockedBy": [4]},
|
||||
{"id": 6, "subject": "Task 6: Site Runtime — stamp the flag on the alarm snapshot", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 7, "subject": "Task 7: Script surface — ScriptAlarm.IsMesReportable + Alarms.MesReportableAsync()", "status": "pending", "blockedBy": [6]},
|
||||
{"id": 8, "subject": "Task 8: Transport bundles", "status": "pending", "blockedBy": [1]},
|
||||
{"id": 9, "subject": "Task 9: CLI flags + README", "status": "pending", "blockedBy": [5]},
|
||||
{"id": 10, "subject": "Task 10: Central UI — template alarm dialog + Alarms table", "status": "pending", "blockedBy": [4]},
|
||||
{"id": 11, "subject": "Task 11: Central UI — instance override modal (tri-state)", "status": "pending", "blockedBy": [4]},
|
||||
{"id": 12, "subject": "Task 12: Documentation", "status": "pending", "blockedBy": [7, 9, 10, 11]},
|
||||
{"id": 13, "subject": "Task 13: Rig — rebuild + seed script", "status": "pending", "blockedBy": [2, 3, 5, 7, 8, 9, 10, 11]},
|
||||
{"id": 14, "subject": "Task 14: Rig — verify script + live gate + Playwright", "status": "pending", "blockedBy": [13]},
|
||||
{"id": 15, "subject": "Task 15: Finish — sweep, review, merge, memory", "status": "pending", "blockedBy": [12, 14]}
|
||||
],
|
||||
"lastUpdated": "2026-09-11T00:00:00Z"
|
||||
}
|
||||
Reference in New Issue
Block a user