# 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 D1–D7). **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:** ≤2–3 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 263–401)
- 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 2–4:** 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 1–5:** 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 2–4:** 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 2–4:** 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).
