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

520 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.cs`**T23a, T24a, T32c, T26a** (the `Parallelizable with` lists already exclude each other — honor it).
- `TemplateEngine/Validation/ValidationService.cs`**T28a, 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
### Task T22: Template tree search box
**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: Commit**`git 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.ExecuteCommandAsync``ManagementHttpClient.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 `discard``DiscardParkedMessageCommand`. 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: Implement**`CachedCallCommands.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 tests**`ReorderFolderAsync(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: Implement**`ReorderFolderAsync` (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: Implement**`MoveDataConnectionCommand(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).