# 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`. Already pushed. **Last commit on this feature**: `5615f3d` — *Phase 1 complete, additive schema only*. **All test suites currently green**: - `tests/ScadaLink.CentralUI.Tests` — 159 passing - `tests/ScadaLink.SiteRuntime.Tests` — 129 passing - `tests/ScadaLink.TemplateEngine.Tests` — 199 passing ## 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`. 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 2 through 9 Each phase ships independently. The cumulative behavior change fully lands at phase 3. ### Phase 2: Compose flow change `TemplateService.AddCompositionAsync` currently creates a `TemplateComposition` that references a base template directly. Change it so NEW compositions: 1. Create a new `Template` entity: - `Name = $"{parentTemplate.Name}.{instanceName}"` - `ParentTemplateId = composedTemplateId` (the base) - `IsDerived = true` - `FolderId = null` (or inherit from parent? — pick null; derived templates aren't visible in the tree) 2. Copy the base's `Attributes` into the new template marked `IsInherited = true`. Same for `Scripts` and `Alarms`. 3. Create the `TemplateComposition` with `ComposedTemplateId = newTemplate.Id` (the derived, not the base). 4. Set the derived template's `OwnerCompositionId = composition.Id` (back-ref). Files to touch: - `src/ScadaLink.TemplateEngine/TemplateService.cs` — extend `AddCompositionAsync` (currently around line 290+). - `src/ScadaLink.TemplateEngine/TemplateService.cs` — also extend `DeleteCompositionAsync` to cascade-delete the derived template. - `src/ScadaLink.TemplateEngine/TemplateService.cs` — extend `DeleteTemplateAsync` to block when the template is composed (already happens) AND when derivatives exist; reword the error. - Validation: derived template names must remain unique (the base existing uniqueness index on `Template.Name` covers this — but if `Pump.TempSensor` collides with a user-created template of that name, the add fails. Add a guard with a clear error message.) Tests to add (`tests/ScadaLink.TemplateEngine.Tests/`): - AddCompositionAsync creates a derived template. - The derived template inherits attributes / scripts with `IsInherited = true`. - DeleteCompositionAsync cascades to remove the derived template. - Deleting a base with derivatives is blocked with a message that lists them. ### Phase 3: Migration of existing compositions EF Core data migration that runs on next startup. Two paths: **Option A (simpler) — write the migration as a custom SQL/code step** In `src/ScadaLink.ConfigurationDatabase/Migrations/`, generate a new migration called e.g. `MigrateCompositionsToDerived`. In its `Up` method, use raw `migrationBuilder.Sql(...)` or a delegate to: ``` FOREACH composition IN TemplateComposition WHERE ComposedTemplate.IsDerived = false: derived := INSERT INTO Templates ( Name = parent.Name || '.' || composition.InstanceName, ParentTemplateId = composition.ComposedTemplateId, IsDerived = true, OwnerCompositionId = composition.Id) INSERT INTO TemplateAttributes (Name, Value, DataType, IsLocked, Description, DataSourceReference, IsInherited=true, LockedInDerived=false, TemplateId=derived.Id) FROM TemplateAttributes WHERE TemplateId = composition.ComposedTemplateId same for TemplateScripts and TemplateAlarms UPDATE TemplateComposition SET ComposedTemplateId = derived.Id WHERE Id = composition.Id ``` Run as raw SQL to keep the migration deterministic across providers (SQL Server vs SQLite if used in dev). **Option B — service-layer one-time migration** Add a startup hook that runs once (controlled by a flag column) and uses `TemplateService` directly. Cleaner for complex logic but more code. **Pick Option A** unless the logic doesn't fit comfortably in SQL. Validation post-migration: every `TemplateComposition.ComposedTemplateId` should now reference a template with `IsDerived = true`. Tests: integration test that seeds pre-migration data, runs the migration, asserts the new shape. ### 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 `5615f3d` 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 2 if the user has explicitly said "continue"). 5. Each phase is its own commit. Prefer phase 2 + phase 3 together because phase 2 alone leaves new and existing compositions in different shapes until phase 3's migration runs. ## 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 2) ```bash git status --short # should be clean git log --oneline -5 # top should include 5615f3d dotnet build # 0 errors, 0 warnings dotnet test tests/ScadaLink.TemplateEngine.Tests/ScadaLink.TemplateEngine.Tests.csproj ```