Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ResolvedTemplateMembers.cs
T
Joseph Doherty 2b5949320c feat(templateengine+centralui): resolve follow-ups #1/#2 — inherited-member propagation & resync
Derived templates store IsInherited placeholder rows mirroring inherited
members, but a base member added/changed/removed AFTER a child was derived
never reached the child — leaving the editor's editable tabs incomplete (#1)
and stored rows drifted from the resolved set (#2).

Fix (one order-independent reconcile, two entry points):
- Auto-propagation: every attribute/alarm/script add/update/delete now
  reconciles the template's derived subtree (TemplateService.ReconcileDescendantsAsync),
  hooked into all member-mutating paths incl. native-alarm-source CRUD in the
  ManagementActor.
- Resync: ResyncInheritedMembersAsync repairs a template + its subtree on
  demand — materialize missing placeholders, re-sync drifted ones, remove
  orphans, across attributes/alarms/scripts/native sources. Exposed as
  management ResyncInheritedMembersCommand (Designer-gated, audited) → CLI
  `template resync-members` → a Resync button on the editor's staleness banner.

Reconcile drives off TemplateInheritanceResolver (same precedence + HiLo merge
as deploy), only ever touches IsInherited placeholders (never an authored
override), and matches the staleness comparison keys so the banner clears.
BuildDerivedTemplate now also materializes native-source placeholders at
compose time (previously omitted → any inherited native source was perpetually
stale).

Tests: +8 TemplateServiceTests (materialize / drift-update / orphan-remove /
override-untouched / base-cascade / multi-type / direct-propagate / end-to-end
add) + 1 ManagementService test fix (native-source add resolves TemplateService).
Affected suites green: TemplateEngine 446, ManagementService 230, CentralUI 866,
CLI 333, Transport 127, ConfigurationDatabase 307; full solution builds 0/0.

Docs: Component-TemplateEngine.md "Inherited-Member Propagation & Resync";
CLI README `template resync-members`; known-issues tracker #1/#2 resolved.
2026-06-24 15:51:26 -04:00

145 lines
6.9 KiB
C#

namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
/// <summary>
/// Read-only response for <see cref="GetResolvedTemplateMembersCommand"/>: 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.
/// </summary>
/// <remarks>
/// <para>
/// The precedence used to compute the effective member set MIRRORS
/// <c>FlatteningService.ResolveInherited*</c> exactly (derived wins;
/// <c>IsInherited</c> 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public sealed record ResolvedTemplateMembers
{
/// <summary>The template the member set was resolved for.</summary>
public int TemplateId { get; init; }
/// <summary>The template's name.</summary>
public string TemplateName { get; init; } = string.Empty;
/// <summary>The immediate parent template id, or null when this is a root template.</summary>
public int? ParentTemplateId { get; init; }
/// <summary>The effective resolved attributes (own + transitively inherited).</summary>
public IReadOnlyList<ResolvedTemplateMemberInfo> Attributes { get; init; } = [];
/// <summary>The effective resolved alarms (own + transitively inherited).</summary>
public IReadOnlyList<ResolvedTemplateMemberInfo> Alarms { get; init; } = [];
/// <summary>The effective resolved scripts (own + transitively inherited).</summary>
public IReadOnlyList<ResolvedTemplateMemberInfo> Scripts { get; init; } = [];
/// <summary>The effective resolved native alarm source bindings (own + transitively inherited).</summary>
public IReadOnlyList<ResolvedTemplateMemberInfo> NativeAlarmSources { get; init; } = [];
/// <summary>
/// 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.
/// </summary>
public ResolvedTemplateStaleness Staleness { get; init; } = new();
}
/// <summary>
/// One effective member in a <see cref="ResolvedTemplateMembers"/> set,
/// annotated with where its winning definition came from and its lock state.
/// </summary>
public sealed record ResolvedTemplateMemberInfo
{
/// <summary>The member name (bare; inheritance-resolved members are never path-qualified).</summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// True when the winning definition lives on an ANCESTOR template rather
/// than on the resolved template itself — i.e. this member is inherited.
/// </summary>
public bool IsInherited { get; init; }
/// <summary>The id of the template that supplied the winning definition.</summary>
public int OriginTemplateId { get; init; }
/// <summary>The name of the template that supplied the winning definition.</summary>
public string OriginTemplateName { get; init; } = string.Empty;
/// <summary>
/// True when the member is locked from override (<c>IsLocked</c> on the
/// winning row) — a derived template cannot change it.
/// </summary>
public bool IsLocked { get; init; }
/// <summary>
/// True when an ANCESTOR marked this member <c>LockedInDerived</c> — the
/// derived template may not override it (rendered read-only with a lock in
/// the editor). Distinct from <see cref="IsLocked"/>: a base may forbid
/// override (<c>LockedInDerived</c>) without the member itself being a
/// locked composition member.
/// </summary>
public bool IsBaseLocked { get; init; }
/// <summary>
/// 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.
/// </summary>
public string? EffectiveValue { get; init; }
/// <summary>
/// For ALARM members only: the effective (inheritance-resolved) trigger
/// configuration JSON — for HiLo alarms this is the per-setpoint MERGED
/// config (a derived template overriding only <c>hi</c> inherits the base
/// <c>lo</c>), identical to what the flattener produces on deploy, so the
/// editor can render the effective HiLo thresholds. Null for non-alarm
/// members and for alarms with no trigger configuration.
/// </summary>
public string? EffectiveTriggerConfiguration { get; init; }
}
/// <summary>
/// Result of a <see cref="ResyncInheritedMembersCommand"/>: how many derived
/// templates were brought into sync and how many inherited member rows were
/// added / updated / removed across the reconciled subtree.
/// </summary>
public sealed record ResyncInheritedMembersResult
{
/// <summary>Number of templates in the subtree that had at least one inherited-row change.</summary>
public int TemplatesChanged { get; init; }
/// <summary>Inherited placeholder rows materialized (members missing from the stored set).</summary>
public int MembersAdded { get; init; }
/// <summary>Inherited placeholder rows re-synced to the live effective value.</summary>
public int MembersUpdated { get; init; }
/// <summary>Orphaned inherited placeholder rows removed (the base no longer defines them).</summary>
public int MembersRemoved { get; init; }
}
/// <summary>
/// Staleness summary comparing a template's STORED member rows against the
/// freshly-resolved inherited member set.
/// </summary>
public sealed record ResolvedTemplateStaleness
{
/// <summary>
/// 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.
/// </summary>
public bool IsStale { get; init; }
/// <summary>
/// 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.
/// </summary>
public int DifferingMemberCount { get; init; }
}