# 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 (`"."`), 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 `.` 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 `