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.
This commit is contained in:
@@ -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" -- <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).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"planPath": "docs/plans/2026-06-18-m9-templates-authoring.md",
|
||||||
|
"milestone": "M9 — Templates & Authoring",
|
||||||
|
"branch": "worktree-m9-templates-authoring",
|
||||||
|
"baseRef": "72aec3b4",
|
||||||
|
"tasks": [
|
||||||
|
{"id": 240, "label": "T22", "subject": "M9-T22: Template tree search box", "classification": "small", "wave": 1, "status": "pending"},
|
||||||
|
{"id": 241, "label": "CLI", "subject": "M9-CLI: cached-call retry/discard CLI group", "classification": "small", "wave": 1, "status": "pending"},
|
||||||
|
{"id": 242, "label": "T28a", "subject": "M9-T28a: Strict expression-trigger kind — backend", "classification": "small", "wave": 1, "status": "pending"},
|
||||||
|
{"id": 243, "label": "T28b", "subject": "M9-T28b: Strict trigger-kind — UI selector + CLI flag", "classification": "small", "wave": 1, "status": "pending", "blockedBy": [242]},
|
||||||
|
{"id": 244, "label": "T23a", "subject": "M9-T23a: Folder sibling reorder — service + command + handler", "classification": "standard", "wave": 2, "status": "pending"},
|
||||||
|
{"id": 245, "label": "T23b", "subject": "M9-T23b: Folder reorder + root context menu — UI", "classification": "standard", "wave": 2, "status": "pending", "blockedBy": [240, 244]},
|
||||||
|
{"id": 246, "label": "T25", "subject": "M9-T25: Connection live-status indicators", "classification": "standard", "wave": 2, "status": "pending"},
|
||||||
|
{"id": 247, "label": "T24a", "subject": "M9-T24a: Move data connection between sites — command + handler + guards", "classification": "high-risk", "wave": 3, "status": "pending"},
|
||||||
|
{"id": 248, "label": "T24b", "subject": "M9-T24b: Move connection — UI dialog + action", "classification": "standard", "wave": 3, "status": "pending", "blockedBy": [247, 246]},
|
||||||
|
{"id": 249, "label": "T32a", "subject": "M9-T32a: SharedSchema entity + EF config + migration + repo", "classification": "high-risk", "wave": 3, "status": "pending"},
|
||||||
|
{"id": 250, "label": "T32b", "subject": "M9-T32b: JSON Schema $ref resolver + deploy-time validation", "classification": "high-risk", "wave": 3, "status": "pending", "blockedBy": [249]},
|
||||||
|
{"id": 251, "label": "T32c", "subject": "M9-T32c: Schema library — CRUD commands + handlers + Central UI page", "classification": "high-risk", "wave": 4, "status": "pending", "blockedBy": [249, 250]},
|
||||||
|
{"id": 252, "label": "T30", "subject": "M9-T30: Schema-driven nested value-entry forms", "classification": "standard", "wave": 4, "status": "pending", "blockedBy": [250]},
|
||||||
|
{"id": 253, "label": "T31", "subject": "M9-T31: Monaco JSON-Schema hover/completion", "classification": "standard", "wave": 4, "status": "pending", "blockedBy": [250, 252]},
|
||||||
|
{"id": 254, "label": "T26a", "subject": "M9-T26a: Inheritance resolve service + query command", "classification": "high-risk", "wave": 4, "status": "pending"},
|
||||||
|
{"id": 255, "label": "T26b", "subject": "M9-T26b: TemplateEdit — full inherited set + staleness banner", "classification": "standard", "wave": 4, "status": "pending", "blockedBy": [254]},
|
||||||
|
{"id": 256, "label": "INT", "subject": "M9-INT: Integration — build, docker, Playwright, smoke, end-to-end trace", "classification": "high-risk", "wave": 5, "status": "pending", "blockedBy": [240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]}
|
||||||
|
],
|
||||||
|
"serializationPoints": {
|
||||||
|
"ManagementActor.cs+ManagementCommandRegistry.cs": [244, 247, 251, 254],
|
||||||
|
"ValidationService.cs": [242, 250],
|
||||||
|
"Templates.razor": [240, 245],
|
||||||
|
"DataConnections.razor": [246, 248],
|
||||||
|
"TemplateEdit.razor": [243, 255],
|
||||||
|
"ParameterValueForm.razor/value-entry-surface": [252, 253]
|
||||||
|
},
|
||||||
|
"lastUpdated": "2026-06-18"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user