A verified audit of all ~90 plan documents (2026-08-01) found ~30 .tasks.json
trackers and several plan headers still reporting 'pending'/'draft' for work
fully merged to main. Sync them so future audits don't re-litigate closed work:
- Flip ~380 stale task statuses to completed across March/May/June/July
trackers (audit-log series, milestones M5-M10, playwright waves, stillpending,
LocalDb, ClusterClient->gRPC DoD rows, and more), each verified against
code/git evidence before flipping.
- Annotate obsolete-not-done rows: ClusterClient CLI transport (never built,
HTTP shipped), TreeView Areas/Instances pages (replaced by Topology),
template-tree drag-drop (dropped for M9 menu reorder), otopcua item C
(premise superseded by #17).
- Flip stale headers: aggregated-live-alarm + kpi-rollups 'Draft not executed'
-> Delivered 2026-07-10; otopcua cutover-scope SCOPING -> DECIDED;
scadabridge-rename -> Implemented; LocalDb phase1/2 status strings ->
merged 28ca04d7.
- Fix doc drift: T9/T10 'deferred' -> shipped as SMS (Teams dropped); waitfor
sandbox follow-up shipped; followups #52/#53/#54/#162/#207 resolved; purge
TODO closed by PendingDeploymentPurgeActor; live-gate pre-existing failures
#28/#29/#31 fixed; auto-down boot-alone residual superseded by self-first
seeds; supersession banners on keep-oldest SBR + ClusterClient-era designs;
requirements-traceability 'Pending' clarified as frozen plan-generation
status.
Deliberately left pending (genuinely open, tracked in the pending-work list):
opcua-tag-browser task 19 (live smoke), ipsen tasks 7-8 (vd03 verification),
selfform task 7 (vd03 overlay, user-held), live-gate observation 1
(external-system delete orphan bug), otopcua item A + maxDepth calibration.
8.5 KiB
Derive-on-compose: implementation status
2026-08-01: The "Still to verify" items below are satisfied — the Phase-3 migration ships and auto-applies, and
2026-05-18-contained-template-names-design.mdrecords it applied and browser-verified on the dev cluster. The "How to resume" section is historical.
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.mdfor rationale.
Where we are
Branch: feature/templates-folder-hierarchy.
Last commit on this feature: a965d4a — Phase 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/ZB.MOM.WW.ScadaBridge.CentralUI.Tests— 159 passingtests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests— 129 passingtests/ZB.MOM.WW.ScadaBridge.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.AddCompositionAsyncbuilds a derived template ("<parent>.<slot>"), copies base attributes/scripts withIsInherited=true, then composes the derived (not the base). SetsOwnerCompositionIdback-ref after the composition's Id is known.- Composing a derived template is rejected — only bases can be composed.
DeleteCompositionAsynccascade-deletes the slot-owned derived template (IsDerived=trueandOwnerCompositionId==compositionId).DeleteTemplateAsyncblocks 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.CanDeleteTemplateAsyncmirrors 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
TemplateCompositionwhose target isIsDerived=0, inserts a derived template, copies attributes/scripts withIsInherited=1, then repointsComposedTemplateId. - Idempotent (only touches non-derived targets), so re-runs are safe.
Down()reverses by repointing compositions toParentTemplateIdand 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/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/Template.cs- Added
IsDerived: bool - Added
OwnerCompositionId: int?(plain int — not an EF nav prop)
- Added
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateAttribute.cs- Added
IsInherited: bool - Added
LockedInDerived: bool
- Added
src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateScript.cs- Same two fields
src/ZB.MOM.WW.ScadaBridge.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.
ScadaBridgeDbContextModelSnapshot.csregenerated.
No behavior changes. New fields are never read or written yet.
Done — Phase 4+5: Flattening + lock enforcement
Commit: f599809.
FlatteningService.ResolveInheritedAttributes/ResolveInheritedScriptstreatIsInherited=truerows as placeholders that don't shadow the resolved base value. Override (IsInherited=false) wins as before.ValidateLockedInDerivedruns once per chain (main + every composed chain) and returns a flatten-time failure if a derived row overrides aLockedInDerivedbase member.TemplateService.UpdateAttributeAsync/UpdateScriptAsyncreject derived-side overrides ofLockedInDerivedbase members, and now persistIsInherited(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
LockedInDerivedthroughUpdateAttribute/UpdateScript.
- Derived: Source badge (Inherited / Override / Local), plus a
"🔒 Base-locked" badge when
- 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.
BuildParentContextsAsyncresolves the editor'sParent.*context to exactly one entry for derived templates (viaOwnerCompositionId) and to an empty list for base templates.- Multi-parent
<select>dropdown removed from the Add Script form. _selectedParentIndex/OnParentContextChangeddeleted;ActiveEditorParentcollapses 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.shordotnet ef database update) with seeded data to confirmMigrateCompositionsToDerivedproduces the right shape and respects the collision pre-check. - Smoke-test the UI flows: add a composition, override an attribute,
revert, toggle
LockedInDerivedon a base, edit a script on a derived template (single-parent context).
How to resume
A future session should:
- Read this file and the design doc.
- Run
git log --oneline -15to confirm the branch is ata965d4aor later. - Run the three test suites named above.
- 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/ZB.MOM.WW.ScadaBridge.CentralUI src/ZB.MOM.WW.ScadaBridge.TemplateEngine src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase
dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.csproj
dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj
dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.csproj
Note: the full dotnet build of the solution fails with NU1608 in
ZB.MOM.WW.ScadaBridge.IntegrationTests and ZB.MOM.WW.ScadaBridge.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.