Files
lmxopcua/docs/plans/2026-07-15-v3-implementation-plan.md
T
Joseph Doherty cf03ca279d docs(v3): implementation plans — master orchestration + 4 batch plans
- v3 design finalized: all 7 open items resolved (live historian probe:
  255-char tagname limit; SDK multi-notifier spike: native AddNotifier,
  never duplicate ReportEvent; NamespaceKind.Simulated retired; CSV column
  dictionaries; ScadaBridge re-bind sized; rename-warning scan decided),
  reconciled with the pre-v3 universal discovery browser (its new §11
  v3 forward note re-targets the browse commit contract).
- Calculation pseudo-driver mini-design (IDependencyConsumer capability,
  mux-fed change/timer triggers, Tarjan cycle deploy gate, VT-parity
  error semantics).
- Implementation plans for Opus-agent execution, grounded against exact
  files/symbols: master plan (batch DAG, worktree-isolated parallel
  waves, contract-first fan-out, global gotcha list) + one plan per
  batch (1 schema+RawPath identity, 2 /raw UI+Calculation, 3 UNS
  reference-only+{{equip}}, 4 dual-namespace address space = v3.0),
  each with per-wave file ownership and a mandatory docker-dev live gate.
2026-07-15 16:50:30 -04:00

163 lines
14 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.
# v3 implementation plan — master orchestration
**Date:** 2026-07-15
**Design:** `docs/plans/2026-07-15-raw-uns-two-subtree-v3-design.md` (all open items resolved; required reading for every agent)
**Companion designs:** `2026-07-15-calculation-driver-mini-design.md` (Batch 2), `2026-07-15-universal-discovery-browser-design.md` (pre-v3 dependency)
**Batch plans:** `2026-07-15-v3-batch1-schema-identity-plan.md` · `2026-07-15-v3-batch2-raw-ui-calculation-plan.md` · `2026-07-15-v3-batch3-uns-rework-plan.md` · `2026-07-15-v3-batch4-address-space-plan.md`
**Executors:** Opus agents. This document is the coordinator's contract: batch order, parallel lanes, merge discipline, and the non-negotiable verification gates.
## 1. Shape of the work
Four **sequential batches** (each = one feature branch + one PR to master, merged only after
its live gate passes), plus one **batch-independent track (Track 0)** that can run in
parallel with Batch 1 because it touches nothing Batch 1 touches.
```
Track 0 (parallel with Batch 1): ContextMenu component · RFC-4180 CSV parser ·
DriverType constants · Calculation evaluator core
Batch 1 (schema + RawPath identity) ──► Batch 2 (/raw UI + Calculation driver)
──► Batch 3 (UNS reference-only + {{equip}}) ──► Batch 4 (dual-namespace
address space) = v3.0
```
**Precondition:** the universal `DiscoveryDriverBrowser`
(`2026-07-15-universal-discovery-browser-design.md`) is a **pre-v3 dependency that is NOT
in the tree yet** (verified 2026-07-15: no `DiscoveryDriverBrowser` exists;
`ITagDiscovery` has no `SupportsOnlineDiscovery` member — the browser design adds it).
Only Batch 2 work package **B2-WP6 (browse re-target)** is blocked on it. Everything else
proceeds; if the browser slips, B2-WP6 detaches into a follow-up PR and the Batch 2 gate
runs without the browse-commit leg.
**Why the batches cannot overlap:** each batch's live gate deploys the previous batch's
schema/UI to docker-dev. Batch 2 authors against Batch 1's schema; Batch 3 references
Batch 2's raw tags; Batch 4 lights up what 13 authored. Parallelism lives *inside*
batches (see each batch plan's wave schedule), not between them.
## 2. Agent execution model
- **One coordinator agent per batch.** It reads the design + the batch plan, spawns
implementation agents per work package, owns the batch feature branch, and is the only
agent that merges.
- **Implementation agents run in isolated git worktrees** (`isolation: worktree` /
`EnterWorktree`). Hard rule from project history: parallel implementers on a shared
checkout race the git index and corrupt each other's staging — never share a tree.
- **Contract-first fan-out.** When a wave shares new types (e.g. the `RawPaths` helper,
the reshaped entities), the coordinator lands a small **contracts commit** on the batch
branch first; wave agents branch from it. No agent invents a shared type mid-wave.
- **File-ownership discipline.** The wave tables in each batch plan assign files to
exactly one work package per wave. If an agent finds it must touch a file owned by
another package, it stops and reports — the coordinator re-slices rather than letting
two agents edit one file.
- **Merge order** is the wave order. After each wave merges, the coordinator runs
`dotnet build ZB.MOM.WW.OtOpcUa.slnx && dotnet test ZB.MOM.WW.OtOpcUa.slnx` before
starting the next wave. A red build blocks the next wave; the coordinator fixes or
reverts, never "fixes forward" into a parallel wave.
- **Review checkpoint per wave:** a code-reviewer agent reviews the wave diff against the
batch plan before merge. Findings are fixed in the worktree, not post-merge.
- **Each batch ends with the live `/run` gate executed by an agent with browser tools**
(the docker-dev AdminUI has `Security__Auth__DisableLogin: "true"` — no sign-in needed;
drive `http://localhost:9200` directly). **Live gates are non-negotiable**: this project
has repeatedly shipped prod-inert code that 250+ unit tests and three reviews missed
(the `DeferredAddressSpaceSink` forwarding trap, the dormant `GatewayTagProvisioner`).
A batch is not done until its live-gate checklist passes.
## 3. Track 0 — batch-independent work (start immediately, parallel with Batch 1)
Four packages with zero overlap with Batch 1's files. Each is a separate worktree +
separate small PR (or merged onto the Batch 2 branch when it opens).
| ID | Package | New files (nothing existing is touched) | Verification |
|---|---|---|---|
| T0-1 | **`ContextMenu` Blazor component** — reusable `oncontextmenu` menu with keyboard/touch fallback (explicit "⋯" affordance). No right-click menu exists anywhere in the Blazor app today (the only `ContextMenu` is in the Avalonia desktop client — do not confuse them). | `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/ContextMenu.razor` (+ `.razor.css`), `ContextMenuItem.cs` | Demo host page under a dev-only route; manual live check on docker-dev (AdminUI has **no bUnit** — Razor bugs must be live-verified) |
| T0-2 | **RFC-4180 CSV parser** — quoted fields, embedded commas/quotes/newlines, CRLF/LF; plus a writer that quotes on demand. Pure, no I/O. No CSV code exists in the repo today (verified) — greenfield. | `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Csv/CsvParser.cs`, `CsvWriter.cs`; tests in `tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/Csv/` | Unit suite incl. RFC 4180 edge corpus (quoted quote, trailing newline, empty field vs absent field) |
| T0-3 | **`DriverTypeNames` constants class** — single source of truth for driver-type strings, authority = the factories' `DriverTypeName` (`TwinCAT`, `FOCAS`, `GalaxyMxGateway`). Consumed later by the four drifted maps (`DriverEditRouter._componentMap` currently keys `TwinCat`/`Focas`; `TagConfigEditorMap`; `TagConfigValidator`; `EquipmentTagConfigInspector` in **ControlPlane**, not AdminUI). T0-3 only *adds* the constants + a reflection test asserting they match every registered factory; the four consumers are rewired in Batch 2 (B2-WP8). | `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeNames.cs`; test in `tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/` (or Commons.Tests if no such project) | Reflection test: every `DriverTypeName` exposed by `DriverFactoryBootstrap.AddOtOpcUaDriverFactories` registrations has a matching constant, and vice versa |
| T0-4 | **Calculation evaluator core**`CalculationEvaluator` built like `RoslynVirtualTagEvaluator` (`src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs`): `CompiledScriptCache` + `TimedScriptEvaluator` (2 s) + `VirtualTagContext`, single-tag mode (`ctx.SetVirtualTag` dropped + logged). Pure engine + unit tests only; the driver shell that hosts it is Batch 2 (B2-WP7). See mini-design §4. | New project `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/` (evaluator + tests only at this stage) | Parity unit tests vs the VT evaluator: passthrough fast-path, timeout, sandbox violation, SetVirtualTag drop |
## 4. Batch summaries + gates (detail in the batch plans)
| Batch | Branch | Content | Gate (summary — full recipe in the batch plan) |
|---|---|---|---|
| 1 | `v3/batch1-schema-identity` | Greenfield EF schema (RawFolder/Device-universal/TagGroup/Tag-raw-only/UnsTagReference; Namespace + EquipmentImportBatch retired); `RawPaths` identity helper; `TagConfigIntent` sheds FullName; all 8 drivers' resolvers keyed by RawPath (blob-fallback parsers retired); endpoint→`DeviceConfig` move; composer/artifact/DraftValidator/UnsTreeService rewired; seeds + golden corpus rewritten. **Address space intentionally dark.** | Full solution build + test green (incl. rewritten corpus); `V3Initial` migration applies to a fresh DB; docker-dev re-seeds and both nodes boot with drivers Connected (values not yet exposed — that's Batch 4) |
| 2 | `v3/batch2-raw-ui` | `/raw` project tree (lazy loading + T0-1 menus), driver config/device modals (typed pages refactored to embeddable bodies), tag manual entry + CSV import/export (T0-2 + decided column dictionaries), browse re-target (gated on universal browser), `Calculation` driver (T0-4 + mini-design), `DriverTypeNames` rewire | Live `/run` on `:9200`: author folder→driver→device→tags in-tree; Test-connect green vs a fixture; CSV round-trip; browse-commit raw tags (if browser landed); author + deploy a calc tag. Proves authoring + probe only — no live values until Batch 4 |
| 3 | `v3/batch3-uns-rework` | Equipment Tags tab → reference list + cluster-scoped multi-select raw picker; effective-name uniqueness (authoring + DraftValidator); `{{equip}}` reference-relative resolution + unresolved-ref deploy error; rename-warning substring scan | Live `/run`: reference raw tags into an equipment; display-name override shows; deploy gate rejects a reference collision and an unresolved `{{equip}}` ref |
| 4 | `v3/batch4-address-space` | Dual namespaces, RawPath NodeIds, UNS fan-out registration, raw-only runtime binding, write routing via either NodeId, alarm multi-notifier (`AddNotifier` pattern + teardown symmetry), historian dual-registration, sink namespace discriminator + `DeferredAddressSpaceSink` forwarding + reflection-test extension | Live `/run` + Client.CLI: browse both namespaces; read/subscribe/write through both NodeIds; HistoryRead via both; native alarm visible at raw + equipment notifiers with exactly one Server-object copy; `{{equip}}` script computes; failed-write revert still works. **= v3.0** |
## 5. Global conventions (binding for every agent)
- .NET 10, C#, xUnit + **Shouldly**, Serilog. Match surrounding code style; comments only
for constraints the code can't show.
- JSON config keys are **camelCase**; enum values serialize as **string names**
(`JsonStringEnumConverter`). Systemic past bug: AdminUI pages serialized enums
numerically while DTOs were string-typed → authored configs faulted drivers. Every new
serialization site gets the converter, and every new editor gets a round-trip test.
- Editors/models **preserve unknown JSON keys** (the established `<Driver>TagConfigModel`
`FromJson`/`ToJson` contract). .NET 10 `JsonNode.ToJsonString` gotchas are documented in
the existing models — copy the Modbus template.
- **Do not enable** `CentralPackageTransitivePinningEnabled` (breaks the Roslyn build via
the 5.0.0/4.12.0 split). Transitive CVE fixes = surgical direct `PackageReference` at a
common-ancestor project.
- EF migrations via `dotnet ef` with `DesignTimeDbContextFactory` (reads
`OTOPCUA_CONFIG_CONNECTION`, defaults to `10.100.0.35,14330`). All model config lives
inline in `OtOpcUaConfigDbContext.OnModelCreating` — keep it there (no
`IEntityTypeConfiguration` files exist; don't introduce the pattern mid-stream).
- Blazor: string component parameters need the `@` prefix when passing C# expressions
(the F15 gotcha that bit Global UNS); no bUnit exists — **live-verify every Razor
change** on docker-dev.
- Name collision: Runtime already owns
`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs` — any
browser-side capturing builder must use a different name or namespace-qualify.
## 6. Global gotchas (project scar tissue — read before coding)
1. **The forwarding trap (highest-severity recurring bug):** any new or changed method on
`IOpcUaAddressSpaceSink` / `ISurgicalAddressSpaceSink`
(`src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/`) **must** be forwarded through
`DeferredAddressSpaceSink` and covered by
`tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredSinkForwardingReflectionTests.cs`.
A non-forwarded method compiles, passes unit tests, and is silently dead on driver
hosts in production. Same family: registering a service in DI without passing it into
its consumer (the dormant-`GatewayTagProvisioner` bug) — every "register X" step needs
a "consume X" assertion.
2. **`OnWriteValue` runs under the node-manager lock** — treat driver write dispatch as
fire-and-forget (`Tell`, never blocking ask) exactly as `OnEquipmentTagWrite` does
today (`OtOpcUaNodeManager.cs:1451`).
3. **Resolve `DriverHostActor` lazily** from consumers wired at startup — the actor
spawns after DI graph construction.
4. **docker-dev `:9200` round-robins central-1/central-2** behind Traefik — rebuild and
restart **both** (`docker compose build central-1 central-2 && docker compose up -d`)
before trusting any AdminUI verification; kind-changing config edits need
deploy-THEN-recreate.
5. Historian provisioning: `StorageRateMs` must be **> 0** (proto default 0 throws
gateway-side and silently fails all provisioning — PR #439).
6. Client.CLI `write` **reads first** — you cannot write a node whose read is Bad. To
force a protocol write failure on the rig, use the modbus fixture's
`exception_injector` (FC06 reject rule on a seeded Good address).
7. Data-plane role tests need `Security:Ldap:GroupToRole` in appsettings; LDAP is the
shared GLAuth `10.100.0.35:3893` (all test users password `password`).
8. Deploy API: `POST http://localhost:9200/api/deployments` with `X-Api-Key` from
`Security:DeployApiKey` (self-disables 503 when unset). Direct SQL edits against the
config DB need `SET QUOTED_IDENTIFIER ON`.
9. Driver fixtures live on the Docker host `10.100.0.35`, controlled via
`lmxopcua-fix ls|up|down|sync` from this VM (`docker -H ssh://` does NOT work).
10. Native-alarm routing keys on **`ConditionId`** (the authored ref), never
`SourceNodeId` — under v3 the ConditionId becomes the RawPath.
## 7. Deliverable checklist (coordinator ticks per batch)
- [ ] Batch branch opened from up-to-date master; contracts commit landed before fan-out.
- [ ] Every wave: worktree-isolated agents, file-ownership respected, reviewer pass, green
build+test after merge.
- [ ] Batch-specific unit/integration suites listed in the batch plan all green, including
the DB-backed suites (`Configuration.Tests` schema-compliance against
`10.100.0.35,14330`) and the 2-node harness
(`tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/`, default in-memory mode).
- [ ] Live `/run` gate checklist from the batch plan executed and evidence recorded in the
PR description (what was clicked/deployed/observed).
- [ ] Docs touched by the batch updated in the same PR (`docs/Configuration.md`,
`docs/Uns.md`, `docs/ScriptEditor.md`, `docs/Historian.md`, `CLAUDE.md` sections
that describe replaced behavior).
- [ ] Cross-repo: after Batch 4 merges, update the ScadaBridge coordination note + the
umbrella index `../scadaproj/CLAUDE.md` OtOpcUa entry (the design's cutover plan
step 4).