From 26e2cdef232cffdf52a09902e3b76a50d9129db0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 12:14:24 -0400 Subject: [PATCH] feat(m9/T26a): read-only inheritance resolve service + GetResolvedTemplateMembersCommand --- .../Management/ResolvedTemplateMembers.cs | 117 +++++++ .../Messages/Management/TemplateCommands.cs | 10 + .../ManagementActor.cs | 15 + .../TemplateInheritanceResolver.cs | 285 +++++++++++++++++ .../TemplateInheritanceResolverTests.cs | 293 ++++++++++++++++++ 5 files changed, 720 insertions(+) create mode 100644 src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs create mode 100644 src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateInheritanceResolver.cs create mode 100644 tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/TemplateInheritanceResolverTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs new file mode 100644 index 00000000..abb99456 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs @@ -0,0 +1,117 @@ +namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; + +/// +/// Read-only response for : the +/// EFFECTIVE inherited member set for a template, computed fresh from the full +/// inheritance chain (root → leaf, arbitrary depth) and annotated per member +/// with its origin and lock state — plus a staleness summary comparing the +/// template's STORED member rows against this freshly-resolved set. +/// +/// +/// +/// The precedence used to compute the effective member set MIRRORS +/// FlatteningService.ResolveInherited* exactly (derived wins; +/// IsInherited placeholders never shadow the live base value; locked +/// members flagged), so the editor preview agrees with what a deploy would +/// produce. This is the AUTHORING view only — it never mutates stored rows and +/// is not on the deploy path. +/// +/// +/// Member values surfaced here are the inheritance-resolved values BEFORE any +/// composition or instance override is applied — composition is a separate +/// concern owned by the instance flattener, and there are no instances in the +/// template-authoring context. +/// +/// +public sealed record ResolvedTemplateMembers +{ + /// The template the member set was resolved for. + public int TemplateId { get; init; } + /// The template's name. + public string TemplateName { get; init; } = string.Empty; + /// The immediate parent template id, or null when this is a root template. + public int? ParentTemplateId { get; init; } + + /// The effective resolved attributes (own + transitively inherited). + public IReadOnlyList Attributes { get; init; } = []; + /// The effective resolved alarms (own + transitively inherited). + public IReadOnlyList Alarms { get; init; } = []; + /// The effective resolved scripts (own + transitively inherited). + public IReadOnlyList Scripts { get; init; } = []; + /// The effective resolved native alarm source bindings (own + transitively inherited). + public IReadOnlyList NativeAlarmSources { get; init; } = []; + + /// + /// Staleness summary: whether the template's stored member rows differ from + /// this freshly-resolved set (e.g. a base member added after the derived + /// template was created, or a multi-level inherited member not present in + /// the stored rows), and how many inherited members differ. + /// + public ResolvedTemplateStaleness Staleness { get; init; } = new(); +} + +/// +/// One effective member in a set, +/// annotated with where its winning definition came from and its lock state. +/// +public sealed record ResolvedTemplateMemberInfo +{ + /// The member name (bare; inheritance-resolved members are never path-qualified). + public string Name { get; init; } = string.Empty; + + /// + /// True when the winning definition lives on an ANCESTOR template rather + /// than on the resolved template itself — i.e. this member is inherited. + /// + public bool IsInherited { get; init; } + + /// The id of the template that supplied the winning definition. + public int OriginTemplateId { get; init; } + /// The name of the template that supplied the winning definition. + public string OriginTemplateName { get; init; } = string.Empty; + + /// + /// True when the member is locked from override (IsLocked on the + /// winning row) — a derived template cannot change it. + /// + public bool IsLocked { get; init; } + + /// + /// True when an ANCESTOR marked this member LockedInDerived — the + /// derived template may not override it (rendered read-only with a lock in + /// the editor). Distinct from : a base may forbid + /// override (LockedInDerived) without the member itself being a + /// locked composition member. + /// + public bool IsBaseLocked { get; init; } + + /// + /// The effective value for the member, type-appropriate: attribute value, + /// script code, alarm priority/trigger summary, or native-source reference. + /// Provided so the editor preview shows the live (post-resolution) value + /// without re-walking the chain itself. Null when the member type carries + /// no single scalar value. + /// + public string? EffectiveValue { get; init; } +} + +/// +/// Staleness summary comparing a template's STORED member rows against the +/// freshly-resolved inherited member set. +/// +public sealed record ResolvedTemplateStaleness +{ + /// + /// True when the stored derived rows differ from the freshly-resolved set — + /// the base changed after the derived template was created, so the editor + /// should surface a "base changed" banner. + /// + public bool IsStale { get; init; } + + /// + /// The number of inherited members whose freshly-resolved state differs + /// from what the stored rows reflect (missing rows, added base members, + /// changed inherited values). This is the count the editor banner shows. + /// + public int DifferingMemberCount { get; init; } +} diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs index 875d3758..7493d77c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateCommands.cs @@ -7,6 +7,16 @@ public record UpdateTemplateCommand(int TemplateId, string Name, string? Descrip public record DeleteTemplateCommand(int TemplateId); public record ValidateTemplateCommand(int TemplateId); +/// +/// Read-only authoring query (M9/T26a): returns the EFFECTIVE inherited member +/// set for a template — computed fresh from the full inheritance chain +/// (arbitrary depth), annotated per member with origin + lock state — plus a +/// staleness summary comparing the template's stored rows against the resolved +/// set. Feeds the template editor's inheritance preview + base-change banner; +/// never mutates stored rows. Response: . +/// +public record GetResolvedTemplateMembersCommand(int TemplateId); + // Template member operations public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null); public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked, string? ElementDataType = null); diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs index 85f0b005..9fdb49aa 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs @@ -244,6 +244,7 @@ public class ManagementActor : ReceiveActor UpdateTemplateCommand cmd => await HandleUpdateTemplate(sp, cmd, user.Username), DeleteTemplateCommand cmd => await HandleDeleteTemplate(sp, cmd, user.Username), ValidateTemplateCommand cmd => await HandleValidateTemplate(sp, cmd), + GetResolvedTemplateMembersCommand cmd => await HandleGetResolvedTemplateMembers(sp, cmd), // Template members AddTemplateAttributeCommand cmd => await HandleAddAttribute(sp, cmd, user.Username), @@ -599,6 +600,20 @@ public class ManagementActor : ReceiveActor return validationResult; } + /// + /// Read-only authoring resolve (M9/T26a): returns the effective inherited + /// member set for a template — computed fresh from the full inheritance + /// chain — plus a staleness summary. Loads every template (children + /// eager-loaded) so the resolver can walk an arbitrary-depth chain; never + /// mutates stored rows. + /// + private static async Task HandleGetResolvedTemplateMembers(IServiceProvider sp, GetResolvedTemplateMembersCommand cmd) + { + var repo = sp.GetRequiredService(); + var allTemplates = await repo.GetAllTemplatesAsync(); + return TemplateInheritanceResolver.Resolve(cmd.TemplateId, allTemplates); + } + // ======================================================================== // Template folder handlers // ======================================================================== diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateInheritanceResolver.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateInheritanceResolver.cs new file mode 100644 index 00000000..098aa955 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateInheritanceResolver.cs @@ -0,0 +1,285 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; +using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; + +namespace ZB.MOM.WW.ScadaBridge.TemplateEngine; + +/// +/// Read-only AUTHORING resolver (M9/T26a). Given a template id and the full +/// template lookup, computes the EFFECTIVE inherited member set fresh from the +/// whole inheritance chain (root → leaf, arbitrary depth, cycle-guarded via +/// ), annotated per member +/// with origin (own / inherited-from-X) and lock state, plus a staleness +/// summary comparing the template's STORED member rows against the resolved set. +/// +/// +/// The inheritance precedence here MIRRORS +/// 's ResolveInherited* methods +/// EXACTLY so the editor preview agrees with what a deploy would produce: +/// walk base → derived (derived wins); an IsInherited placeholder row on +/// a derived template never shadows the live base value (it only contributes a +/// row when no ancestor defines the member); a locked member, once seen, is not +/// overridden by a downstream template; and an ancestor's +/// LockedInDerived flag is propagated so the editor can render the +/// member read-only. +/// +/// +/// +/// This is inheritance only — composition and instance overrides are the +/// instance flattener's concern (there are no instances in the +/// template-authoring context). It NEVER mutates stored rows and is not on the +/// deploy path. +/// +/// +public static class TemplateInheritanceResolver +{ + /// + /// Resolves the effective inherited member set for . + /// + /// The template to resolve members for. + /// The complete set of templates (for chain lookups). + /// + /// The resolved member set (attributes / alarms / scripts / native alarm + /// sources), each annotated with origin + lock state, plus the staleness + /// summary. Returns an empty set when the template id is not found. + /// + public static ResolvedTemplateMembers Resolve(int templateId, IReadOnlyList