Files
scadalink-design/docs/plans/2026-05-12-derive-on-compose-status.md
Joseph Doherty 552c9e4065 docs(templates): record phase 4-9 completion + verification TODOs
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.
2026-05-12 08:59:19 -04:00

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.md for rationale.

Where we are

Branch: feature/templates-folder-hierarchy.

Last commit on this feature: a965d4aPhase 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

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.