feat(template-engine): contained names for composition-derived templates

A composition-derived template now stores its contained name — the
composition slot's InstanceName (e.g. "Pump"), unique only within its
owner — instead of the dotted global path ("Motor Controller.Pump").
The qualified hierarchical name is computed on read.

- TemplateNaming.QualifiedName: walks the OwnerCompositionId chain to
  build the dotted path; null-safe, cycle-guarded.
- TemplateConfiguration: the unique index on Template.Name becomes
  filtered (WHERE IsDerived = 0) — base templates stay globally unique;
  derived templates' uniqueness is the existing (TemplateId,
  InstanceName) index on TemplateComposition.
- Migration ContainedDerivedTemplateNames: rewrites derived rows to the
  contained name; Down rebuilds the dotted names via a recursive CTE
  before restoring the global index.
- TemplateService: composition create/rename store the contained name;
  the dotted-name collision pre-checks and cascade-rename are removed
  (a slot rename no longer touches nested derived templates).
- TemplateEdit: title shows the contained name; the qualified path is a
  breadcrumb subtitle; "composed inside" uses the owner's qualified name.

TDD: 4 TemplateNaming tests + updated composition tests. TemplateEngine
293, ConfigurationDatabase 114, CentralUI 316 green. Migration applied to
the dev cluster and verified in the browser (Motor Controller.Pump now
titled "Pump"; nested Motor Controller.Pump.TempSensor resolves).

Design: docs/plans/2026-05-18-contained-template-names-design.md
This commit is contained in:
Joseph Doherty
2026-05-18 17:50:30 -04:00
parent 2d4b287ab2
commit 06462a0100
11 changed files with 1662 additions and 139 deletions

View File

@@ -81,6 +81,18 @@ When a template composes a feature module, members from that module are addresse
- The composing template's own members (not from a module) have no prefix — they are top-level names.
- Naming collision detection operates on canonical names, so two modules can define the same member name as long as their module instance names differ.
### Derived template naming
A composition slot is materialized as its own *derived* template. A derived
template stores a **contained name** — the composition slot's instance name
(e.g. `Pump`), unique only within its owner. The **qualified name**
(`Motor Controller.Pump`, or `Motor Controller.Pump.TempSensor` when nested) is
*computed* on read by walking the owner-composition chain — it is not stored.
Only base (user-authored) templates are globally unique by name; a derived
template's uniqueness is the slot-name uniqueness within its owner. The Central
UI shows the contained name as the template title and the qualified path as a
breadcrumb.
## Override Granularity
Override and lock rules apply per entity type at the following granularity: