- 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.
14 KiB
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 1–3 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
RawPathshelper, 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.slnxbefore 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
/rungate executed by an agent with browser tools (the docker-dev AdminUI hasSecurity__Auth__DisableLogin: "true"— no sign-in needed; drivehttp://localhost:9200directly). Live gates are non-negotiable: this project has repeatedly shipped prod-inert code that 250+ unit tests and three reviews missed (theDeferredAddressSpaceSinkforwarding trap, the dormantGatewayTagProvisioner). 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>TagConfigModelFromJson/ToJsoncontract). .NET 10JsonNode.ToJsonStringgotchas 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 directPackageReferenceat a common-ancestor project. - EF migrations via
dotnet efwithDesignTimeDbContextFactory(readsOTOPCUA_CONFIG_CONNECTION, defaults to10.100.0.35,14330). All model config lives inline inOtOpcUaConfigDbContext.OnModelCreating— keep it there (noIEntityTypeConfigurationfiles 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)
- 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 throughDeferredAddressSpaceSinkand covered bytests/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-GatewayTagProvisionerbug) — every "register X" step needs a "consume X" assertion. OnWriteValueruns under the node-manager lock — treat driver write dispatch as fire-and-forget (Tell, never blocking ask) exactly asOnEquipmentTagWritedoes today (OtOpcUaNodeManager.cs:1451).- Resolve
DriverHostActorlazily from consumers wired at startup — the actor spawns after DI graph construction. - docker-dev
:9200round-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. - Historian provisioning:
StorageRateMsmust be > 0 (proto default 0 throws gateway-side and silently fails all provisioning — PR #439). - Client.CLI
writereads first — you cannot write a node whose read is Bad. To force a protocol write failure on the rig, use the modbus fixture'sexception_injector(FC06 reject rule on a seeded Good address). - Data-plane role tests need
Security:Ldap:GroupToRolein appsettings; LDAP is the shared GLAuth10.100.0.35:3893(all test users passwordpassword). - Deploy API:
POST http://localhost:9200/api/deploymentswithX-Api-KeyfromSecurity:DeployApiKey(self-disables 503 when unset). Direct SQL edits against the config DB needSET QUOTED_IDENTIFIER ON. - Driver fixtures live on the Docker host
10.100.0.35, controlled vialmxopcua-fix ls|up|down|syncfrom this VM (docker -H ssh://does NOT work). - Native-alarm routing keys on
ConditionId(the authored ref), neverSourceNodeId— 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.Testsschema-compliance against10.100.0.35,14330) and the 2-node harness (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/, default in-memory mode). - Live
/rungate 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.mdsections that describe replaced behavior). - Cross-repo: after Batch 4 merges, update the ScadaBridge coordination note + the
umbrella index
../scadaproj/CLAUDE.mdOtOpcUa entry (the design's cutover plan step 4).