From ea4a3c9289ca138e33da581be7429546b085cc5a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 09:14:35 -0500 Subject: [PATCH] docs: add reviewed OPC UA SDK 2.0 upgrade plan --- docs/plans/2026-11-10-opcua-sdk-2-upgrade.md | 343 +++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 docs/plans/2026-11-10-opcua-sdk-2-upgrade.md diff --git a/docs/plans/2026-11-10-opcua-sdk-2-upgrade.md b/docs/plans/2026-11-10-opcua-sdk-2-upgrade.md new file mode 100644 index 00000000..a7d642a4 --- /dev/null +++ b/docs/plans/2026-11-10-opcua-sdk-2-upgrade.md @@ -0,0 +1,343 @@ +# OPC Foundation SDK 2.0 Upgrade Implementation Plan + +> **For Codex:** REQUIRED SUB-SKILL: Use `executeplan` to implement this plan task-by-task. + +**Goal:** Upgrade every OtOpcUa OPC Foundation dependency to stable 2.0.1, preserve the deployed OPC UA contract, and replace #751 private transport reflection with public peer-address evidence where that helper exists in the execution baseline. + +**Architecture:** Keep the existing application host, custom node manager, SDK adapters, driver lifecycle, and Akka redundancy authority. Convert SDK types at adapter boundaries while keeping domain contracts independent of SDK types. Migrate required APIs first; evaluate optional SDK features only after compatibility is proven. + +**Tech Stack:** .NET 10, C#, central NuGet package management, OPCFoundation.NetStandard.Opc.Ua 2.0.1, xUnit, existing server/client and driver test rigs, Windows and Linux deployment artifacts. + +--- + +## Scope, evidence, and execution boundary + +Prepared 2026-11-10 for [lmxopcua#782](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/782). This document is a design and implementation specification, not evidence that the upgrade has run. No package changes, restore, build, tests, deployments, issue edits, or sister-repository changes were performed to prepare it. + +Observed checkout: `master`, commit `7ec5b95d24e235ef72e81ab180a4f5e6a412ef08`, initially clean. `Directory.Packages.props` pins Client, Core, Configuration, Server to **1.5.378.106**; `Directory.Build.props` targets **net10.0**. Most production projects treat warnings as errors. This is older than the issue's described implementation: `Security/SecureChannelRemoteEndpoint.cs` and `RemoteEndpointResolutionTests` do **not** exist here. The live issue references later #751/#770 work and the current Gitea backlog mentions a 1.5.378.182 maintenance upgrade. Do not overwrite that newer work or assume this checkout is the deployable baseline. Task 1 must reconcile the actual baseline before editing packages. + +Stable target: **2.0.1**, upstream tag commit `07e62f72500cc0923a2bab940653c8c1116fa286`, GitHub release published 2026-10-12. NuGet flat-container indexes were checked for Core, Client, Configuration, Server and MigrationAnalyzer; all contain exact version 2.0.1. Use no floating versions or master builds. If execution discovers a newer stable release, retain 2.0.1 unless a separately reviewed target decision updates this plan and repeats the API evidence checks. + +### Source register + +These are primary sources; tagged sources control implementation when master differs. + +- [S1: requested current What's New](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/WhatsNewIn2.0.md) — discovery only; master is moving. +- [S2: What's New at 2.0.1](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/WhatsNewIn2.0.md) — capabilities and limitations of the target overview. +- [S3: 2.0.1 release](https://github.com/OPCFoundation/UA-.NETStandard/releases/tag/2.0.1), [2.0.0 release](https://github.com/OPCFoundation/UA-.NETStandard/releases/tag/2.0.0) — release authority. Some tagged package guide prose still says prerelease; this is stale prose, not a reason to use preview packages. +- [S4: tagged migration guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/MigrationGuide.md), [thematic index](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/README.md). +- [S5: types](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/types.md), [node states](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/node-states.md). +- [S6: certificates](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/certificates.md), [identity](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/identity.md). +- [S7: sessions/subscriptions](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/sessions-subscriptions.md), [alarms/model changes](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/alarms-model-change.md). +- [S8: packages](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/packages.md), [configuration](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/configuration.md), [source generation](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/source-generation.md), [encoders](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/encoders.md). +- [S9: actual SecureChannelContext source](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/src/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs) — `IPAddress? PeerAddress` exists in 2.0.1; channel certificate and thumbprint properties are `ByteString`. +- [S10: telemetry](https://github.com/OPCFoundation/UA-.NETStandard/blob/2.0.1/docs/migrate/2.0.x/telemetry.md). + +Context7's official-repository index corroborated migration themes, but its master-based snippets are not a substitute for the tag or restored assembly signatures. Later master additions (for example expanded modeling features and subscription callback migration guidance) must not be treated as 2.0.1 requirements without tagged API evidence. + +## Research findings and feature disposition + +| Change/new capability | OtOpcUa implication | Upgrade decision | +|---|---|---| +| Immutable built-in value types; `Variant` in place of object-facing SDK APIs | NodeId/ExpandedNodeId, QualifiedName, LocalizedText, StatusCode, DataValue, ExtensionObject and DateTimeUtc conversions affect server, adapters, UI/CLI and tests | Required. Explicit boundary conversion; test null/default/value semantics, equality and timestamps [S5] | +| `ByteString`, `ArrayOf`, `MatrixOf` and removed generated collections | EventIds, certificates, continuation points, method arguments, arrays, matrices and results change representation | Required. Build mutable buffers locally then freeze once; no repeated immutable append in hot loops [S5] | +| Strongly typed source-generated models and encodeable factories | Existing generated state types/defaults and serialization may change; factory globals are obsolete/removed | Required where used. Do not redesign dynamic raw/UNS tree into companion models [S8] | +| `AddOpcUa()` DI, host services, injectable providers | Could consolidate construction, telemetry and certificates | Use required provider/telemetry interfaces within existing host. Full hosting rewrite is optional and excluded [S2, S10] | +| `ManagedSession`, V2 subscriptions, streaming callbacks and failover helpers | Potential future simplification of custom driver/client lifecycle | Keep classic Session if supported by exact target. Port removed APIs locally; do not combine competing reconnect engines [S7] | +| `AsyncCustomNodeManager`, cancellation, TimeProvider | Better async data sources and deterministic tests | Preserve CustomNodeManager2 when target supports required overrides. Async conversion is a conditional migration task only if parity cannot be achieved; existing blocking bridges need explicit lifetime/lock tests [S2, S4] | +| New node lifecycle, synchronization, permission hooks and continuation management | SDK defaults can bypass custom assumptions even if code compiles | Required audit of lifecycle, locks, metadata and all security hooks [S4, S5] | +| Public peer address | Removes private channel-table/socket reflection from later #751 code | Required on rebased baseline; unknown remains unknown and never becomes a fabricated client IP [S9] | +| Certificate manager, reference-counted wrappers, providers and identity registry | Trust and resource ownership change, token events are no longer preferred integration | Required certificate migration; retain impersonation fallback only when target contract and allow/deny tests prove it is invoked. Prefer explicit registry authenticator if fallback order changes behavior [S6] | +| Stricter Write, permission, filter and locale semantics | Earlier rejection can change statuses, alarm visibility and which callbacks run | Required behavioral compatibility review; preserve stricter protocol validation and document intentional corrections [S4] | +| Alarm state compliance and automatic model-change events | Duplicate reports or information leaks are possible around custom ACL delivery | Required. One authoritative filtered emission path; check optional state nodes and Retain [S7] | +| Pooling, allocation and server scalability improvements | Potential gain; borrowed buffers must not outlive callback/owner | Measure; no automatic claim that every workload gets faster [S2] | +| Native AOT | SDK supports suitable applications; this host also uses Roslyn/dynamic components | Excluded. Continue normal JIT publishing; SDK support does not establish host compatibility [S2] | +| GDS pull/push, credential services, ECC/AEAD and crypto provider extensibility | Useful future certificate/security capabilities | Preserve existing RSA/policy/trust configuration. No automatic new endpoints, trust rules or hardware-provider adoption [S2, S6] | +| SDK redundancy/distributed state/Kubernetes and replica identity | Could overlap custom Akka mesh and ServiceLevel authority | Excluded. Existing identity and redundancy remain authoritative [S2] | +| PubSub modernization, MQTT/Kafka/UDP transports | Separate OPC UA Part 14 feature; existing MQTT driver is not equivalent | Excluded; no package additions for this feature [S2] | +| Companion models, file/alias/state-machine helpers, runtime NodeSets, MCP tooling | SDK expansion does not imply OtOpcUa implements/certifies these features | Excluded; no new exposed capabilities or certification claims [S2] | + +2.0.1 specifically changes channel certificates to ByteString, makes empty LocalizedText null, adds nullable API annotations, refuses expired channel tokens and tightens anonymous sessionless security. Test existing empty-text consumers and token renewal; do not silence nullable warnings globally [S3]. + +## Design contract + +1. Keep SDK dependencies in current infrastructure boundaries. `Driver.OpcUaClient.Contracts` must stay dependency-light; Core domain types must not gain SDK dependencies to make conversions convenient. Core abstractions tests already reference SDK types as test oracles; that does not authorize production coupling. +2. Raw namespace `https://zb.com/otopcua/raw` and UNS namespace `https://zb.com/otopcua/uns`, stable identifier strings, source quality/timestamps, one source feeding both realms, historian naming, condition identities and deployed config schema stay stable. Compare by namespace URI plus identifier, not accidental namespace indexes. +3. Preserve custom API-key identity (`apikey` user name), anonymous handling, ACL-on scoped permissions, ACL-off role behavior, current key expiry/revocation checks, and per-event filtering. Read `.claude/rules/data-plane-auth.md`; LDAP is AdminUI-only despite older CLAUDE.md passages mentioning data-plane LDAP. +4. SDK protocol permissions and application ACLs must compose without denying legitimate narrow grants or granting broader rights. Do not grant SecurityAdmin globally to recover audit events. Test intended audit audiences and suppression with SDK EventType permissions. +5. Centralize object ↔ SDK Variant conversion at existing server/client/driver adapters. Preserve supported CLR payload types outward. Handle null vs empty ByteString/array explicitly, ByteString vs Byte array distinctly, DateTime UTC and picoseconds where exposed, numerical widths, enum values and matrices by rank/dimensions. Unsupported values return the documented bad status; never stringify or silently coerce. +6. For history, keep `HistoryContinuationState` in `HistoryPaging.cs` SDK-free. Wrap it in an SDK-facing `IHistoryContinuationPoint` carrying Guid Id and explicit Dispose ownership. Use `session.ContinuationPoints.SaveHistory(point)` / `RestoreHistory(bytes)` rather than removed ISession methods. Verify consumption, release, eviction and session-close disposal against the target implementation; do not assume restore disposes or assume a restored point stays live. Maintain binding to session, node, query and authority. +7. For peer address, use the SDK-provided request/activation channel context, captured before any async hop. Retain its `IPAddress?` as evidence; use `MapToIPv4` only for mapped addresses if existing CIDR policy requires normalization. Never resolve an advertised endpoint hostname or trust a caller's application URI as the source IP. Preserve unknown-address fail-closed restrictions and proxy/NAT meaning (observed immediate peer). No static mutable last-peer cache. +8. Retain one owner for reconnect, failover, namespace generation and subscription disposal in each client/driver. No implicit replay of non-idempotent writes following transport loss. Classic API retention is an explicit design choice, not a requirement to keep obsolete methods that are absent in target. +9. Use existing own locks for shared maps and batched updates. NodeState internal locking does not synchronize multiple nodes, maps or policy generation. Do not mechanically remove the node-manager Lock, introduce lock inversion, or hold a lock over I/O/await. Retain bounded history/write concurrency and shutdown deadlines. +10. Disable SDK automatic model-change emission (`ModelChangeEmissionEnabled = false` where supported by the rebased manager) when the application supplies the filtered path. Test actual Create/Delete paths to establish exactly one event and no unfiltered SDK leak. Review SDK alarm setter side effects to avoid duplicate state events. + +## Verification commands and result rules + +Run these only when executing the plan. Use the repository's `global.json` SDK. Capture logs/TRX under an ignored `artifacts/opcua2/` directory. Before choosing that directory verify `.gitignore`; add an ignore rule if necessary. Use one test project per process for diagnosis; do not run concurrent builds into shared obj/bin directories. + +```bash +git status --short +git rev-parse HEAD +dotnet --info +rg -n 'OPCFoundation' Directory.Packages.props +rg -l 'OPCFoundation|Opc.Ua' src tests --glob '*.csproj' +dotnet restore ZB.MOM.WW.OtOpcUa.slnx +dotnet build ZB.MOM.WW.OtOpcUa.slnx -c Release --no-restore +``` + +Define this portable helper in the execution shell once; task commands below use it. Before each invocation set `OPC_RUN_DIR` to a new, previously nonexistent directory of the form `artifacts/opcua2////` identifying gate and attempt; the helper fails if that run directory exists. Do not reuse baseline or earlier candidate TRX for another gate: + +```bash +opc_test() { + local opc_project="$1" + local opc_name="${opc_project##*/}" + : "${OPC_RUN_DIR:?Set a fresh run directory for this invocation}" + if [ -e "$OPC_RUN_DIR" ]; then return 2; fi + mkdir -p "$OPC_RUN_DIR" || return + local opc_test_status=0 + dotnet test "$opc_project/$opc_name.csproj" -c Release \ + --logger trx --results-directory "$OPC_RUN_DIR" || opc_test_status=$? + local opc_gate_status=0 + MIN_EXECUTED=1 bash scripts/ci/assert-not-all-skipped.sh "$OPC_RUN_DIR"/*.trx \ + || opc_gate_status=$? + if [ "$opc_test_status" -ne 0 ]; then return "$opc_test_status"; fi + return "$opc_gate_status" +} +``` + +Task test commands reference this helper. An unfiltered targeted project run must pass with nonzero executed tests. Integration categories needing external fixtures must run with the documented fixture, not be counted as passed when skipped. Run the existing skip-guard fixture tests with `python3 scripts/ci/test-assert-not-all-skipped.py`; run result validation with `MIN_EXECUTED=1 bash scripts/ci/assert-not-all-skipped.sh "$OPC_RUN_DIR"/*.trx`, and inspect counts for individual required cases. Record environment failures separately from product failures. Never update snapshots or expected bad statuses solely to make tests green. + +## Implementation tasks and dependency gates + +For each task after its dependency graph compiles: add a focused regression first; run it against the unmodified behavior (a new target-specific test may fail at compile time); record the failure or demonstrate a passing existing contract oracle; make the smallest coherent production change; rerun the focused case and whole owning test project; review diff; commit only that task's files with the message indicated. Tasks 4–7 are one server compilation batch: their test edits and implementation can be committed in reviewable slices, but no intermediate slice is claimed passing while later server compile breaks remain. Use source/API diagnostics for intermediate progress, then run every focused regression and the full owning project once the complete server batch compiles. Similarly, client/driver lanes depend on server fixes for wire tests. Record every deferred command and execute it before G2/G3; a compile-only intermediate commit is not a behavioral pass. Individual test/fix/recheck actions should be 2–5 minute steps; large migrations are split by file/behavior, not one unreviewed codefix pass. Exact final SDK override signatures must come from 2.0.1 assemblies/source; this plan does not invent uncompiled replacement signatures. + +### Task 1 — Reconcile baseline and freeze the design (coordinator, serial) + +**Read:** `CLAUDE.md`, `.claude/rules/{data-plane-auth,alarms,historian,drivers,cluster-topology}.md`, `Directory.Packages.props`, `global.json`, `.github/workflows/v2-ci.yml`, current #782 and its comments plus #751/#770. Inspect current remote branch without mutating another person's work. + +**Create:** `docs/plans/2026-11-10-opcua-sdk-2-evidence.md` with baseline SHA, target SHA/version, dependency inventory, effective package sources, environment matrix, test totals, release API discrepancies, and decision records. + +Steps: (1) Inventory working changes and latest remote commits; (2) choose suitable existing checkout or create a worktree through the Codex worktree tool if required, branch `codex/opcua-sdk-2`; (3) reconcile latest security/SDK maintenance work before branching migration; (4) record each source/test project directly or transitively affected, including historian test projects and all consumers found by search; (5) run baseline restore/build and all currently runnable targeted tests; (6) produce baseline host and client artifacts for cross-version comparison; (7) freeze decisions in this design and review with Astra. + +**Gate G0:** Current baseline's history, ACL, driver/client lifecycle and trusted certificate connections pass, or pre-existing failures are reproduced and explicitly accounted for. No fabricated #751 file changes. If the helper is absent even on the current authorized baseline, record the issue mismatch and implement only the peer-evidence integration actually needed by its restrictions. Do not recreate reflection merely to delete it. + +**Commit:** `docs: record OPC UA 2.0 migration baseline and decisions`. + +### Task 2 — Package and analyzer spike (coordinator, serial) + +**Modify:** `Directory.Packages.props`; only SDK-using `.csproj` files identified in Task 1; `NuGet.config` only if package-source mapping prevents already authorized nuget.org restore. + +Steps: (1) pin all four existing packages to 2.0.1 in one change; (2) add MigrationAnalyzer 2.0.1 centrally and as a development-only project reference (`PrivateAssets="all"`, `IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive"`) to relevant consumers, or follow the package's verified analyzer-only asset instructions; (3) restore; (4) build and inventory errors by owner and breaking API in the evidence file; (5) inspect `obj/project.assets.json` / `dotnet list package --include-transitive` for every entry executable and SDK consumer; (6) resolve package downgrades/conflicts and central Microsoft.Extensions/JSON/security dependencies explicitly. Astra verified the 2.0.1 net10 nuspec dependencies require Microsoft.Extensions/Bcl.TimeProvider/RateLimiting 10.0.12 and Client Http.Resilience 10.10.0, while this checkout pins Extensions 10.0.7; inspect each actual nuspec, raise the compatible central baseline deliberately, and regression-test all hosts consuming those shared dependencies. The tagged package guide contains stale version prose, so the restored assets/nuspec is authoritative. Existing comments in Directory.Packages.props deliberately avoided a family-wide servicing uplift; this upgrade makes a reviewed Extensions uplift unavoidable, so give it a separate reviewable dependency commit and test AdminUI, authentication/DataProtection, control plane and test hosts. Retain existing Xml/Pkcs 10.0.13 and other security pins when above SDK minimums; do not lower them to match a dependency floor. Keep EF/ASP.NET versions unchanged unless the actual resolved graph requires a coherent uplift, then include that uplift explicitly in the same dependency review. Do not broadly bump unrelated packages or suppress NU1605. + +Analyzer warnings may become errors under existing production settings; use a temporary **migration-only local diagnostic command** if necessary to inventory them, never weaken committed warning policy. Run only code fixes available in the **2.0.1** analyzer, scoped per owned file and diagnostic. Master's UA rule count/coverage is not guaranteed in that package. Verify the analyzer can load under the global.json compiler (local Microsoft.CodeAnalysis pins deliberately stay at 5.0.0 to avoid CS9057); if it cannot, omit incompatible automation and perform the same migration manually rather than upgrading the compiler toolchain as an unreviewed side effect. Collection shims emitted by its generator are temporary scaffolding, not the delivered contract. + +**Gate G1:** Restore graph contains one consistent SDK family; no mixed 1.x/2.x runtime assembly dependencies; exact compile-break inventory and owner contract ready. Compile failure is expected at this gate, not accepted at merge. + +**Commit:** `build: pin OPC UA SDK 2.0.1 and migration tooling`. + +### Task 3 — Boundary conversion contract (coordinator designs; owners implement locally) + +**Modify:** `src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Helpers/ValueConverter.cs`, `.../Adapters/DefaultSessionAdapter.cs`; server `OtOpcUaNodeManager.cs`; driver `OpcUaClientDriver.cs` and `Session/UpstreamAttributeMapping.cs` in their respective lanes. Create private conversion helpers within owning projects only if repeated conversions justify them. + +**Tests:** extend `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs`, `VariableShapeTests.cs`, `tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/Helpers/ValueConverterTests.cs` (create if missing), and driver `OpcUaClientAttributeMappingTests.cs`. + +Test table: Boolean; signed/unsigned 8/16/32/64-bit; float/double; string including null/empty; Guid; timestamp; ByteString null/empty/nonempty; numeric/string arrays empty/single/many; matrix rank/dimensions if already supported; Variant.Null vs a default scalar; status and source/server timestamps; opaque NodeId. Assert both payload type and value. Test unsupported shape rejection rather than adding matrix support to the product accidentally. Mutation-after-publication test proves retained payload does not reference reused buffers. + +Implementation recipe: replace scalar constructors with target typed Variant factories, use explicit type extraction; rebuild immutable DataValue as a whole preserving metadata; do not assume `Variant.AsBoxedObject()` returns legacy CLR arrays (it can return ArrayOf/ByteString); normalize explicitly to existing domain payloads; change null checks on value types to `.IsNull` and retain actual nullable semantics where domain requires them; build `List`/arrays once then convert to `ArrayOf`. Use `NodeId.Parse` for encoded references and proper string-identifier construction for raw paths; no unchecked object casts. Record supported shape mapping in evidence before lanes begin. + +**Gate:** All lanes agree the CLR outward contract; no new SDK references in Core/Contracts. **Commit per lane:** `refactor: adapt OPC UA 2.0 value boundaries`. + +### Task 4 — Server host, authentication, certificates and peer evidence (server owner) + +**Modify:** `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs`, `OtOpcUaSdkServer.cs`, `Security/RoleCarryingUserIdentity.cs`, `Security/IOpcUaUserAuthenticator.cs` only if unavoidable; host `src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/` authentication composition; rebased `Security/SecureChannelRemoteEndpoint.cs` if present. + +**Tests:** existing `OpcUaApplicationHostSecurityTests.cs`, `OpcUaApplicationHostImpersonationTests.cs`, `OpcUaApplicationHostQuotaTests.cs`, `DataPlaneAuthorizerTests.cs`; rebased `RemoteEndpointResolutionTests.cs`; add `Sdk2CertificateLifecycleTests.cs` under server unit tests and `PeerAddressAuthenticationWireTests.cs` under server integration tests. + +Steps: first characterize allow/deny for valid/invalid/expired/revoked API keys, anonymous allowed/disabled, restricted source CIDR and unknown peer; then port configuration/certificate initialization and validator policy to manager/provider/result APIs; retain current rejection policy (do not copy upstream autoaccept examples); port identity integration with explicit registry order or verified fallback; invalid/disabled app credentials and forbidden anonymous must return an explicit Reject, never NotHandled fallthrough to another authenticator; preserve Principal, Roles and ApiKeyId on RoleCarryingUserIdentity; replace private endpoint lookup with S9 public context; remove only the private-field reflection test and replace it with behavioral evidence tests; validate teardown and restart. + +PKI cases: existing app cert reused with identical thumbprint/private key; trusted peer/issuer chain succeeds; untrusted, expired, wrong-host/URI where applicable, revoked and incomplete-chain certs fail according to target policy; CA keyCertSign/cRLSign requirements checked during preflight; no insecure retry. Verify every returned cert/store ownership contract, release on success/failure/cancel/restart, and do not dispose borrowed channel buffers. Preserve endpoints and token policies, including None only where already explicitly configured. + +Peer cases: IPv4/IPv6/mapped IPv6, null, two simultaneous clients on distinct source addresses, reconnect, reverse-connect if used, and Sign/SignAndEncrypt. Real channel source evidence must reach restrictions; advertised client endpoints cannot spoof it. Test endpoint discovery context without a selected endpoint without null dereference. No automatic cert rotation/GDS adoption. + +**Run after server compilation batch:** `opc_test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests`. **Commit:** `feat: migrate OPC UA host security to SDK 2.0`. + +### Task 5 — Node manager values, lifecycle and security hooks (same server owner, serial with 4/6/7) + +**Modify:** `OtOpcUaNodeManager.cs`, `SdkAddressSpaceSink.cs`, `SdkServiceLevelPublisher.cs`, `AdvertisedAggregateFunctions.cs`, `HistoryPaging.cs` only for SDK-facing capability code. Do not refactor composer/planner domain logic during mechanical migration. + +**Tests:** `NodeManagerAclEnforcementTests.cs`, `NodeManagerStage6EnforcementTests.cs`, `NodeManagerBatchWriteTests.cs`, `NodeManagerWriteRevertTests.cs`, `NodeManagerSurgicalRemoveTests.cs`, `NodeManagerStaleMonitoredNodeTests.cs`, `SdkServiceLevelPublisherTests.cs`, server integration `ArrayWriteWireTests.cs`, `AclSubscriptionEnforcementTests.cs`, `SubscriptionSurvivalTests.cs`, `DualNamespaceAddressSpaceTests.cs`. + +Steps: port actual override and callback signatures, generic variable state use and immutable result collections; preserve per-item error indexes and realm keys; check removed Dispose/Clone/FindChild/lock members only at actual call sites; establish lock order and collection snapshot lifetime; verify create/delete/rebuild/shutdown and subscription detach/reattach against SDK internalization changes. Retain ACL decision cache invalidation and key checks outside grant-cache hits. + +Write tests must distinguish protocol prevalidation from application denial: wrong type/rank rejected before driver callback; current scalar/array and IndexRange behavior; status/timestamp access bits and caller rights; denied writes never reach driver; batch partial failure retains result alignment; downstream driver failure corrects both realms. Do not broaden AccessLevel to keep invalid clients working. Check sampling defaults, array dimensions, Historizing, UserAccessLevel and capability metadata even if compilation succeeds. + +**Run:** same full server unit project, then named wire tests as part of Task 11. **Commit:** `refactor: migrate custom node manager to SDK 2.0`. + +### Task 6 — Production history continuation and service migration (same server owner) + +**Modify:** `HistoryContinuationStore.cs`, SDK-facing portions of `OtOpcUaNodeManager.cs`, `ReverseRawReader.cs`, `ReverseEventsReader.cs`, `HistoryPaging.cs` only as required; preserve pure state types. + +**Create:** `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/SessionHistoryContinuationLifecycleTests.cs` and unit `SessionHistoryContinuationStoreTests.cs`. + +Steps: write lifecycle tests using actual session-backed production store; implement disposable SDK wrapper around immutable domain state (Id belongs to wrapper; no-op Dispose allowed only if no owned resources, otherwise dispose exactly once); port save/restore/release; port raw/modified/processed/event history request/result/ExtensionObject/ByteString conversions; propagate cancellation/deadline and dispose consumed point resources after use. Do not substitute the in-memory test store in integration fixtures. + +Cases: multiple real pages with no loss/duplicates in ascending/descending order and timestamp ties; reuse of consumed token; malformed bytes; cross-session and wrong-node/query token; explicit release; capacity eviction; session close; manager rebuild/invalidated node; key/grant revoked between pages; disconnect/cancel mid-read; cleanup following backend failure. Raw/modified/events CP, empty pages and bounds, aggregates, timestamps and per-item errors remain correct. Set cap small in fixture to make eviction deterministic and prove saved-state disposal where applicable. + +**Run:** full server unit project plus production session lifecycle wire tests. Existing `EventHistoryPagingOverTheWireTests.cs` already exercises the production session store; extend it where possible rather than duplicate coverage. Existing in-memory paging tests remain necessary but insufficient. **Commit:** `fix: migrate session-backed history continuation ownership`. + +### Task 7 — Alarms, event ACLs and model changes (same server owner) + +**Modify:** `OtOpcUaNodeManager.cs`, `NativeAlarmAck.cs`, SDK-facing condition method dispatch; `ServerAggregateConfiguration.cs` / `AggregateFunctionMap.cs` only if target capability APIs require it. + +**Tests:** server unit `NodeManagerMultiNotifierAlarmTests.cs`, `NodeManagerModelChangeFilterTests.cs`, `HistoryServerCapabilitiesAdvertisementTests.cs`; integration `NativeAlarmMultiNotifierEventDeliveryTests.cs`, `NativeAlarmEventIdentityFieldDeliveryTests.cs`, `AclEventSubscriptionEnforcementTests.cs`, `AclServerObjectModelChangeTests.cs`, `ConditionMethodEventTests.cs`, `ClientAlarmMethodTests.cs`, `AdvertisedAggregateFunctionsWireTests.cs`. + +Steps: prevent automatic unfiltered model events; port EventId ByteString and Variant argument/results; preserve ConditionId/EventId identities, SourceNode/SourceName/ConditionName, one Server copy, direct notifier fan-out and ConditionRefresh; check SDK changed Retain/latched/silenced/out-of-service behavior against actually populated optional state nodes; preserve manual event guard and audit policy; assert advertised history/aggregate capabilities match served backends (2.0 no longer masks missing registration). SDK 2.0.1 changes AggregateManager TreatUncertainAsBad default to true while `ServerAggregateConfiguration.cs` promises false: explicitly set the application value in the runtime manager and advertised capabilities; retain app semantics in historian SDK-oracle tests. Extend existing `NodeManagerHistoryReadTests.cs` default assertion and `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests/SdkAggregateParityTests.cs` with uncertain-value fixtures; never update expected values merely to match the new SDK default. + +Call checks require permission on both object and method. Test narrow AlarmAck grant allow without global roles, wrong condition/EventId deny, expired key, ack/confirm/shelve/unshelve, refreshed condition privacy, two distinct ACL audiences, broad BaseEventType filter and model-change duplicate/leak checks. Null content-filter semantics and partial select-clause errors must be checked by field index, never assumed to behave as before. + +**Run:** server unit/integration projects. **Commit:** `fix: preserve alarm and event authorization under SDK 2.0`. + +### Task 8 — Shared client, CLI and UI (client owner; parallel with server lane after G1/Task 3 contract) + +**Modify:** `src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/{DefaultApplicationConfigurationFactory,DefaultEndpointDiscovery,DefaultSessionFactory,DefaultSessionAdapter,DefaultSubscriptionAdapter}.cs`, their interfaces as needed, `OpcUaClientService.cs`, `Helpers/{ValueConverter,HistoryReadPaging}.cs`; CLI `Commands/*.cs`, `Helpers/NodeIdParser.cs`; UI SDK-consuming code found in Task 1. + +**Tests:** all three `tests/Client/ZB.MOM.WW.OtOpcUa.Client.{Shared,CLI,UI}.Tests/` projects. Add client tests beside existing adapter/service tests for target conversions and shutdown races. + +Steps: port cert manager and identity factory; port request/results and subscription APIs only where necessary; preserve existing explicit factory/session abstraction and failover ownership; test caller cancellation, browse/history CP release and per-item diagnostics; refresh namespace mappings on new session; drop late notifications from old generations; preserve alarm method input ordering and historian status handling. No ManagedSession swap unless supported classic lifecycle cannot meet parity, in which case return to design gate with an explicit replacement state machine and tests before implementation. + +Cases: invalid encoded NodeId rejected vs literal string id preserved; opaque ids; empty LocalizedText rendering; arrays display/write round-trip; subscription delete while reconnecting; connect twice; failover racing disconnect; disposal exactly once; callback after disposal ignored; methods/history outputs preserved. + +**Run:** `opc_test tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests`, then corresponding CLI.Tests and UI.Tests paths. **Commit:** `refactor: migrate client applications to OPC UA SDK 2.0`. + +### Task 9 — OPC UA driver, session and browser (driver owner; parallel with server/client) + +**Modify:** `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/{OpcUaClientDriver,OpcUaClientDriverProbe,SdkTelemetry}.cs`, `.Session/{NamespaceMap,OpcUaUserIdentityFactory,UpstreamAttributeMapping}.cs`, `.Browser/{OpcUaClientDriverBrowser,OpcUaClientBrowseSession}.cs`. Keep `.Contracts` SDK-free. + +**Tests:** driver unit and browser unit projects, especially `OpcUaClient{Namespace,LifecycleGeneration,StaleSessionRace,ContinuationPointRelease,CertAuth,SecurityPolicy,ReadWrite,SubscribeCleanup,ReinitializeReleasesSession}Tests.cs`; server wire `OpcUaClientDriver{Wire,ReconnectWire,StallWire}Tests.cs` owned by integration coordinator to avoid overlapping edits. + +Steps: adapt identity/certificate/session construction, ByteString/array values and service signatures; update opaque NodeId handling and namespace URI binding; preserve bounded browse, CP release, stall detection, reconnect gate and generation ownership. Test cold connect, namespace reorder, lost session, no-response peer, idempotent vs non-idempotent writes, partial subscription create, cancellation at every ownership transfer, secret rotation and reinitialize. Never log credentials or auto-downgrade security on connect failure. + +**Run:** `opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests` and Browser.Tests; integration projects in Task 11. **Commit:** `refactor: migrate OPC UA driver and browser to SDK 2.0`. + +### Task 10 — Integrate lanes and remove migration scaffolding (coordinator, serial) + +**Modify:** only unresolved shared package/build configuration and SDK-using tests outside lanes, including historian SQL/QuestDB and Core.Abstractions test oracles identified by inventory. Reconcile solution-wide source-gen and mock API changes. + +Steps: merge lane commits in dependency order; restore/build whole solution; resolve conflicts by reading behavioral tests, never blind accepting; audit all generated model boolean defaults, removed serializers and encodeable factory calls at real usage sites; eliminate Collection shims, obsolete boxing and unnecessary object conversions; remove temporary analyzer references unless retained deliberately as development-only policy; ensure no analyzer runtime artifact or migration-only warning suppression ships. Run `dotnet list ... package --include-transitive` on host, CLI, UI and driver consumers and examine publish output for duplicate SDK assemblies. + +**Gate G2:** Compilation convergence is complete across all lane dependency graphs; run deferred owning-project focused/full tests now, before cross-stack wire verification. Whole solution Release build passes under original production warning policy; no unresolved migration diagnostics/shims or unreviewed new warnings; every inventory consumer has compiled. **Commit:** `build: complete SDK 2.0 integration and remove migration scaffolding`. + +### Task 11 — Compatibility, security and backend gates (coordinator, serial shared rigs) + +**Create:** `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/Sdk2CompatibilityContractTests.cs` for same-process contract checks only; cross-version execution uses separate processes/artifacts to avoid loading two SDK major versions in one process. Create `scripts/e2e/test-opcua-sdk-compatibility.ps1` using repository conventions, extending `scripts/e2e/test-opcuaclient.ps1` only when appropriate. Record exact artifact hashes, endpoint policies and results in evidence. + +Run server unit/integration, all client unit, driver/browser unit/integration projects. Use `docs/drivers/OpcUaClient-Test-Fixture.md` and `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/README.md` for the fixture; set `OPCUA_SIM_ENDPOINT` explicitly to a reserved reachable simulator endpoint. The existing opc-plc setup advertises unsecured transport and autoaccepts certs, so it is only one interoperability leg: trusted secure-channel coverage requires a separate configured fixture. opc-plc uses the Foundation stack internally; do not call it an independent-stack test. No history capability is added to the OPC UA driver; historical-access client tests run through Client.Shared/CLI. Project command example: + +```bash +opc_test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests +opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests +opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests +``` + +Matrix: for OtOpcUa server rows run connect, trusted SignAndEncrypt + API key, browse raw/UNS by URI, read/write scalar and array, monitor quality/timestamps, method ack, event filtering and history CP paging using clients that implement those operations. For ScadaBridge, exercise its actual consumed subset and use Client.Shared/CLI for the remaining server contract. External simulator/browser rows exercise only supported upstream capabilities: connect/discovery, browse, read, subscribe, configured writes and reconnect; no OtOpcUa-specific raw/UNS, API-key, history or ack requirement is imposed on them. Mark a capability N/A only with recorded implementation evidence, never use N/A to excuse a supported failing operation: + +| Client | Server | Purpose | +|---|---|---| +| Baseline 1.5.378 client artifact | Candidate 2.0.1 server | Existing consumers survive server-first rollout | +| Candidate 2.0.1 client and driver | Baseline 1.5.378 server artifact | Client/driver rollout and rollback compatibility | +| Candidate 2.0.1 | Candidate 2.0.1 | Native target behavior and full application tests | +| Current ScadaBridge OPC UA adapter | Candidate server | Real downstream wire/auth contract; external process retains its own SDK | +| Candidate driver/browser | Existing external simulator/server | Real upstream compatibility beyond our own stack | + +Run all existing endpoint policies, anonymous policy cases and ACL flag states as appropriate; do not rely on None/autoaccepted certificates only. Validate token renewal and expiry, reauthentication and server restart. Verify healthy/standby ServiceLevel (250/240), active-node health behavior and pair failover with reconnect/namespace rebinding. Mixed SDK cluster operation is permitted only after this application's existing Akka serialization and deployment contracts are checked; SDK wire interoperability alone does not prove cluster rolling-upgrade safety. + +Historical backend matrix: SqlServer, QuestDB and Gateway where configured; raw, modified, processed aggregates, events, update errors and unsupported-capability metadata. Follow `.claude/rules/historian.md`; do not enable deferred native gateway aggregates or call missing backend coverage passed. Coordinate real AVEVA/ScadaBridge gates using approved environments. If inaccessible, mark that gate pending with owner and exact reproduction instructions, never complete #782 or production rollout. + +Set `OPC_RUN_DIR` to a fresh nonexistent directory before this solution command; its helper creates it once and preserves both test and result-count failures; do not combine its TRX with any other gate. Also run the repository's full CI-equivalent solution test command after merging because the required Microsoft.Extensions uplift affects consumers outside OPC UA: + +```bash +opc_solution_test() { + : "${OPC_RUN_DIR:?Set a fresh solution run directory}" + if [ -e "$OPC_RUN_DIR" ]; then return 2; fi + mkdir -p "$OPC_RUN_DIR" || return + local opc_solution_status=0 + dotnet test ZB.MOM.WW.OtOpcUa.slnx -c Release --no-build \ + -m:1 -p:TestTfmsInParallel=false --settings scripts/ci/ci.runsettings \ + --filter 'Category!=E2E&Category!=LiveIntegration' \ + --logger trx --results-directory "$OPC_RUN_DIR" || opc_solution_status=$? + local opc_solution_gate_status=0 + MIN_EXECUTED=12000 bash scripts/ci/assert-not-all-skipped.sh "$OPC_RUN_DIR"/*.trx \ + || opc_solution_gate_status=$? + if [ "$opc_solution_status" -ne 0 ]; then return "$opc_solution_status"; fi + return "$opc_solution_gate_status" +} +opc_solution_test +``` + +Keep the current CI floor or raise it based on Task 1 totals; never lower it to hide regressions. Fixture-backed required gates remain separate from this fixtureless CI leg. Run the existing `python3 code-reviews/regen-readme.py --check` only if review artifacts changed; do not rewrite unrelated review findings for this migration. + +**Gate G3:** Mandatory cases execute and pass, no skipped substitute; expected stricter protocol behavior documented and consumer impact resolved. **Commit:** `test: prove SDK 2.0 wire and security compatibility`. + +### Task 12 — Performance, lifecycle and publish validation (coordinator) + +**Read/extend:** `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerLockTimeBenchmarkTests.cs`; existing benchmark/load commands discovered in baseline; add focused SDK-facing load harness only if no existing harness covers required behavior. Do not turn noisy timing tests into ordinary unit CI gates. + +Compare baseline and candidate on identical hardware/config/tag counts, sampling intervals, security and active clients; warm up, measure at least three runs of representative scalar+array loads. Capture throughput, p50/p95/p99 read/write/notification latency, allocated bytes, GC and steady RSS, queue depth, reconnect duration, history page throughput and node-manager lock hold time. Use production-representative driver publish fan-out to both realms. Provisional gates: ≤10% median throughput regression, ≤15% p95 latency or allocation/RSS growth versus baseline; exceedance requires investigation and a recorded acceptance decision, not silent threshold adjustment. These are project acceptance budgets, not upstream promises. + +Soak for at least two hours with repeated connect/disconnect/reinitialize, browse abandonment, CP churn, node surgical edits/rebuild, revocation and network stalls. Require no monotonic retained sessions/items/points/cert-store handles, no stalled shutdown and no unbounded growth. Follow with Windows and Linux publish/install smoke using repository-supported RID/framework settings; inspect transitive assemblies and start/stop twice with existing PKI directory. Verify logs redact keys, passwords, certificates/private material and authorization failures stay diagnosable. Do not attempt Native AOT. + +**Gate G4:** Performance budgets met or reviewed deviations resolved; resource counts return to steady baseline; all supported publish artifacts run. **Commit:** `test: validate SDK 2.0 lifecycle and release artifacts`. + +### Task 13 — Documentation, deployment and rollback runbook (coordinator) + +**Modify:** `docs/security.md`, `docs/drivers/OpcUaClient.md` and `docs/drivers/OpcUaClient-Test-Fixture.md`, `docs/Historian.md`, `CLAUDE.md` SDK facts only where needed. Create `docs/plans/2026-11-10-opcua-sdk-2-rollout.md` containing artifact hashes, preflight, operator commands for the approved environment, stop/go checks, rollback and results. Document intentional protocol status/locale/filter changes and no new companion/PubSub capability claims. + +Preflight: archive known-good binaries/config and PKI with permissions, verify private keys/trust/issuer chains and CA usage, capacity settings and client versions; stage candidate separately. No planned ConfigDb migration; if implementation introduces persistence changes, design gate reopens and rollback must include its data strategy. Do not automatically rotate application certificates or rewrite persisted sessions/subscriptions. Capture actual schema/serialization effects rather than assuming none. + +Rollout sequence: isolated test rig → ScadaBridge/upstream interoperability → DEV canary → QA pair → production canary. Restart order for a pair is conditional on successful mixed-version cluster gate: standby first, verify health/security/data/history/events and failover, then peer. Otherwise use a documented maintenance window and coordinated pair restart. Monitor reconnects, stale values, quality, denied legitimate calls, leaks, CP failures and latency. Stop for any authorization regression, missing/duplicate alarm or history data, incompatible identifiers, insecure downgrade, sustained budget miss or restart failure. + +Rollback: stop candidate on approved channel, restore complete known-good artifact/config and preserved PKI, restart, verify thumbprints, endpoint/namespace identity, active/standby state, subscriptions, API-key writes/ack and history. Force client reconnect if needed; in-memory sessions/continuation points do not survive process rollback, and non-idempotent writes are never replayed. Revert package/application commits together if rolling source back. Preserve candidate logs without secrets. + +Repository remote-access hold remains binding: no WinRM/psbridge/replacement remote-exec on wonder boxes vd03/vq03/vq04 without a cleared channel. UNC/SMB and permitted SQL reads do not authorize remote service commands. Required operator steps stay pending until actually performed; do not bypass the hold. Execution may complete software tasks while the deploy gate remains open. + +When implementation is complete, append consumer-impact detail to `../scadaproj/.claude/skills/sister-project-history/otopcua.md` using that repository's instructions; update the umbrella summary only if its facts changed. Keep Gitea the single status system: update #782 with commit, artifacts, gates and deployment issue references only during authorized execution; file follow-ups using required type/priority/cross-repo labels. Planning does not change issue state. + +**Commit:** `docs: document OPC UA 2.0 compatibility and rollout`. + +## Safe parallel-agent execution instructions + +At most four active agents total (coordinator + three workers). These recommendations are explicit instructions for future execution; they do not authorize execution in this planning session. The user has requested parallel-agent planning and Astra review. + +| Role | Model / effort | Exclusive ownership | Dependencies | +|---|---|---|---| +| Coordinator/integrator | `gpt-6.1-sol`, high; xhigh for contested cross-layer design | package/build files, evidence/design/runbook, integration harness, shared rigs, merge/review | Tasks 1–3 serial, then G1 | +| Server/security worker | `gpt-6.1-sol`, xhigh | entire OpcUaServer production project, host OpcUa integration, server unit tests; Tasks 4–7 sequential (integration tests authored by coordinator) | Frozen boundary contract and G1 | +| Client worker | `gpt-6.1-sol`, high | `src/Client/**`, `tests/Client/**`; Task 8 | Same contract and G1 | +| Driver worker | `gpt-6.1-sol`, high | three SDK-facing OpcUaClient driver/session/browser projects and their unit tests; Task 9 | Same contract and G1 | +| Independent reviewer | `gpt-6-astra`, high; xhigh for security/ownership concerns | Read-only review; never edit implementation | Free a worker slot before spawning review | + +All integration test additions/edits listed in Tasks 4–9, and Task 7 historian `SdkAggregateParityTests.cs` edits outside the server unit project, including `PeerAddressAuthenticationWireTests.cs` and `SessionHistoryContinuationLifecycleTests.cs`, are authored and run by the coordinator in a serial integration lane. Server/client/driver workers supply required scenarios and adapter contracts, and own only their listed production/unit paths. The coordinator can write those tests while production workers operate disjoint paths, but runs them only after dependencies compile. + +Do not split OtOpcUaNodeManager.cs among agents, even by conceptual history/alarm/security sections: it is a shared 7k+ line file with common callbacks, locks and state. Do not run server tasks 4–7 in parallel. Do not let workers edit central package files, public shared contracts, server wire tests, generated outputs, common fixtures or sister repos. A worker needing such a change sends a concrete request to coordinator and continues independent work; coordinator serializes it and refreshes all workers' base before dependent work resumes. + +Default: suitable isolated worktrees via `create_worktree` tool, each branched from G1+contract commit; a shared directory is permitted only with these strict disjoint ownership boundaries and coordinator-controlled builds/commits. Never reset/clean another worker's changes. Every worker task message includes exact base SHA, owned paths, tests, the design invariants above, forbidden scope, expected deliverables and instruction to report blockers with API evidence. Return commit SHAs (or precise diff if shared tree), tests executed/counts, remaining diagnostics and unresolved risks. No worker may deploy, update issue status, bump target versions or select a new architecture alone. + +Use Sol medium only for deterministic documentation/link inventory after design is settled; use high/xhigh for lifecycle, security, immutable values and concurrency. Luna/low effort is inappropriate for production migration or security review. Use Astra read-only reviews after design, after security/history integration and after all gates; reviewers must inspect actual diffs and negative **and positive** authorization tests, not simply test summaries. If a specified model is unavailable, report that and keep the review pending rather than silently substituting. + +Merge order: server compile primitives and boundary decisions → client and driver commits → remaining server history/event behavior → full solution integration. All workers must compile their own dependency graph; shared-base compile blockers owned by another lane are recorded, not worked around with stubs. Coordinator owns true cross-stack tests after merges. Separate worktrees may run unit builds concurrently; the docker-dev rig, external simulator sessions and mutable PKI/config stores need distinct fixtures or a serial reservation. Never run baseline and candidate against the same ports/store concurrently. + +## Review gates and completion checklist + +- **Design review:** Astra checks source/tag accuracy, baseline drift, architecture decisions, dependency graph, safe ownership and acceptance criteria before implementation begins. +- **Security/ownership review:** Astra checks identity fallback order, peer evidence provenance/null handling, certificate ownership, CP production lifecycle, permission composition, event information leakage and reconnect replay rules after Tasks 4–9. +- **Release review:** Astra checks integrated diff, compatibility matrix execution, performance evidence, publish artifacts and rollback; coordinator resolves every actionable finding and reruns affected checks. +- **Software complete:** exact versions aligned, solution builds, mandatory unit/integration/interoperability/security/resource gates execute successfully, no unsafe shims/suppressions, docs/runbook updated. +- **Issue complete:** software complete plus every live/deployment gate required by #782 actually satisfied, or remaining deployment work formally separated into linked issues with accurate scope/status. An inaccessible live environment is pending, never a passing result. + +No implementation action in this document has been executed as part of preparing the plan. + + +## Planning review record + +On 2026-11-10 an independent `gpt-6-astra` agent at high reasoning effort reviewed the research and full draft read-only. It judged the architecture and migration coverage sound, conditional on four P2 execution corrections: fresh SHA/run/project test evidence with preserved exit status; explicit compilation convergence before claiming behavior checks pass; coordinator ownership of integration and historian oracle edits; and capability-specific compatibility requirements. These corrections are incorporated above. Astra reread them and confirmed closure, identifying one final standalone solution-command exit-status issue; that command now uses the same captured-status and fresh-directory pattern as the reviewed project helper. After that correction, Astra confirmed: "Approved as an implementation plan." No actionable review findings remained; implementation and release validation remain pending. The reviewer also supplied the package dependency-floor, aggregate-default, production continuation-store, identity fallthrough and Variant normalization findings reflected in the design. This is a plan review, not implementation or release approval. No application code or package configuration was changed and no restore/build/test/deploy was run.