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

14 KiB
Raw Blame History

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 coreCalculationEvaluator 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).