# Derive-on-compose: implementation status + remaining plan

> **For Claude resuming after compaction:** This is the single starting point.
> Read it in full. Then read the companion design doc
> `2026-05-12-derive-on-compose-design.md` for the architectural rationale.
> Do NOT make code changes until you've also confirmed which phase the user
> wants next.

## Where we are

**Branch**: `feature/templates-folder-hierarchy`.

**Last commit on this feature**: `03a8c4a` — *Phase 3 complete, data
migration in place*.

**Phases 1–3 done**. Compose now derives; existing compositions migrate
on next `dotnet ef database update`. Everything from Phase 4 onward is
still pending.

**All test suites currently green**:
- `tests/ScadaLink.CentralUI.Tests` — 159 passing
- `tests/ScadaLink.SiteRuntime.Tests` — 129 passing
- `tests/ScadaLink.TemplateEngine.Tests` — 205 passing (+6 phase 2 tests)

## Design decisions already made (from the brainstorm)

User picked the **full Aveva model** with all four customization scopes:

- **Naming**: dot-separated → `Pump.TempSensor`
- **Delete base with derivatives**: block with a list of the dependents
- **Migration of existing compositions**: auto-migrate all on the EF Core
  migration step in Phase 3
- **Tree UX**: derived templates hidden by default; toggle to reveal
- **Customization scope**: override attribute values, override script bodies,
  add new attrs/scripts per slot, lock fields against override

## Done — Phase 1: Additive schema

Commits: `6854843` (design doc) + `a968cef` (decisions recorded) + `5615f3d`.

## Done — Phase 2: Compose flow change

Commit: `fa86750`.

- `TemplateService.AddCompositionAsync` builds a derived template
  (`"<parent>.<slot>"`), copies base attributes/scripts with
  `IsInherited=true`, then composes the derived (not the base). Sets
  `OwnerCompositionId` back-ref after the composition's Id is known.
- Composing a derived template is rejected — only bases can be composed.
- `DeleteCompositionAsync` cascade-deletes the slot-owned derived
  template (`IsDerived=true` and `OwnerCompositionId==compositionId`).
- `DeleteTemplateAsync` blocks direct deletion of derived templates and
  splits the inheritor check into regular children vs. derivatives — the
  derivative branch labels each by `'OwnerName' (as 'SlotName')`.
- `TemplateDeletionService.CanDeleteTemplateAsync` mirrors the same
  derivative-aware checks.

## Done — Phase 3: Migration of existing compositions

Commit: `03a8c4a`. Migration `20260512122746_MigrateCompositionsToDerived`.

- Pre-flight aborts with a descriptive error if any
  `<parent>.<slot>` derived name would collide.
- Cursor-walks every `TemplateComposition` whose target is `IsDerived=0`,
  inserts a derived template, copies attributes/scripts with
  `IsInherited=1`, then repoints `ComposedTemplateId`.
- Idempotent (only touches non-derived targets), so re-runs are safe.
- `Down()` reverses by repointing compositions to `ParentTemplateId` and
  dropping the derived templates.

The migration was NOT verified against a live SQL Server in this
session — run `bash docker/deploy.sh` (or `dotnet ef database update`)
once with seeded test data to confirm shape.

Files touched in `5615f3d`:

- `src/ScadaLink.Commons/Entities/Templates/Template.cs`
  - Added `IsDerived: bool`
  - Added `OwnerCompositionId: int?` (plain int — not an EF nav prop)
- `src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs`
  - Added `IsInherited: bool`
  - Added `LockedInDerived: bool`
- `src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs`
  - Same two fields
- `src/ScadaLink.ConfigurationDatabase/Migrations/20260512121446_AddDerivedTemplateFields.cs`
  - EF Core migration. Six new columns, all NOT NULL DEFAULT 0 (or nullable
    int). No data transform — existing rows get defaults.
- `ScadaLinkDbContextModelSnapshot.cs` regenerated.

**No behavior changes**. New fields are never read or written yet.

## Remaining — phases 4 through 9

### Phase 4: Inherit/override resolution in flattening

`src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs`:

- The service currently walks `Template.ParentTemplateId` chains in
  `ResolveInheritedScripts`. That same chain now naturally includes the
  base→derived link. No special handling needed for the chain itself.
- BUT when a derived template's attribute is `IsInherited = true`, the
  effective value comes from the base (resolved through the chain). If
  `IsInherited = false`, the derived's own value is the override and wins.
- Treat `LockedInDerived` as an enforcement check: if a derived row has
  `IsInherited = false` (override) AND the base row has `LockedInDerived =
  true`, emit a flattening validation error.

Tests: round-trip flattening with overridden + inherited combinations.

### Phase 5: Lock semantics enforcement

In `TemplateService.UpdateAttributeAsync` and `UpdateScriptAsync`:

- If the target template has `IsDerived = true`, look up the base via
  `ParentTemplateId`.
