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