Files
ScadaBridge/docs/plans/2026-06-18-m9-templates-authoring.md
T
Joseph Doherty 939aea159b docs(m9): implementation plan + task persistence (17 tasks, 5 waves)
Bite-sized per-task plan for T22-T26, T28, T30-T32 + CLI cached-call retry/discard.
ManagementActor.cs/ValidationService.cs/shared-razor serialization points enumerated;
blockedBy deps + Parallelizable-with sets; integration trace + EF model-drift + shared
fixture re-run checklist per integration-catches-cross-cutting-gaps.
2026-06-18 10:08:57 -04:00

33 KiB
Raw Blame History

M9 — Templates & Authoring Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task.

Goal: Deliver the in-scope authoring backlog — searchable/reorderable template tree, move + live-status for data connections, multi-level inheritance authoring + staleness banner, opt-in strict trigger analysis, schema-driven value entry ($ref library + nested forms + Monaco hover), and CLI cached-call Retry/Discard.

Architecture: Mostly Central UI (Blazor Server) layered on existing seams, a few new ManagementActor commands + handlers, one new EF entity (SharedSchema) + migration, and a read-only template-inheritance resolve service. Every cross-cluster command is traced end-to-end at integration. No new NuGet packages (System.Text.Json only).

Tech Stack: .NET 10, C#, Akka.NET (ManagementActor), EF Core (ConfigurationDatabase), Blazor Server + Bootstrap (CentralUI), xUnit + bUnit + NSubstitute (tests), System.CommandLine (CLI).

Design doc: docs/plans/2026-06-18-m9-templates-authoring-design.md (decisions D1D7). Branch: worktree-m9-templates-authoring off origin/main @ 72aec3b4.


Execution rules (read before dispatching)

  • Worktree: all work happens in this worktree. Implementer subagents do NOT create their own worktrees.
  • Commits: pathspec form only — git commit -m "msg" -- <explicit paths> (-m BEFORE --). New files: git add <explicit path> (targeted; never git add -A/-a/.). Retry once on index.lock.
  • Concurrency: ≤23 concurrent committers. After each wave, verify every wave commit is on HEAD (git log --oneline); recover any orphan via cherry-pick.
  • Shared-file serialization points (NEVER dispatch two of these concurrently):
    • ManagementService/ManagementActor.cs + Commons/Messages/Management/ManagementCommandRegistry.csT23a, T24a, T32c, T26a (the Parallelizable with lists already exclude each other — honor it).
    • TemplateEngine/Validation/ValidationService.csT28a, T32b.
    • Components/Pages/Design/Templates.razor → T22, T23b (T23b is blocked by both).
    • Components/Pages/Design/DataConnections.razor → T25, T24b (T24b blocked by both).
    • Components/Pages/Design/TemplateEdit.razor → T28b, T26b (different waves; do not run together).
    • Components/Shared/ParameterValueForm.razor / value-entry surface → T30, T31 (T31 blocked by T30).
  • Builds/tests: targeted per task — build only the touched project(s), run the filtered test class. Full-solution build + docker rebuild + Playwright + live smoke happen ONLY in the integration task (M9-INT).
  • Do not trust filtered/--no-build green from a subagent; the integration task runs an unfiltered build of every touched project.

Wave / dependency overview

Wave Tasks (parallel within wave unless shared-file)
1 T22 ‖ CLI ‖ T28a → T28b
2 T23a → T23b ‖ T25
3 (T23a✓ then) T24a ‖ T32a → T32b
4 T32c ‖ T30 ‖ T31 ‖ T26a → T26b ; T24b
5 INT

blockedBy (logical deps): T28b←T28a · T23b←T22,T23a · T24b←T24a,T25 · T32b←T32a · T32c←T32a,T32b · T30←T32b · T31←T32b,T30 · T26b←T26a · INT←all. ManagementActor tasks (T23a,T24a,T32c,T26a) are serialized by Parallelizable with omission, not blockedBy.


Wave 1

Classification: small Estimated implement time: ~3 min Parallelizable with: CLI, T28a, T32a (NOT T23b — same file)

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor
  • Test: tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ (bUnit — add a Templates search test; mirror an existing Templates/DataConnections render test)

