All nine derive-on-compose phases are now implemented. The status doc captures what shipped per phase, what was deferred (LockedInDerived override warning toast, SCADA008 base-Parent hint), and the live-DB / UI smoke checks worth running before merge.
8.0 KiB
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.mdfor 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 passingtests/ScadaLink.SiteRuntime.Tests— 129 passingtests/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.AddCompositionAsyncbuilds a derived template ("<parent>.<slot>"), copies base attributes/scripts withIsInherited=true, then composes the derived (not the base). SetsOwnerCompositionIdback-ref after the composition's Id is known.- Composing a derived template is rejected — only bases can be composed.
DeleteCompositionAsynccascade-deletes the slot-owned derived template (IsDerived=trueandOwnerCompositionId==compositionId).DeleteTemplateAsyncblocks 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.CanDeleteTemplateAsyncmirrors 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
TemplateCompositionwhose target isIsDerived=0, inserts a derived template, copies attributes/scripts withIsInherited=1, then repointsComposedTemplateId. - Idempotent (only touches non-derived targets), so re-runs are safe.
Down()reverses by repointing compositions toParentTemplateIdand 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)
- Added
src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs- Added
IsInherited: bool - Added
LockedInDerived: bool
- Added
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.csregenerated.
No behavior changes. New fields are never read or written yet.
Done — Phase 4+5: Flattening + lock enforcement
Commit: f599809.
FlatteningService.ResolveInheritedAttributes/ResolveInheritedScriptstreatIsInherited=truerows as placeholders that don't shadow the resolved base value. Override (IsInherited=false) wins as before.ValidateLockedInDerivedruns once per chain (main + every composed chain) and returns a flatten-time failure if a derived row overrides aLockedInDerivedbase member.TemplateService.UpdateAttributeAsync/UpdateScriptAsyncreject derived-side overrides ofLockedInDerivedbase members, and now persistIsInherited(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
LockedInDerivedthroughUpdateAttribute/UpdateScript.
- Derived: Source badge (Inherited / Override / Local), plus a
"🔒 Base-locked" badge when
- 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.
BuildParentContextsAsyncresolves the editor'sParent.*context to exactly one entry for derived templates (viaOwnerCompositionId) and to an empty list for base templates.- Multi-parent
<select>dropdown removed from the Add Script form. _selectedParentIndex/OnParentContextChangeddeleted;ActiveEditorParentcollapses 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.shordotnet ef database update) with seeded data to confirmMigrateCompositionsToDerivedproduces the right shape and respects the collision pre-check. - Smoke-test the UI flows: add a composition, override an attribute,
revert, toggle
LockedInDerivedon a base, edit a script on a derived template (single-parent context).
How to resume
A future session should:
- Read this file and the design doc.
- Run
git log --oneline -15to confirm the branch is ata965d4aor later. - Run the three test suites named above.
- 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
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.