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.
231 lines
10 KiB
Markdown
231 lines
10 KiB
Markdown
# 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
|
||
(`"<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.
|