Context: TemplateFolderTree.razor already implements recursive substring filtering + ancestor auto-expand via its Filter parameter (ApplyFilter/CopyMatching, invoked in OnParametersSet). Templates.razor uses TemplateFolderTree but never sets Filter and has no search input. DataConnections.razor is the reference for a search box. This is UI wiring only — no service/entity/command change.

Step 1: Write the failing bUnit test — render Templates with a folder/template set, type into the search input, assert the tree shows only matching nodes (and ancestors), and that clearing restores the full tree. Reference an existing CentralUI.Tests fixture for service-substitute setup (the page injects template/folder query services — register NSubstitute fakes returning a small tree).

Step 2: Run it, expect FAIL (no search input / Filter not bound). dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~Templates"

Step 3: Implement — add a _searchText field + a search <input> (@bind="_searchText" @bind:event="oninput") above the tree, with a clear (✕) affordance; pass Filter="_searchText" to <TemplateFolderTree>. Match the styling of the DataConnections.razor search box.

Step 4: Run test, expect PASS.

Step 5: Commitgit commit -m "feat(m9/T22): template tree search box (wire TemplateFolderTree.Filter)" -- src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor tests/...

Acceptance: search filters the template tree with auto-expanded ancestors; clearing restores full tree + prior expansion; no backend change.


Task CLI: cached-call Retry/Discard command group

Classification: small Estimated implement time: ~4 min Parallelizable with: T22, T28a, T28b, T23a, T25, T32a (CLI-isolated)

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CachedCallCommands.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.CLI/Program.cs (register the new command group)
  • Modify: src/ZB.MOM.WW.ScadaBridge.CLI/README.md, docs/requirements/Component-CLI.md
  • Test: tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/ (mirror NotificationCommands/SiteCommands command-construction tests)

Context: Backend relay already exists and is Deployer-gated: RetryParkedMessageCommand / DiscardParkedMessageCommand in Commons/Messages/Management/RemoteQueryCommands.cs, dispatched at ManagementActor.cs:220,380. The CLI sends commands via CommandHelpers.ExecuteCommandAsyncManagementHttpClient.SendCommandAsync, resolving the command name via ManagementCommandRegistry.GetCommandName. First verify RetryParkedMessageCommand/DiscardParkedMessageCommand are present in ManagementCommandRegistry.cs — if not, that registration is part of this task (and flag it for the M9-INT trace).

Step 1: Write the failing test — construct cached-call retry --site-id S1 --tracked-operation-id <guid> and assert it maps to RetryParkedMessageCommand(siteIdentifier, messageId); same for discardDiscardParkedMessageCommand. Model on the existing NotificationCommands test.

Step 2: Run, expect FAIL (command group absent). dotnet test tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/ZB.MOM.WW.ScadaBridge.CLI.Tests.csproj --filter "FullyQualifiedName~CachedCall"

Step 3: ImplementCachedCallCommands.Build() returning a cached-call command with retry + discard subcommands (options --site-id, --tracked-operation-id), each calling CommandHelpers.ExecuteCommandAsync(...). Register in Program.cs. Verify/add registry mappings. Document in both CLI docs.

Step 4: Run test, expect PASS.

Step 5: Commit (pathspec, including the new file via git add).

Acceptance: cached-call retry|discard resolves + sends the existing Deployer-gated commands; registry maps both names; docs updated.


Task T28a: Strict expression-trigger analysis kind — backend

Classification: small Estimated implement time: ~5 min Parallelizable with: T22, CLI, T32a (NOT T32b — ValidationService shared)

Files:

  • Modify: trigger config carrier — confirm whether the kind rides the existing TriggerConfiguration JSON (preferred — additive, no migration) on Commons/Entities/Templates/TemplateAlarm.cs / TemplateScript.cs, or needs a dedicated field. Default: carry it in the trigger-config JSON to avoid a migration.
  • Modify: src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ValidationService.cs (CheckExpressionTrigger, ~line 263401)
  • Test: tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/

