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. > **For Claude resuming later:** All nine phases are implemented. This
> Read it in full. Then read the companion design doc > file is the change-record for the work, not a plan. See the companion
> `2026-05-12-derive-on-compose-design.md` for the architectural rationale. > design doc `2026-05-12-derive-on-compose-design.md` for rationale.
> Do NOT make code changes until you've also confirmed which phase the user
> wants next.
## Where we are ## Where we are
**Branch**: `feature/templates-folder-hierarchy`. **Branch**: `feature/templates-folder-hierarchy`.
**Last commit on this feature**: `03a8c4a` — *Phase 3 complete, data **Last commit on this feature**: `a965d4a` — *Phase 9 complete,
migration in place*. single-parent editor context*.
**Phases 13 done**. Compose now derives; existing compositions migrate **All nine phases done**. Live verification against SQL Server (phase-3
on next `dotnet ef database update`. Everything from Phase 4 onward is migration shape) and a UI smoke test are still recommended before merge.
still pending.
**All test suites currently green**: **All test suites currently green**:
- `tests/ScadaLink.CentralUI.Tests` — 159 passing - `tests/ScadaLink.CentralUI.Tests` — 159 passing
- `tests/ScadaLink.SiteRuntime.Tests` — 129 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) ## 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. **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 ## Done — Phase 6: Template tree hides derived
`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. 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 Commit: `f05b03f`.
`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. - 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. - `BuildParentContextsAsync` resolves the editor's `Parent.*` context
- Add a "Show derived templates" checkbox at the top. to exactly one entry for derived templates (via `OwnerCompositionId`)
- When shown, render derived templates indented under their base and to an empty list for base templates.
(`t.ParentTemplateId == base.Id && t.IsDerived`). - Multi-parent `<select>` dropdown removed from the Add Script form.
- Compositions tab on `TemplateEdit` should link each composition row to the - `_selectedParentIndex` / `OnParentContextChanged` deleted;
derived template's edit page (the existing link likely points to the base `ActiveEditorParent` collapses to `_editorParents.FirstOrDefault()`.
— needs to follow `composition.ComposedTemplateId` which after phase 3 is - The SCADA008 hint diagnostic on `Parent.*` use within base templates
the derived). 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`: - Apply the Phase-3 migration against a real SQL Server (run
`bash docker/deploy.sh` or `dotnet ef database update`) with seeded
- Top banner when `_selectedTemplate.IsDerived`: data to confirm `MigrateCompositionsToDerived` produces the right
*"Derived from `[base.Name]` — composed inside `[parent.Name]` as shape and respects the collision pre-check.
`[composition.InstanceName]`."* - Smoke-test the UI flows: add a composition, override an attribute,
- Attributes table: new column showing **Inherited** or **Overridden** badge. revert, toggle `LockedInDerived` on a base, edit a script on a
- Locked-from-base attributes (base has `LockedInDerived = true`) render derived template (single-parent context).
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 ## How to resume
After compaction, the future session should: A future session should:
1. Read this file. Read the design doc 1. Read this file and 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 `a965d4a` or
2. Run `git log --oneline -15` to confirm the branch is at `03a8c4a` or
later. later.
3. Check test status: `dotnet test` across the three suites named above. 3. Run the three test suites named above.
4. Ask the user which phase to tackle next (or proceed from phase 4 if the 4. Ask the user whether to ship or to address one of the deferred items
user has explicitly said "continue"). ("when base toggles LockedInDerived while derivatives override",
5. Each phase is its own commit. SCADA008 base-Parent hint, or the live-DB / UI smoke verifications).
## Pre-existing capability worth knowing ## Quick sanity script
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 ```bash
git status --short # should be clean git status --short # should be clean
git log --oneline -5 # top should include 03a8c4a git log --oneline -10 # top should include a965d4a
dotnet build src/ScadaLink.TemplateEngine src/ScadaLink.ConfigurationDatabase 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.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 Note: the full `dotnet build` of the solution fails with NU1608 in