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.
This commit is contained in:
Joseph Doherty
2026-06-24 15:51:26 -04:00
parent b3f6833b36
commit 2b5949320c
11 changed files with 873 additions and 19 deletions
@@ -158,6 +158,15 @@ The resolved result carries, per member:
The resolver is consumed only by the Central UI `TemplateEdit` page. It is not part of the flattening pipeline and is not called during deployment.
## Inherited-Member Propagation & Resync
A derived template stores `IsInherited` **placeholder rows** mirroring every member it inherits (attributes, alarms, scripts, native alarm sources). These placeholders are what the editor's editable member tabs render and what the staleness summary above compares against. They are materialized when the derived template is created (compose / inherit), and kept in sync by two mechanisms so the stored rows never drift incomplete:
- **Auto-propagation.** Whenever a member is **added, updated, or removed** on a template, the change is propagated to that template's entire derived subtree: a missing inherited placeholder is materialized, a drifted one is re-synced to the live effective value, and an orphaned one (its base member removed) is deleted. This runs automatically as part of the member-mutating commands, so children stay complete going forward.
- **Resync (operator repair).** `ResyncInheritedMembersAsync` (CLI `template resync-members`, and a **Resync** button on the editor's "base changed" banner) reconciles a template **and its subtree** on demand — repairing templates that drifted before auto-propagation existed (e.g. base members added after a child was derived). Resyncing a base repairs every derivation; resyncing a leaf repairs just it.
Both delegate to one **order-independent reconcile** that compares a template's stored inherited rows against the inheritance resolver's effective set (the same precedence + HiLo merge the editor preview and deploy use) and only ever touches `IsInherited` placeholder rows — never an authored override (`IsInherited == false`). Because the resolver ignores placeholder rows when picking winners, reconciling one template never changes what another resolves, so the operation needs no particular ordering. The effective value mirrored into each placeholder matches the staleness comparison key per member type, so after a reconcile the "base changed" banner clears. Reconcile is best-effort housekeeping for the *stored authoring rows*: a deploy always re-resolves the chain fresh regardless, so a not-yet-resynced template still deploys correctly.
## Diff Calculation
The Template Engine can compare: