feat(template-engine): resolve TemplateEngine-002 — per-slot alarm override for derived templates

Adds IsInherited/LockedInDerived to the TemplateAlarm entity (mirroring the
attribute/script override model), an EF migration, base-alarm copy-on-derive,
inherited-alarm flattening skip, and LockedInDerived override-rejection validation.
This commit is contained in:
Joseph Doherty
2026-05-16 20:12:24 -04:00
parent bc548e1447
commit 305b42ea6d
9 changed files with 1700 additions and 21 deletions

View File

@@ -8,7 +8,7 @@
| Last reviewed | 2026-05-16 |
| Reviewer | claude-agent |
| Commit reviewed | `9c60592` |
| Open findings | 10 |
| Open findings | 9 |
## Summary
@@ -91,7 +91,7 @@ Regression tests: `Flatten_ThreeLevelComposition_AttributesAlarmsScriptsAllResol
|--|--|
| Severity | High |
| Category | Correctness & logic bugs |
| Status | Open |
| Status | Resolved |
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:799` |
**Description**
@@ -116,22 +116,25 @@ already do.
**Resolution**
_Unresolved (re-triaged 2026-05-16)._ Partially mis-stated and out of the
current fix scope. Correction to the description: composed/inherited alarms
are **not** dropped from the flattened deployment output — `FlatteningService`
resolves alarms from the entire inheritance chain (`ResolveInheritedAlarms`
walks `templateChain`, which includes the base of a derived template), so an
instance of a derived template still receives the base template's alarms. The
real, valid gap is narrower: there is no per-slot **alarm override**
mechanism. The fix genuinely requires adding `IsInherited` / `LockedInDerived`
fields to the `TemplateAlarm` entity, which lives in `ScadaLink.Commons`
(a different module). Adding an alarm copy loop to `BuildDerivedTemplate`
without those fields would be actively harmful: copied alarm rows on the
derived template would shadow the live base alarm with stale data during
flattening (`ResolveInheritedAlarms` has no `IsInherited` skip for alarms,
unlike attributes/scripts). Resolving this safely is a cross-module change
(`Commons` + `TemplateEngine`) and must be scheduled as a coordinated edit;
left **Open** pending that.
Resolved 2026-05-16 (commit `<pending>`): implemented the per-slot alarm
override mechanism as a coordinated `Commons` + `ConfigurationDatabase` +
`TemplateEngine` change, mirroring the existing attribute/script override
design. Added `IsInherited` / `LockedInDerived` to the `TemplateAlarm` POCO
(`ScadaLink.Commons`) and an EF migration `AddDerivedAlarmFields` adding two
`bit NOT NULL DEFAULT 0` columns to `TemplateAlarms`. `BuildDerivedTemplate`
now copies base alarms as `IsInherited = true` placeholder rows.
`FlatteningService.ResolveInheritedAlarms` skips `IsInherited` placeholder
rows so they no longer shadow the live base alarm, and `ValidateLockedInDerived`
now rejects a derived override of a `LockedInDerived` base alarm.
`UpdateAlarmAsync` honours the base `LockedInDerived` lock and persists
`IsInherited` / `LockedInDerived`, exactly as `UpdateAttributeAsync` /
`UpdateScriptAsync` do. Regression tests:
`Flatten_InheritedAlarmOnDerived_BaseValueWins`,
`Flatten_OverriddenAlarmOnDerived_DerivedValueWins`,
`Flatten_LockedInDerivedAlarmOverride_Fails`,
`AddComposition_CopiesAlarmsAsInherited`,
`UpdateAlarm_LockedInDerivedBase_RejectsOnDerived`,
`UpdateAlarm_DerivedOverride_PersistsIsInheritedFalse`.
### TemplateEngine-003 — `UpdateAttributeAsync` lets a non-locked attribute change its fixed DataType / DataSourceReference