# Derive-on-compose template specialization

## Goal

Match Aveva System Platform's composition model: composing template
`$Sensor` into template `$Pump` no longer references `$Sensor` directly. Instead
the system creates a derived template that **inherits** from `$Sensor`, then the
composition references the derived template. The derived template lives under
the owning parent and can:

- override attribute default values
- override script bodies
- add new attributes / scripts the base doesn't have
- be prevented from overriding fields the base marks as locked

This is the user-selected approach (Option C "Always-derive") from the
brainstorming session, with all four customization scopes enabled.

## Why

- Per-composition customization is a real SCADA use case (Pump's TempSensor
  needs different alarm thresholds from Motor's TempSensor).
- Single parent always at design time: removes the multi-parent picker we just
  added.
- Industry-standard mental model for users coming from Aveva / Wonderware.

## Non-goals

- Replacing the existing `ParentTemplateId` inheritance chain — we reuse it.
- Versioning of base templates separately from derived (out of scope; can layer
  later).
- Cross-template attribute references (already covered by Children/Parent).

## Data model changes

`Template` gains:

```csharp
public bool IsDerived { get; set; }                 // hides from main tree
public int? OwnerCompositionId { get; set; }        // back-ref to composition
```

`TemplateAttribute` gains:

```csharp
public bool IsInherited { get; set; }               // value came from base
public bool LockedInDerived { get; set; }           // base marks "no override"
```

`TemplateScript` gains the same `IsInherited` / `LockedInDerived` pair.

`TemplateComposition` is unchanged in shape — `ComposedTemplateId` now points
at the **derived** template, not the base. The base is reachable via
`derived.ParentTemplateId`.

**Why a separate `IsDerived` flag rather than just "has a parent and is composed
once":** explicit marker keeps the tree-view filtering trivial and signals
intent independent of current composition state.

**Why `OwnerCompositionId` instead of inferring from `TemplateComposition`
back-pointers:** O(1) lookup for cascade-delete and forbid-direct-edit paths.

## Lifecycle

```
Compose "$Sensor" into "$Pump" as instance "TempSensor":
  1. Create new template { Name: "Pump.TempSensor",  ParentTemplateId: $Sensor.Id,
                           IsDerived: true,           Description: from $Sensor }
  2. Copy $Sensor.Attributes into the new template marked IsInherited=true
  3. Copy $Sensor.Scripts    into the new template marked IsInherited=true
  4. Create TemplateComposition { TemplateId: $Pump.Id,
                                   ComposedTemplateId: newTemplate.Id,
                                   InstanceName: "TempSensor" }
  5. Set newTemplate.OwnerCompositionId = the new composition's Id
```

Delete composition or owning parent → cascade-delete the derived template.

Rename composition InstanceName → rename the derived template (`Pump.NewName`).

Edit base attribute that is `IsInherited=true` on derivatives → the derivatives
pick up the change *if* they haven't overridden that field. Override sets
`IsInherited=false`.

## Lock semantics

Existing `IsLocked` on `TemplateAttribute` already exists with the meaning
"this attribute on this template is locked for editing." Add a second flag
`LockedInDerived` meaning "derived templates may not override the value
inherited from this attribute." These compose:

| State on base | What derived can do |
|---|---|
| neither flag set | Override value freely |
| `LockedInDerived` only | Cannot override; inherited value is final |
| `IsLocked` only | Base itself can't be edited; derived can still override |
| both | Locked everywhere |

## Flattening implications

`FlatteningService.ResolveInheritedScripts` already walks a template chain via
`ParentTemplateId`. That logic already handles "child overrides parent;
parent's `IsLocked` blocks override." We extend the same with
`LockedInDerived` for both attributes and scripts.

`ResolveComposedScripts` walks compositions → composed templates. Today the
prefix is the `InstanceName`. With derived templates the prefix is still the
`InstanceName` (the derived template's name `Pump.TempSensor` doesn't show up
in canonical paths — paths use the slot name, not the template name).

The `ResolvedScript.Scope` we landed for Phase 2 of the previous design still
applies: `SelfPath = "TempSensor"`, `ParentPath = ""`. No change.

## UI changes

### Template tree

Hide `IsDerived` templates from the main list. They're reachable via:
- the Compositions tab on the parent template (click the row → opens the
  derived template's edit page)
- a "Show derived templates" toggle on the tree page (off by default)

### TemplateEdit for a derived template

Top banner: *"Derived from `$Sensor` — composed inside `$Pump` as `TempSensor`."*

Attributes table renders three columns of state:
- **Override / Inherited** badge per row
- Locked-from-base attributes render readonly with a 🔒 icon and tooltip
  *"Locked by base — cannot override."*

Scripts table same treatment.

Adding a new attribute or script on the derived template is allowed (creates
a row with `IsInherited = false`).

Removing an inherited row reverts it to the base value (the row goes back to
inherited state). Removing an own-added row deletes it.

### TemplateEdit for a base template

Two extra columns on attribute / script tables:
- 🔒 toggle for `LockedInDerived` — "Lock this against per-slot override"

### Compositions tab

Today: lists composition rows with InstanceName + ComposedTemplate name.
After: each row links to *its derived template* (not the base). InstanceName
becomes the visible label.

Renaming a composition renames the derived template too.

### Composition picker (when adding a composition)

Today: pick a template + provide an instance name.
After: pick a **base** template + provide an instance name. The system creates
the derived template behind the scenes.

The picker filters out `IsDerived` templates — you can only compose bases.

## Editor metadata implications

The multi-parent picker becomes mostly irrelevant:

- **Derived template**: always single parent (the composition it's owned by).
  `Parent.*` resolves to that one. No picker.
- **Base template**: still has no direct parent (it's a library entry).
  `Parent.*` autocompletion is suppressed. Scripts on bases that use
  `Parent.*` get a warning *"Parent access on a base template is ambiguous —
  override this script in the derived template instead."*

`TemplateEdit.BuildParentContextsAsync` simplifies to: "if derived, return the
single owning parent; else return null."

`GetTemplatesComposingAsync` repository method still useful (e.g., for "find
all uses of this base"), but the editor metadata path doesn't need it.

## Migration

One-shot for existing data:

```sql
-- pseudo-SQL describing intent
FOREACH composition IN TemplateComposition:
  derived := INSERT INTO Templates (
    Name = parent.Name + "." + composition.InstanceName,
    ParentTemplateId = composition.ComposedTemplateId,
    IsDerived = true,
    OwnerCompositionId = composition.Id
  )
  -- Copy attributes from base, mark IsInherited=true
  INSERT INTO TemplateAttributes
    SELECT @derived.Id, Name, Value, DataType, true, ... FROM base.Attributes
  -- Same for scripts
  UPDATE TemplateComposition SET ComposedTemplateId = derived.Id WHERE Id = composition.Id
```

EF Core migration in `ScadaLink.ConfigurationDatabase/Migrations/`.

Rollback strategy: the migration is one-way for new derivations, but old
composition data can be reconstructed from `IsDerived` templates' `ParentTemplateId`.

## Phased rollout

Each phase is independently shippable and reviewable.

1. **Schema + entities.** Add the new fields. Empty migration. EF mappings.
   No behavior changes. Existing data unaffected.

2. **Composition flow change.** Modify `TemplateService.AddCompositionAsync`
   to derive on compose for *new* compositions. Existing data still has direct
   compositions and continues to work. Two modes coexist during the cutover.

3. **Migration.** EF Core migration script that walks existing compositions
   and creates the derived templates retroactively. After this all
   compositions are derived.

4. **Inherit/override resolution.** Update `FlatteningService` to merge
   inherited and overridden fields. Tests for the override semantics.

5. **Lock semantics.** Wire `LockedInDerived` through `TemplateService`
   update paths. Tests.

6. **Template tree UI.** Hide derived templates from the main listing;
   surface them through the parent's Compositions tab.

7. **Derived TemplateEdit UI.** Banner, inherited/override badges,
   readonly-when-locked, override/revert actions.

8. **Base TemplateEdit UI.** Add the LockedInDerived toggle column.

9. **Editor metadata simplification.** Replace the multi-parent picker with
   the single-parent resolver. Base templates suppress `Parent.*` assistance
   and warn on use.

## Out of scope (for now)

- Versioning of base templates with explicit "update derived templates to
  base v2" workflow.
- Reverse-flow: editing a derived value and asking "promote to base."
- Multiple inheritance levels for derivation (e.g., `$Sensor → $Sensor.Pump →
  $Sensor.Pump.HighTemp`) — the data model supports it via
  `ParentTemplateId`, but the UX hasn't been designed.
- Cross-tenant template libraries.

## Decisions

- **Naming**: dot-separated (`Pump.TempSensor`). Matches the canonical-path
  format used in flattening. Visible in audit logs / error messages.
- **Delete base with derivatives**: block the delete and list the derivatives.
  User must remove or repoint them first.
- **Migration of existing data**: EF Core migration on next startup
  auto-derives every existing composition. After deploy all compositions are
  derived; no mixed-mode code paths.
- **Tree UX**: derived templates hidden by default. "Show derived templates"
  toggle on the tree page reveals them indented under their base. Always
  reachable from the parent's Compositions tab.

## Confirmed semantics

- **Re-composing the same base on the same parent in two slots** (e.g. Pump
  composes Sensor twice as `IntakeSensor` and `OutletSensor`) produces two
  derived templates: `Pump.IntakeSensor` and `Pump.OutletSensor`, both
  inheriting from `Sensor`.

- **Inheritance updates flow downward**: if a base attribute changes value
  later and the derivative has `IsInherited = true` for that attribute, the
  derived value updates. Once overridden (`IsInherited = false`), changes to
  the base no longer affect that field.

- **Subsequent `LockedInDerived` after overrides exist**: surface as a
  validation error at deploy time; do not force-revert silently.