Context (D7): Expression triggers ALREADY get a real Roslyn compile + forbidden-API + undefined-attribute analysis that blocks deploy (M2/M3). Blank-expression and similar are currently advisory (warnings). First confirm the exact current advisory set by reading CheckExpressionTrigger. T28a adds an AnalysisKind (default Advisory = today's behavior exactly; Strict = promote those advisory findings to deploy-blocking errors). Keep it minimal — do NOT re-implement analysis.

Step 1: Write failing tests — (a) Advisory kind on a trigger with a blank/ambiguous expression → validation passes with a warning (current behavior preserved); (b) Strict kind on the same → validation FAILS (deploy-blocking error). Use the existing ValidationService test fixtures for a template + expression trigger.

Step 2: Run, expect FAIL (no kind; both currently warn). dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.csproj --filter "FullyQualifiedName~Trigger"

Step 3: Implement — read AnalysisKind from the trigger config (default Advisory); in CheckExpressionTrigger, when Strict, add the currently-advisory findings to the error list instead of the warning list. Additive; Advisory path byte-for-byte unchanged.

Step 4: Run, expect PASS (+ existing ValidationService tests still green).

Step 5: Commit.

Acceptance: Advisory preserves current pass/warn behavior; Strict escalates advisory findings to deploy-blocking; no migration; analysis logic unchanged.


Task T28b: Strict trigger-kind — UI selector + CLI flag

Classification: small Estimated implement time: ~4 min Parallelizable with: T25, T23a (NOT T26b — TemplateEdit shared; different wave anyway) blockedBy: T28a

Files:

  • Modify: the alarm/script trigger editor UI — locate the trigger-config editor in/under src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor (or its trigger sub-component)
  • Modify: src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs (add --trigger-kind/--strict to the relevant alarm/script trigger sub-command)
  • Test: CentralUI.Tests (bUnit selector) + CLI.Tests (flag → command field)

Step 1: Write failing tests — selector defaults to Advisory and round-trips Strict into the trigger config; CLI --strict sets the kind on the create/update command.

Step 2: Run, expect FAIL.

Step 3: Implement — a small Advisory|Strict <select> in the trigger editor bound into the trigger config; CLI flag wired into the existing trigger command.

Step 4: Run, expect PASS.

Step 5: Commit.

Acceptance: authors can set the kind in the UI and CLI; default Advisory; value persists through the trigger config consumed by T28a.


Wave 2

Task T23a: Folder sibling reorder — service + command + handler

Classification: standard Estimated implement time: ~5 min Parallelizable with: T25 (NOT T24a/T32c/T26a — ManagementActor.cs shared)

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateFolderService.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TemplateFolderCommands.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs
  • Test: tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Services/TemplateFolderServiceTests.cs

Context: TemplateFolder.SortOrder (TemplateFolder.cs:12) already exists; TemplateFolderService has Create/Rename/Move/Delete (Designer-gated, audited) but no reorder. Match the existing command/handler/registry pattern exactly (see the sibling folder commands).

Step 1: Write failing testsReorderFolderAsync(folderId, direction, user) swaps SortOrder with the adjacent sibling under the same parent; no-op at the top/bottom; reorder of 3+ siblings produces the expected order; audit row written.

Step 2: Run, expect FAIL. dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.csproj --filter "FullyQualifiedName~TemplateFolderService"

Step 3: ImplementReorderFolderAsync (swap SortOrder among same-parent siblings, ordered by SortOrder then Name); ReorderTemplateFolderCommand(FolderId, Direction) (Up/Down); ManagementActor handler (Designer-gated, mirror Move handler); register in ManagementCommandRegistry. Ensure folder-tree builders order siblings by SortOrder.

Step 4: Run, expect PASS.

Step 5: Commit (include ManagementActor + registry in the pathspec).

Acceptance: reorder swaps adjacent siblings, ends are no-ops, ordering persists + is honored by tree loads, audited; command registered.


Task T23b: Folder reorder + root context menu — UI

Classification: standard Estimated implement time: ~4 min Parallelizable with: T25 (NOT T22 — same file) blockedBy: T22, T23a

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor
  • Test: CentralUI.Tests (bUnit menu render/actions)

Context: Folder context menu already has New Folder/Template, Rename, Move…, Delete (Templates.razor:217). Add Move-up/Move-down (calling the new ReorderTemplateFolderCommand via the folder service used by the page) and a root-level context menu (right-click root/empty → New Folder at root, New Template at root).

Step 1: Write failing test — folder context menu exposes Move up/Move down and invokes the reorder command; root context menu offers New Folder/Template at root.

Step 24: FAIL → implement → PASS.

Step 5: Commit.

Acceptance: menu-based sibling reorder works end-to-end through the page; root context menu present; no drag-drop (D4).


Task T25: Connection live-status indicators

Classification: standard Estimated implement time: ~5 min Parallelizable with: T23a, T23b, T28b (NOT T24b — DataConnections.razor shared)

Files:

  • Create/Modify: a CentralUI health-query method exposing connectionId → ConnectionHealth for a site (add to an existing CentralUI service, e.g. alongside Services/AuditLogQueryService.cs, or inject ICentralHealthAggregator directly) + register in src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/DataConnections.razor
  • Test: tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/and update existing DataConnections fixtures to register the new injected service (shared-component injection-regression guard).

Context (D6): Health already flows DCL → SiteHealthReport.DataConnectionStatuses (name→ConnectionHealth) → ICentralHealthAggregator.GetSiteState. Health.razor renders the badges (GetConnectionHealthBadge). Surface the SAME data on the design page: resolve names→ids via the site repo, render a per-node badge, refresh on a ~10s poll timer (mirror Health page).

Step 1: Write failing tests — (a) the query maps DataConnectionStatuses to {connectionId: health}, tolerating missing reports (empty map) and unknown names; (b) bUnit: a connection node renders the health badge for its status.

Step 2: Run, expect FAIL. dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~DataConnection"

Step 3: Implement — the query method + DI registration; in DataConnections.razor, load the health map, render a badge per connection node, add a poll timer. Reuse GetConnectionHealthBadge-style classes.

Step 4: Run, expect PASS (+ existing DataConnections fixtures green with the new service substitute).

Step 5: Commit.

Acceptance: design page shows live per-connection health (badge), refreshed on poll; missing-report tolerated; existing fixtures updated + green.


Wave 3

Task T24a: Move data connection between sites — command + handler + guards

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T32a (NOT T23a/T32c/T26a — ManagementActor.cs shared)

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/DataConnectionCommands.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs
  • Modify (if a binding lookup is needed): src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISiteRepository.cs + src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs
  • Test: tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/

Context (D5): No move today; UpdateDataConnectionCommand doesn't change SiteId. Persist via existing ISiteRepository.UpdateDataConnectionAsync. Guards, server-side, all enforced before the write:

  1. Target site exists.
  2. No connection name collision at the target site.
  3. Reject if any InstanceConnectionBinding references the connection (instances are site-scoped) — error message names the blocking instance(s).
  4. Validate name-based references (TemplateNativeAlarmSource.ConnectionName, InstanceNativeAlarmSourceOverride.ConnectionNameOverride) won't collide/orphan at the target. Emit an audit row on success.

Step 1: Write failing tests — success path moves SiteId; blocked-by-binding returns an error naming the instance; name-collision-at-target returns an error; audit row asserted. Use the ManagementActor test harness with substituted repos.

Step 2: Run, expect FAIL. dotnet test tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests.csproj --filter "FullyQualifiedName~MoveDataConnection"

Step 3: ImplementMoveDataConnectionCommand(DataConnectionId, TargetSiteId) (Designer-gated); HandleMoveDataConnection with the four guards + audit; register in ManagementCommandRegistry; add a binding-count/query repo method if one doesn't exist.

Step 4: Run, expect PASS.

Step 5: Commit (include ManagementActor + registry).

Acceptance: guarded move succeeds only when safe; blockers return clear errors; audited; command registered. (Routing trace re-verified at M9-INT.)


Task T24b: Move connection — UI dialog + action

Classification: standard Estimated implement time: ~4 min Parallelizable with: T32b (NOT T25 — DataConnections.razor shared) blockedBy: T24a, T25

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/MoveDataConnectionDialog.razor
  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/DataConnections.razor
  • Test: CentralUI.Tests (bUnit)

Context: Mirror MoveFolderDialog.razor (picker + error surface). Add a "Move to Site…" action (context menu or row action) opening the dialog (target-site picker), which calls MoveDataConnectionCommand and surfaces guard errors inline.

Step 15: failing bUnit (dialog opens, site picker, calls command, shows guard error) → implement → PASS → commit.

Acceptance: operator can move a connection via the dialog; guard errors shown inline; tree refreshes on success.


Task T32a: SharedSchema entity + EF config + migration + repository

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T24a, T23a (disjoint — Commons/ConfigDB only)

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Schemas/SharedSchema.cs
  • Create: src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISharedSchemaRepository.cs
  • Create: src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SharedSchemaConfiguration.cs
  • Create: src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SharedSchemaRepository.cs
  • Modify: the ConfigDB DbContext (DbSet<SharedSchema>) + DI registration of the repo
  • Create: EF migration in src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Migrations/ (idempotent; dotnet ef migrations add AddSharedSchema)
  • Test: ConfigurationDatabase tests (round-trip) — use the existing ConfigDB test harness

Context (D2): SharedSchema { int Id; string Name (unique); string? Scope; string SchemaJson; ... }. Idempotent migration (the M2-pre PendingModelChangesWarning lesson) — after adding, run dotnet build and confirm no pending-model-changes warning. No new package.

Step 1: Write failing test — create + read-by-name a SharedSchema; unique-name enforced.

Step 2: Run, expect FAIL.

Step 3: Implement — entity + config (unique index on Name) + repo (AddAsync/GetByNameAsync/ListAsync/UpdateAsync/DeleteAsync) + DbSet + migration + DI.

Step 4: Run, expect PASS; confirm dotnet build has no model-drift warning.

Step 5: Commit (add new files explicitly).

Acceptance: entity persists + round-trips, unique name, repo wired, migration idempotent, no model drift.


Task T32b: JSON Schema $ref resolver + deploy-time validation

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T24b (NOT T28a — ValidationService shared) blockedBy: T32a

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.Commons/Types/InboundApi/InboundApiSchema.cs (Parse/ParseSchema)
  • Modify: src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ValidationService.cs (deploy-time $ref-target existence check)
  • Test: tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ (resolver) + TemplateEngine.Tests (deploy-block on dangling ref)

Context (D2): Add a $ref resolver to the existing recursive parser. Resolve {"$ref":"lib:Name"} against a library-lookup seam (a Func<string,string?>/interface supplied by the caller — the library entries come from ISharedSchemaRepository from T32a). Depth/cycle-guarded; System.Text.Json only; no new package. Deploy-time validation blocks on a dangling $ref.

Step 1: Write failing tests — (a) a schema with a valid $ref resolves to the referenced shape; (b) a dangling $ref → deploy-blocking validation error; (c) cycle/depth guard returns a controlled error (no stack overflow).

Step 2: Run, expect FAIL. dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/ZB.MOM.WW.ScadaBridge.Commons.Tests.csproj --filter "FullyQualifiedName~InboundApiSchema"

Step 3: Implement$ref resolution in the parser via the lookup seam; wire deploy-time target-existence validation into ValidationService.

Step 4: Run, expect PASS (+ existing Commons.Tests/ValidationService tests green).

Step 5: Commit.

Acceptance: $ref resolves through the library seam; dangling ref blocks deploy; depth/cycle-safe; no new package.


Wave 4

Task T32c: Schema library — CRUD commands + handlers + Central UI page

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T30, T31, T26b (NOT T26a/T24a/T23a — ManagementActor.cs shared) blockedBy: T32a, T32b

Files:

  • Create: src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SchemaLibraryCommands.cs
  • Modify: src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs + ManagementCommandRegistry.cs
  • Create: a Central UI schema-library page under src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/ (reuse SchemaBuilder.razor) + nav entry
  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs (query service if needed)
  • Test: ManagementService.Tests (CRUD handlers) + CentralUI.Tests (page bUnit)

Step 1: Write failing tests — create/list/update/delete SharedSchema via commands (Designer-gated); page lists + edits schemas via SchemaBuilder.

Step 2: Run, expect FAIL.

Step 3: Implement — CRUD commands + handlers (call ISharedSchemaRepository) + registry; the page (list + SchemaBuilder editor) + nav entry under Design.

Step 4: Run, expect PASS.

Step 5: Commit (include ManagementActor + registry).

Acceptance: schema-library CRUD round-trips through ManagementActor; UI authors library schemas; commands registered.


Task T30: Schema-driven nested value-entry forms

Classification: standard Estimated implement time: ~5 min Parallelizable with: T32c, T26a, T26b (NOT T31 — value-entry surface shared) blockedBy: T32b

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/ParameterValueForm.razor
  • Test: CentralUI.Tests (bUnit)

Context: ParameterValueForm.razor:52 renders scalars but falls back to a JSON textarea for Object/List. Extend it to recursively render object fields + list items as typed inputs, driven by the parsed InboundApiSchema (including $ref-resolved schemas from T32b). Per-field validation via InboundApiSchema.Validate; collect to canonical JSON.

Step 1: Write failing tests — a nested object schema renders per-field inputs (not a raw textarea); a list renders add/remove typed item rows; per-field validation error surfaces; round-trips to canonical JSON; a $ref-bearing schema renders the resolved shape.

Step 2: Run, expect FAIL. dotnet test tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.csproj --filter "FullyQualifiedName~ParameterValue"

Step 3: Implement — recursive render of object/list via the schema model; per-field validate; keep the JSON textarea only as an "advanced/raw" escape hatch if useful.

Step 4: Run, expect PASS.

Step 5: Commit.

Acceptance: object/list values entered via typed nested forms; per-field validation; $ref-resolved schemas render; canonical JSON output preserved.


Task T31: Monaco JSON-Schema hover/completion

Classification: standard Estimated implement time: ~4 min Parallelizable with: T32c, T26a (NOT T30 — value-entry surface shared) blockedBy: T32b, T30

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/MonacoEditor.razor (+ its JS interop file, if separate)
  • Modify: the value-entry surface that uses Monaco for JSON (e.g. the raw-JSON escape hatch in ParameterValueForm.razor)
  • Test: CentralUI.Tests where feasible (interop is JS — assert the schema is passed to the editor config; otherwise a smoke render)

Context: Monaco is already integrated for script editing. Use Monaco's built-in JSON language schema support (no new package): feed the resolved JSON Schema to the editor so JSON value editing gets hover + completion.

Step 1: Write failing test — the editor is configured with the JSON-schema option carrying the (resolved) schema for the current parameter/value.

Step 24: FAIL → implement (pass schema into the Monaco JSON config via interop) → PASS.

Step 5: Commit.

Acceptance: JSON value editing surfaces schema-driven hover + completion; no new package.


Task T26a: Inheritance resolve service + query command

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: T30, T31 (NOT T32c/T24a/T23a — ManagementActor.cs shared)

Files:

  • Create/Modify: a read-only resolve method — extend src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateResolver.cs (reuse BuildInheritanceChain) or a new TemplateInheritanceResolver
  • Create: GetResolvedTemplateMembersCommand (Commons/Messages/Management)
  • Modify: src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs + ManagementCommandRegistry.cs
  • Test: tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/

Context (D1): Read-only. Given a derived/child template, walk the FULL inheritance chain (BuildInheritanceChain, arbitrary depth) and return the effective inherited member set — including base members added AFTER the derived template was created and across ≥2 levels — annotated per member with origin (own override / inherited-from-X / locked) + a staleness summary (stored derived rows vs. freshly-resolved chain). No mutation — flattening at deploy is already correct; this only feeds the editor.

Step 1: Write failing tests — A→B→C chain: editing C resolves members inherited transitively from A; a base member added to A after C was created appears in C's resolved set; locked members flagged; staleness summary true when stored rows differ, false when in sync; composition-derived template handled.

Step 2: Run, expect FAIL. dotnet test tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.csproj --filter "FullyQualifiedName~Resolve"

Step 3: Implement — the resolve method + GetResolvedTemplateMembersCommand + handler + registry. Strictly read-only.

Step 4: Run, expect PASS.

Step 5: Commit (include ManagementActor + registry).

Acceptance: full multi-level inherited member set resolved (incl. post-creation base additions), origin + staleness annotated, read-only, command registered.


Task T26b: TemplateEdit — full inherited set + staleness banner

Classification: standard Estimated implement time: ~5 min Parallelizable with: T30, T31, T32c (NOT T28b — TemplateEdit shared; T28b is wave 1) blockedBy: T26a

Files:

  • Modify: src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TemplateEdit.razor
  • Test: CentralUI.Tests (bUnit)

Context: Today the editor loads only the immediate base (_baseTemplate, _baseAttributesByName, …). Switch it to render the FULL resolved inherited set from GetResolvedTemplateMembersCommand (T26a), and show a read-only banner when the staleness summary reports the derived template differs from its base ("Base changed — N inherited members differ"). No mutation — existing override actions unchanged.

Step 1: Write failing test — for a multi-level derived template the editor lists transitively-inherited members; the staleness banner appears when the resolve result flags drift and is absent when in sync.

Step 24: FAIL → implement → PASS.

Step 5: Commit.

Acceptance: editor shows the full multi-level inherited set + a read-only staleness banner; no deploy-path change.


Wave 5

Task INT: Integration — build, docker, Playwright, smoke, end-to-end trace

Classification: high-risk Estimated implement time: ~ (controller-run verification phase, not a single implementer) blockedBy: all of T22, CLI, T28a/b, T23a/b, T25, T24a/b, T32a/b/c, T30, T31, T26a/b

Files: docs sync (docs/requirements/Component-*.md for TreeView/TemplateEngine/CLI/Health as touched; README component table if changed); no new feature code (fixes only if a check fails).

Verification checklist (per integration-catches-cross-cutting-gaps):

  1. Full-solution build: dotnet build ZB.MOM.WW.ScadaBridge.slnx — zero warnings (TreatWarningsAsErrors). EF model-drift check: confirm no PendingModelChangesWarning for SharedSchema.
  2. End-to-end command trace (ManagementActor routing + registry name resolution) for every new command: ReorderTemplateFolderCommand, MoveDataConnectionCommand, GetResolvedTemplateMembersCommand, the SchemaLibrary CRUD commands, and the CLI's RetryParkedMessageCommand/DiscardParkedMessageCommand mappings. Confirm each resolves a name in ManagementCommandRegistry and reaches its handler (these are central-only — no site hop — but verify the registry + handler wiring; the CLI fails silently without the name mapping).
  3. Re-run full bUnit suites of every shared component touched: TreeView, TemplateFolderTree, Templates, DataConnections, TemplateEdit, ParameterValueForm, SchemaBuilder — register substitutes for any newly-injected service in their existing fixtures. (Shared-component injection-regression guard.)
  4. Run all touched test projects unfiltered: CentralUI.Tests, TemplateEngine.Tests, ManagementService.Tests, Commons.Tests, CLI.Tests, ConfigurationDatabase tests. Before calling any failure "pre-existing," PROVE it: git diff --stat <base>..HEAD -- <failing test's target files> empty.
  5. Docker rebuild: bash docker/deploy.sh; /health/ready on central-a (9001), central-b (9002), LB (9000) all 200.
  6. Playwright: coverage for the new UI surfaces — template tree search, folder reorder menu + root context menu, move-connection dialog, connection health badge, schema-library page, schema-driven nested form, strict-trigger selector.
  7. Docs: sync the affected component docs + README table; clear any stale markers for the now-shipped items.

Acceptance: full build green + no model drift; every new command traced; all touched suites green; cluster healthy; Playwright covers new UI; docs synced.


Post-milestone follow-ups to log (not in M9)

  • Unified notifications + site-calls outbox page (deferred per D3 — union view-model is high-risk; revisit if operators want one place).
  • Folder drag-drop (HTML5 DnD via JS interop — [PERM]).
  • T26 explicit RefreshDerivedTemplate mutation (only if stored-row resync is later wanted; deploys are already fresh).