Files
scadalink-design/docs/plans/2026-05-12-derive-on-compose-status.md
Joseph Doherty 8b8b85c839 docs(templates): record phase 2+3 completion in status doc
Phase 1 → 3 marked done; remaining work is phases 4-9. Sanity script now
targets the post-Phase-3 commit (03a8c4a) and notes the pre-existing
NU1608 build error in IntegrationTests / Host.Tests so future sessions
don't chase a phantom regression.
2026-05-12 08:31:20 -04:00

231 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 13 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
(`"<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.
## 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.