fix(template-engine): resolve TemplateEngine-001/003/004/005, re-triage 002 — recursive composed flattening, fixed-field guard, alarm script refs, dead collision query

This commit is contained in:
Joseph Doherty
2026-05-16 19:57:28 -04:00
parent 71c0564ec0
commit 74aae53500
5 changed files with 506 additions and 130 deletions

View File

@@ -51,15 +51,12 @@ public class TemplateService
FolderId = folderId
};
// Check acyclicity (inheritance) — for new templates this is mostly a parent-exists check,
// but we validate anyway for consistency
if (parentTemplateId.HasValue)
{
var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
// The new template doesn't exist yet, so we simulate by adding it to the list
// with a temporary ID. Since it has no children yet, the only cycle would be
// if parentTemplateId somehow pointed at itself (already handled above).
}
// No collision or acyclicity check is needed here: a freshly created
// template has no members of its own, the parent (validated above to
// exist) was already collision-checked when its members were added,
// and a brand-new child cannot be an ancestor of its parent. Naming
// collisions are enforced on every member-mutating call (AddAttribute,
// AddAlarm, AddScript, AddComposition) and on rename in UpdateTemplate.
await _repository.AddTemplateAsync(template, cancellationToken);
await _auditService.LogAsync(user, "Create", "Template", "0", name, template, cancellationToken);
@@ -281,17 +278,19 @@ public class TemplateService
if (lockError != null)
return Result<TemplateAttribute>.Failure(lockError);
// Validate fixed-field granularity
// Validate fixed-field granularity. DataType and DataSourceReference are
// fixed by the defining level for every attribute — locked or not — so
// the error is always honoured (a locked attribute is already rejected
// earlier inside the helper).
var granularityError = LockEnforcer.ValidateAttributeOverride(existing, proposed);
if (granularityError != null && existing.IsLocked)
if (granularityError != null)
return Result<TemplateAttribute>.Failure(granularityError);
// Apply overridable fields
// Apply overridable fields. DataType / DataSourceReference are fixed and
// are deliberately not copied from the proposed attribute.
existing.Value = proposed.Value;
existing.Description = proposed.Description;
existing.IsLocked = proposed.IsLocked;
existing.DataType = proposed.DataType;
existing.DataSourceReference = proposed.DataSourceReference;
if (template?.IsDerived == true)
existing.IsInherited = proposed.IsInherited;
else