# Derive-on-compose: implementation status

> **For Claude resuming later:** All nine phases are implemented. This
> file is the change-record for the work, not a plan. See the companion
> design doc `2026-05-12-derive-on-compose-design.md` for rationale.

## Where we are

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

**Last commit on this feature**: `a965d4a` — *Phase 9 complete,
single-parent editor context*.

**All nine phases done**. Live verification against SQL Server (phase-3
migration shape) and a UI smoke test are still recommended before merge.

**All test suites currently green**:
- `tests/ScadaLink.CentralUI.Tests` — 159 passing
- `tests/ScadaLink.SiteRuntime.Tests` — 129 passing
- `tests/ScadaLink.TemplateEngine.Tests` — 212 passing (+13 derive-on-compose 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.

## Done — Phase 4+5: Flattening + lock enforcement

Commit: `f599809`.

- `FlatteningService.ResolveInheritedAttributes` / `ResolveInheritedScripts`
  treat `IsInherited=true` rows as placeholders that don't shadow the
  resolved base value. Override (`IsInherited=false`) wins as before.
- `ValidateLockedInDerived` runs once per chain (main + every composed
  chain) and returns a flatten-time failure if a derived row overrides
  a `LockedInDerived` base member.
- `TemplateService.UpdateAttributeAsync` / `UpdateScriptAsync` reject
  derived-side overrides of `LockedInDerived` base members, and now
  persist `IsInherited` (on derived) / `LockedInDerived` (on base) from
  the proposed payload so the UI can drive override state.

## Done — Phase 6: Template tree hides derived

Commit: `f05b03f` (combined with phases 7+8).

`Templates.razor` filters `t.IsDerived` from the main tree. A "Show
derived" form-switch in the page header flips the filter — derived
templates surface in the flat list so users can still reach them.

## Done — Phase 7+8: Derived/base TemplateEdit UI

Commit: `f05b03f`.

- Derived banner: links to base + slot owner / instance name from
  `OwnerCompositionId`.
- Attributes / Scripts tables grew a context-aware column:
  * Derived: Source badge (Inherited / Override / Local), plus a
    "🔒 Base-locked" badge when `LockedInDerived`.
  * Base: a form-switch that flips `LockedInDerived` through
    `UpdateAttribute` / `UpdateScript`.
- Effective Value / Code resolves from the base when the derived row
  carries an inherited (potentially stale) copy — matches the runtime
  flatten behavior so the UI doesn't lie.
- Override and Revert-to-base actions live on the row kebab. Delete is
  hidden on inherited rows (the base owns those).
- "When a base toggles LockedInDerived while derivatives override the
  field, warn via toast" is NOT implemented — kept out of scope; flatten
  validation already surfaces it at deploy time.

## Done — Phase 9: Single-parent editor context

Commit: `a965d4a`.

- `BuildParentContextsAsync` resolves the editor's `Parent.*` context
  to exactly one entry for derived templates (via `OwnerCompositionId`)
  and to an empty list for base templates.
- Multi-parent `<select>` dropdown removed from the Add Script form.
- `_selectedParentIndex` / `OnParentContextChanged` deleted;
  `ActiveEditorParent` collapses to `_editorParents.FirstOrDefault()`.
- The SCADA008 hint diagnostic on `Parent.*` use within base templates
  was NOT added in this pass — the analyzer simply emits no completions
  when the parent context is empty. Add it later if users want a
  positive nudge.

## Still to verify

- Apply the Phase-3 migration against a real SQL Server (run
  `bash docker/deploy.sh` or `dotnet ef database update`) with seeded
  data to confirm `MigrateCompositionsToDerived` produces the right
  shape and respects the collision pre-check.
- Smoke-test the UI flows: add a composition, override an attribute,
  revert, toggle `LockedInDerived` on a base, edit a script on a
  derived template (single-parent context).

## How to resume

A future session should:

1. Read this file and the design doc.
2. Run `git log --oneline -15` to confirm the branch is at `a965d4a` or
   later.
3. Run the three test suites named above.
4. Ask the user whether to ship or to address one of the deferred items
   ("when base toggles LockedInDerived while derivatives override",
   SCADA008 base-Parent hint, or the live-DB / UI smoke verifications).

## Quick sanity script

```bash
git status --short         # should be clean
git log --oneline -10      # top should include a965d4a
dotnet build src/ScadaLink.CentralUI src/ScadaLink.TemplateEngine src/ScadaLink.ConfigurationDatabase
dotnet test tests/ScadaLink.TemplateEngine.Tests/ScadaLink.TemplateEngine.Tests.csproj
dotnet test tests/ScadaLink.CentralUI.Tests/ScadaLink.CentralUI.Tests.csproj
dotnet test tests/ScadaLink.SiteRuntime.Tests/ScadaLink.SiteRuntime.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.
