fix(template-engine): resolve TemplateEngine-006..010 — code-region-aware API/brace scanning, composed-alarm override validation, N+1 fix, doc correction

This commit is contained in:
Joseph Doherty
2026-05-16 21:44:11 -04:00
parent 5672502d83
commit 804697f873
11 changed files with 832 additions and 160 deletions

View File

@@ -3,6 +3,7 @@ using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.TemplateEngine;
namespace ScadaLink.TemplateEngine.Services;
@@ -13,7 +14,11 @@ namespace ScadaLink.TemplateEngine.Services;
/// - Override non-locked attribute values
/// - Cannot add or remove attributes (only override existing ones)
/// - Per-attribute connection binding (bulk assignment support)
/// - Enabled/disabled state with optimistic concurrency
/// - Enabled/disabled state. Concurrent edits are last-write-wins — there is no
/// version token or conflict detection on instance state, matching the design
/// decision (Component-TemplateEngine.md: "Concurrent editing uses
/// last-write-wins — no pessimistic locking or conflict detection"). Optimistic
/// concurrency in the system applies to deployment status records, not here.
/// - Audit logging
/// </summary>
public class InstanceService
@@ -170,10 +175,11 @@ public class InstanceService
}
/// <summary>
/// Sets a per-instance alarm override. The alarm must exist on the
/// template and must not be locked. For HiLo alarms, the override JSON
/// merges into the inherited TriggerConfiguration setpoint-by-setpoint;
/// for binary trigger types, it replaces the whole config.
/// Sets a per-instance alarm override. The alarm must exist in the
/// instance's effective alarm set (direct, inherited, or composed) and
/// must not be locked. For HiLo alarms, the override JSON merges into the
/// inherited TriggerConfiguration setpoint-by-setpoint; for binary trigger
/// types, it replaces the whole config.
/// </summary>
public async Task<Result<InstanceAlarmOverride>> SetAlarmOverrideAsync(
int instanceId,
@@ -187,17 +193,25 @@ public class InstanceService
if (instance == null)
return Result<InstanceAlarmOverride>.Failure($"Instance with ID {instanceId} not found.");
// Verify alarm exists in the template and is not locked. Only direct
// template alarms are checked here — composed-member overrides go
// through but are silently ignored at runtime if the name doesn't
// match (same behavior as attribute overrides).
var templateAlarms = await _repository.GetAlarmsByTemplateIdAsync(instance.TemplateId, cancellationToken);
var templateAlarm = templateAlarms.FirstOrDefault(a => a.Name == alarmCanonicalName);
if (templateAlarm != null && templateAlarm.IsLocked)
{
// Verify the alarm exists in the instance's effective alarm set and is
// not locked. The effective set is resolved via TemplateResolver so that
// composed (path-qualified) and inherited alarms are found — a lookup
// against the template's direct alarms alone would miss them, silently
// accepting an override for a non-existent name or bypassing the lock
// rule for a composed alarm. Mirrors SetAttributeOverrideAsync.
var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
var resolvedAlarm = TemplateResolver
.ResolveAllMembers(instance.TemplateId, allTemplates)
.FirstOrDefault(m => m.MemberType == "Alarm" && m.CanonicalName == alarmCanonicalName);
if (resolvedAlarm == null)
return Result<InstanceAlarmOverride>.Failure(
$"Alarm '{alarmCanonicalName}' does not exist in template {instance.TemplateId}. " +
"Cannot override an unknown alarm.");
if (resolvedAlarm.IsLocked)
return Result<InstanceAlarmOverride>.Failure(
$"Alarm '{alarmCanonicalName}' is locked and cannot be overridden.");
}
var existingOverride = await _repository.GetAlarmOverrideAsync(
instanceId, alarmCanonicalName, cancellationToken);