From 939aea159bbb69b1bc6cafedc6632cd40ed7e761 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 10:08:57 -0400 Subject: [PATCH] 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-m9-templates-authoring.md | 519 ++++++++++++++++++ ...06-18-m9-templates-authoring.md.tasks.json | 34 ++ 2 files changed, 553 insertions(+) create mode 100644 docs/plans/2026-06-18-m9-templates-authoring.md create mode 100644 docs/plans/2026-06-18-m9-templates-authoring.md.tasks.json diff --git a/docs/plans/2026-06-18-m9-templates-authoring.md b/docs/plans/2026-06-18-m9-templates-authoring.md new file mode 100644 index 00000000..4bf76e18 --- /dev/null +++ b/docs/plans/2026-06-18-m9-templates-authoring.md @@ -0,0 +1,519 @@ +# 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" -- ` (`-m` BEFORE `--`). New files: `git add ` (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 `` (`@bind="_searchText" @bind:event="oninput"`) above the tree, with a clear (✕) affordance; pass `Filter="_searchText"` to ``. 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 ` 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` `