From f5998094860e3e540ceab2881f0809129f3953b0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 12 May 2026 08:50:49 -0400 Subject: [PATCH] =?UTF-8?q?feat(templates):=20phase=204+5=20=E2=80=94=20in?= =?UTF-8?q?herit/override=20resolution=20+=20lock=20enforcement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FlatteningService now treats IsInherited rows as placeholders: when a derived template carries an inherited attribute or script, the live base value resolves through the ParentTemplateId chain instead of the (possibly stale) copy. An IsInherited=false row is a real override and wins as before. ValidateLockedInDerived runs once per chain (main + composed) and returns a flatten-time failure if a derived template overrides a base row that the base marked LockedInDerived. TemplateService.Update{Attribute,Script}Async reject mid-flight when a derived target tries to override a LockedInDerived base member, and now persist IsInherited/LockedInDerived from the proposed payload so the UI can flip override state or set base-locks via the same endpoints. --- .../Flattening/FlatteningService.cs | 71 +++++++++++- .../TemplateService.cs | 28 +++++ .../Flattening/FlatteningServiceTests.cs | 108 ++++++++++++++++++ .../TemplateServiceTests.cs | 64 +++++++++++ 4 files changed, 265 insertions(+), 6 deletions(-) diff --git a/src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs b/src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs index e018283..edc32ed 100644 --- a/src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs +++ b/src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs @@ -54,6 +54,17 @@ public class FlatteningService try { + // Step 0: Validate LockedInDerived isn't violated by any chain. + var lockError = ValidateLockedInDerived(templateChain); + if (lockError != null) + return Result.Failure(lockError); + foreach (var composedChain in composedTemplateChains.Values) + { + lockError = ValidateLockedInDerived(composedChain); + if (lockError != null) + return Result.Failure(lockError); + } + // Step 1: Resolve attributes from inheritance chain (most-derived-first wins for same name) var attributes = ResolveInheritedAttributes(templateChain); @@ -124,7 +135,10 @@ public class FlatteningService { var result = new Dictionary(StringComparer.Ordinal); - // Walk from base (last) to most-derived (first) so derived values win + // Walk from base (last) to most-derived (first) so derived values win. + // IsInherited rows on a derived template are placeholders that should + // not shadow the live base value; they only contribute a row when the + // base lacks one. for (int i = templateChain.Count - 1; i >= 0; i--) { var template = templateChain[i]; @@ -132,9 +146,13 @@ public class FlatteningService foreach (var attr in template.Attributes) { - // If a parent defined this attribute as locked, derived cannot change the value - if (result.TryGetValue(attr.Name, out var existing) && existing.IsLocked) - continue; + if (result.TryGetValue(attr.Name, out var existing)) + { + if (existing.IsLocked) + continue; + if (attr.IsInherited) + continue; + } result[attr.Name] = new ResolvedAttribute { @@ -152,6 +170,42 @@ public class FlatteningService return result; } + /// + /// Reports any LockedInDerived violations across the chain — i.e., a base + /// attribute/script marked LockedInDerived that a downstream derived + /// template overrides (IsInherited=false). Returns null on success or an + /// error message describing the first offending entries. + /// + private static string? ValidateLockedInDerived(IReadOnlyList