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