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.
This commit is contained in:
Joseph Doherty
2026-05-12 08:59:19 -04:00
parent a965d4a5bd
commit 552c9e4065

View File

@@ -1,26 +1,23 @@
# Derive-on-compose: implementation status + remaining plan
# Derive-on-compose: implementation status
> **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.
> **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**: `03a8c4a` — *Phase 3 complete, data
migration in place*.
**Last commit on this feature**: `a965d4a` — *Phase 9 complete,
single-parent editor context*.
**Phases 13 done**. Compose now derives; existing compositions migrate
on next `dotnet ef database update`. Everything from Phase 4 onward is
still pending.
**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` — 205 passing (+6 phase 2 tests)
- `tests/ScadaLink.TemplateEngine.Tests` — 212 passing (+13 derive-on-compose tests)
## Design decisions already made (from the brainstorm)
@@ -89,138 +86,95 @@ Files touched in `5615f3d`:
**No behavior changes**. New fields are never read or written yet.
## Remainingphases 4 through 9
## DonePhase 4+5: Flattening + lock enforcement
### Phase 4: Inherit/override resolution in flattening
Commit: `f599809`.
`src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs`:
- `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.
- 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.
## Done — Phase 6: Template tree hides derived
Tests: round-trip flattening with overridden + inherited combinations.
Commit: `f05b03f` (combined with phases 7+8).
### Phase 5: Lock semantics enforcement
`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.
In `TemplateService.UpdateAttributeAsync` and `UpdateScriptAsync`:
## Done — Phase 7+8: Derived/base TemplateEdit UI
- 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.
Commit: `f05b03f`.
Tests: lock-blocked updates fail with the expected message.
- 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.
### Phase 6: Template tree UI — hide derived
## Done — Phase 9: Single-parent editor context
`src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor`:
Commit: `a965d4a`.
- 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).
- `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.
### Phase 7: Derived TemplateEdit UI
## Still to verify
`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.
- 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
After compaction, the future session should:
A 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
1. Read this file and the design doc.
2. Run `git log --oneline -15` to confirm the branch is at `a965d4a` 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.
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).
## 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)
## Quick sanity script
```bash
git status --short # should be clean
git log --oneline -5 # top should include 03a8c4a
dotnet build src/ScadaLink.TemplateEngine src/ScadaLink.ConfigurationDatabase
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