Files
ScadaBridge/docs/plans/2026-05-12-derive-on-compose-status.md
T
Joseph Doherty d1ade5653b docs(plans): bookkeeping sync — reconcile stale trackers with merged code
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.
2026-08-01 08:53:56 -04:00

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.md records 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.md for rationale.

Where we are

Branch: feature/templates-folder-hierarchy.

Last commit on this feature: a965d4aPhase 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 passing
  • tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests — 129 passing
  • tests/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.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/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/Template.cs
    • Added IsDerived: bool
    • Added OwnerCompositionId: int? (plain int — not an EF nav prop)
  • src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateAttribute.cs
    • Added IsInherited: bool
    • Added LockedInDerived: bool
  • 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.cs regenerated.

No behavior changes. New fields are never read or written yet.

Done — Phase 4+5: Flattening + lock enforcement

Commit: f599809.

  • 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.

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 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.

Done — Phase 9: Single-parent editor context

Commit: a965d4a.

  • 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.

Still to verify

  • 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

A future session should:

  1. Read this file and the design doc.
  2. Run git log --oneline -15 to confirm the branch is at a965d4a or later.
  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).

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.