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
@@ -180,13 +180,14 @@ public class FlatteningService
/// <summary>
/// Reports any LockedInDerived violations across the chain — i.e., a base
/// attribute/script marked LockedInDerived that a downstream derived
/// attribute/alarm/script marked LockedInDerived that a downstream derived
/// template overrides (IsInherited=false). Returns null on success or an
/// error message describing the first offending entries.
/// </summary>
private static string? ValidateLockedInDerived(IReadOnlyList<Template> templateChain)
{
var attrLocks = new Dictionary<string, Template>(StringComparer.Ordinal);
var alarmLocks = new Dictionary<string, Template>(StringComparer.Ordinal);
var scriptLocks = new Dictionary<string, Template>(StringComparer.Ordinal);
var errors = new List<string>();
@@ -202,6 +203,14 @@ public class FlatteningService
errors.Add($"Attribute '{attr.Name}' is LockedInDerived by base template '{lockingTemplate.Name}' and cannot be overridden by '{template.Name}'.");
}
foreach (var alarm in template.Alarms)
{
if (alarm.LockedInDerived)
alarmLocks[alarm.Name] = template;
else if (!alarm.IsInherited && alarmLocks.TryGetValue(alarm.Name, out var lockingTemplate) && lockingTemplate.Id != template.Id)
errors.Add($"Alarm '{alarm.Name}' is LockedInDerived by base template '{lockingTemplate.Name}' and cannot be overridden by '{template.Name}'.");
}
foreach (var script in template.Scripts)
{
if (script.LockedInDerived)
@@ -385,8 +394,16 @@ public class FlatteningService
foreach (var alarm in template.Alarms)
{
if (result.TryGetValue(alarm.Name, out var existing) && existing.IsLocked)
continue;
if (result.TryGetValue(alarm.Name, out var existing))
{
if (existing.IsLocked)
continue;
// IsInherited rows on a derived template are placeholders
// that must not shadow the live base alarm; they only
// contribute a row when the base lacks one.
if (alarm.IsInherited)
continue;
}
// HiLo per-setpoint override: derived templates can supply a
// partial TriggerConfiguration (e.g., just `hi`) and have the