- If the same-named attribute on the base has `LockedInDerived = true`,
  reject the update with a clear error: *"This attribute is locked by the
  base template '$Sensor' and cannot be overridden."*
- If the update is being applied on a base template AND there are
  derivatives that have `IsInherited = false` for this field (overrides), the
  caller may want to know — surface a warning result. Out of scope for now;
  flag as TODO.

Tests: lock-blocked updates fail with the expected message.

### Phase 6: Template tree UI — hide derived

`src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor`:

- Filter `_templates` to exclude `t.IsDerived` by default.
- Add a "Show derived templates" checkbox at the top.
- When shown, render derived templates indented under their base
  (`t.ParentTemplateId == base.Id && t.IsDerived`).
- Compositions tab on `TemplateEdit` should link each composition row to the
  derived template's edit page (the existing link likely points to the base
  — needs to follow `composition.ComposedTemplateId` which after phase 3 is
  the derived).

### Phase 7: Derived TemplateEdit UI

`src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor`:

- Top banner when `_selectedTemplate.IsDerived`:
  *"Derived from `[base.Name]` — composed inside `[parent.Name]` as
  `[composition.InstanceName]`."*
- Attributes table: new column showing **Inherited** or **Overridden** badge.
- Locked-from-base attributes (base has `LockedInDerived = true`) render
  readonly with a 🔒 icon and tooltip *"Locked by base — cannot override."*
- Editing an inherited row flips it to override (`IsInherited = false`).
- "Revert to base" button per row when `IsInherited = false` — clears the
  override; row reverts to the base value (`IsInherited = true`, value
  cleared).
- Adding a new attribute/script: creates a row with `IsInherited = false`
  (it's not from the base).
- Removing an inherited row should be blocked (the row exists because it's
  on the base; can't remove it on the derived). Removing an own-added row
  deletes normally.

### Phase 8: Base TemplateEdit UI — lock toggle

Same `TemplateEdit.razor`, when editing a base template:

- New column on Attributes table: a 🔒 toggle representing `LockedInDerived`.
- Same on Scripts table.
- Tooltip on the toggle: *"Lock this against per-slot override in derived
  templates."*

When a base template is open and the user toggles `LockedInDerived = true`
while derived templates exist with overrides, surface a warning toast:
*"N derived templates currently override this. Existing overrides won't be
silently reverted but deploy validation will flag them."*

### Phase 9: Editor metadata simplification

Now that derived templates have a single owner, `BuildParentContextsAsync`
in `TemplateEdit.razor` simplifies:

- **If the open template `IsDerived`**: parent = the single template that
  owns the `OwnerCompositionId`. Always one — no picker.
- **If the open template is a base** (`!IsDerived`): suppress `Parent.*`
  assistance entirely. Add a SCADA008 hint diagnostic on `Parent.*` use:
  *"Parent access on a base template is ambiguous — override this script in
  a derived template instead."*

Remove the multi-parent `select` dropdown that was added in commit `0139c9c`
(or keep it but guard with a check that should never fire after phase 3).

`GetTemplatesComposingAsync` (added in `0139c9c`) is still useful elsewhere —
keep it.

## How to resume

After compaction, the future session should:

1. Read this file. Read the design doc
   `2026-05-12-derive-on-compose-design.md` for context.
2. Run `git log --oneline -15` to confirm the branch is at `03a8c4a` or
   later.
3. Check test status: `dotnet test` across the three suites named above.
4. Ask the user which phase to tackle next (or proceed from phase 4 if the
   user has explicitly said "continue").
5. Each phase is its own commit.

## Pre-existing capability worth knowing

The script-scope editor work (commits `3ed05f0` → `0139c9c`) is already in
place. Scripts on derived templates will automatically benefit from the
single-parent context simplification in phase 9. The runtime accessors
(`Attributes` / `Children` / `Parent` / `Scope`) defined there continue to
work unchanged — the canonical-name paths they resolve are stable across
the derivation change.

The multi-parent picker introduced in `0139c9c` becomes mostly dormant
after phase 3 (no template should be composed by multiple parents anymore
through the new flow). Plan to remove it in phase 9.

## Quick sanity script (run before phase 4)

```bash
git status --short         # should be clean
git log --oneline -5       # top should include 03a8c4a
dotnet build src/ScadaLink.TemplateEngine src/ScadaLink.ConfigurationDatabase
dotnet test tests/ScadaLink.TemplateEngine.Tests/ScadaLink.TemplateEngine.Tests.csproj
```

Note: the full `dotnet build` of the solution fails with NU1608 in
`ScadaLink.IntegrationTests` and `ScadaLink.Host.Tests` due to a
pre-existing `Microsoft.CodeAnalysis.Common` 4.13 vs 5.0 mismatch — not
related to the derive-on-compose work. Build the three suites listed in
"Where we are" individually.
