cf03ca279d
- 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.
229 lines
15 KiB
Markdown
229 lines
15 KiB
Markdown
# v3 Batch 1 — greenfield schema + RawPath identity rewiring
|
|
|
|
**Branch:** `v3/batch1-schema-identity`
|
|
**Design authority:** `2026-07-15-raw-uns-two-subtree-v3-design.md` — §"v3 identity contract (RawPath)", §"Data model", §"Build plan" row 1.
|
|
**Master plan:** `2026-07-15-v3-implementation-plan.md` (conventions + gotchas are binding).
|
|
**End state:** the config schema and every identity seam speak RawPath; the solution builds and all tests pass; the address space composes hierarchy-only/empty (**deliberately dark until Batch 4**); docker-dev re-seeds and boots.
|
|
|
|
## Scope guard
|
|
|
|
- NO AdminUI feature work (Batch 2). AdminUI changes here are limited to *keeping it
|
|
compiling* against the new entity shapes (pages that authored the old shapes may be
|
|
temporarily stubbed with an "unavailable until Batch 2" banner — record each stub in the
|
|
PR description; Batch 2 owns their replacement).
|
|
- NO OPC UA node materialization changes beyond making the composer emit an empty/
|
|
hierarchy-only composition (Batch 4 owns the dual namespace).
|
|
- Greenfield means greenfield: no data-migration code, no back-compat shims for old
|
|
`TagConfig` shapes, no blob-fallback retention "just in case."
|
|
|
|
## The RawPath contract (what every package codes against)
|
|
|
|
`RawPath = <Folder/…>/<DriverName>/<DeviceName>/<TagGroup/…>/<TagName>` — cluster-scoped,
|
|
slash-separated, built ONLY by the new `RawPaths` helper (never string-concatenated ad
|
|
hoc). Segments forbid `/` and leading/trailing whitespace (validated at authoring).
|
|
Case-sensitive ordinal comparison everywhere. It keys: driver wire-refs, the
|
|
`DriverHostActor` fan-out/write maps, historian default tagname + mux refs, native-alarm
|
|
`ConditionId`, `ctx.GetTag` paths, and (Batch 4) `ns=Raw` NodeIds.
|
|
|
|
## Work packages
|
|
|
|
### Wave A — foundations (2 agents, parallel, disjoint files)
|
|
|
|
**B1-WP1 — Greenfield EF model + `V3Initial` migration + seeds**
|
|
|
|
Files (all under `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/` unless noted):
|
|
- `Entities/`: new `RawFolder.cs`, `TagGroup.cs`, `UnsTagReference.cs`; reshape
|
|
`DriverInstance.cs` (add nullable `RawFolderId`; **drop `NamespaceId`**), `Device.cs`
|
|
(add `DeviceConfig` JSON; now universal — every driver ≥1 device), `Tag.cs`
|
|
(required `DeviceId` FK, nullable `TagGroupId`; **drop `EquipmentId` + `FolderPath`**;
|
|
**keep `WriteIdempotent`** — dropping it regresses the R2 resilience fix),
|
|
`Equipment.cs` (**drop the `DriverInstanceId`/`DeviceId` binding**). Delete
|
|
`Namespace.cs`, `EquipmentImportBatch.cs` (both classes), and
|
|
`Enums/NamespaceKind.cs`; delete `NamespaceKindCompatibility` + `AllowedNamespaceKinds`
|
|
from `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs`.
|
|
- `OtOpcUaConfigDbContext.cs` — new DbSets; **all** uniqueness/integrity config inline in
|
|
`OnModelCreating` per the design's rules: sibling-name unique indexes at every raw
|
|
level (filtered indexes for nullable `ParentRawFolderId`/`RawFolderId`/`TagGroupId`),
|
|
`OnDelete(Restrict)` so raw-tag delete is blocked while a `UnsTagReference` points at
|
|
it and non-empty `RawFolder`/`TagGroup` deletes are blocked.
|
|
- **Migration strategy (decided): squash.** Delete the existing `Migrations/` folder and
|
|
scaffold a single `V3Initial` migration. Port the hand-written stored-proc migrations'
|
|
`Up` SQL (the `*_StoredProcedures.cs` / `*_ExtendComputeGenerationDiffWith*.cs` files)
|
|
into `V3Initial` verbatim — audit them first: any proc referencing dropped
|
|
columns/tables (`NamespaceId`, `Tag.EquipmentId`, `Tag.FolderPath`) must be rewritten
|
|
to the new shape, and `ComputeGenerationDiff` must diff the new tables
|
|
(`RawFolder`/`Device`/`TagGroup`/`UnsTagReference`). Greenfield = no production DB to
|
|
upgrade, so squashing is safe; the fixture DBs are dropped/recreated.
|
|
- Seeds: rewrite `docker-dev/seed/seed-clusters.sql` (+ anything in
|
|
`docker-dev/seed/entrypoint.sh` that names tables) and every `scripts/smoke/seed-*.sql`
|
|
to the new schema — each driver gets a folder (optional), a device row with
|
|
`DeviceConfig` (endpoint moved out of `DriverConfig` — coordinate the JSON shape with
|
|
B1-WP5's contracts commit), and raw tags with RawPath-consistent names.
|
|
- Tests: `tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/` —
|
|
`SchemaComplianceFixture`/`SchemaComplianceTests` updated to the new schema; new unit
|
|
tests for every integrity rule (delete-while-referenced blocked, non-empty folder/group
|
|
delete blocked, default-device-with-tags delete blocked, sibling-name uniqueness per
|
|
level, `/`-in-name rejected).
|
|
|
|
DoD: migration applies to a fresh DB (`dotnet ef database update` via
|
|
`DesignTimeDbContextFactory`); Configuration project + tests compile and pass (other
|
|
projects may be red until Wave B/C — that's expected mid-batch; the *batch branch* is
|
|
only merged green).
|
|
|
|
**B1-WP2 — `RawPaths` helper + `TagConfigIntent` successor + key normalization**
|
|
|
|
Files:
|
|
- New `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/RawPaths.cs` — pure static: `Build(...)`
|
|
from segment lists, `Validate(segment)` (no `/`, no lead/trail whitespace, non-empty),
|
|
`Combine`, `TryParent`. This is the contracts-commit centerpiece — land it first.
|
|
- `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/TagConfigIntent.cs` — sheds the
|
|
FullName-derivation role: remove `FullName`/`ExplicitFullName` (the blob-as-identity
|
|
convention dies here); **keep** the alarm/historize/array parsing
|
|
(`TagAlarmIntent`, `ParseAlarm`/`ParseHistorize`/`ParseArray`) — those remain real
|
|
`TagConfig` concerns. Callers to sweep (grounded list): `DraftValidator` (Galaxy
|
|
ExplicitFullName rule → deleted; replaced by B1-WP6 rules),
|
|
`Core/OpcUa/EquipmentNodeWalker.cs`, `OpcUaServer/AddressSpaceComposer.cs` +
|
|
`AddressSpaceChangeClassifier.cs`, `AdminUI/ScriptAnalysis/IScriptTagCatalog.cs`,
|
|
`AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs`,
|
|
`Runtime/Drivers/DeploymentArtifact.cs`.
|
|
- Key normalization (greenfield, decided in the design): OpcUaClient's PascalCase
|
|
`"FullName"` → `"nodeId"`; Galaxy's → `"attributeRef"`; the `deviceHostAddress` key
|
|
(AbCip/AbLegacy/TwinCAT/FOCAS) is **dropped** (the tag's `DeviceId` FK owns placement).
|
|
Update `OpcUaClientTagConfigModel.cs` (AdminUI) and the two browsers' commit paths
|
|
(`Driver.OpcUaClient.Browser/OpcUaClientBrowseSession.cs`,
|
|
`Driver.Galaxy.Browser/GalaxyBrowseSession.cs`) to emit the new keys.
|
|
- Tests: rewrite `tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/TagConfigIntentTests.cs`;
|
|
new `RawPathsTests`; delete
|
|
`Configuration.Tests/DraftValidatorGalaxyFullNameCorpusTests.cs` and
|
|
`Core.Tests/OpcUa/EquipmentNodeWalkerFullNameCorpusTests.cs` (superseded — B1-WP6
|
|
replaces them with RawPath-shaped corpus tests).
|
|
|
|
### Wave B — per-driver resolver rework (up to 9 agents, parallel, one per driver + exemplar-first)
|
|
|
|
**B1-WP3 — `EquipmentTagRefResolver` re-keying + blob-fallback retirement**
|
|
|
|
Shared seam first (the wave's contracts commit, done by the coordinator or the exemplar
|
|
agent): `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs` — the
|
|
`_byName` branch becomes the primary and only path: each deploy hands the driver its
|
|
authored raw tags keyed by RawPath (ref string) → `TagConfig`-derived definition. The
|
|
`_cache.GetOrAdd(fullReference, _parseRef)` blob-parse fallback is **deleted**; a miss is
|
|
a miss (`TryResolve` false → driver logs + Bad quality), never a parse attempt.
|
|
|
|
Then **exemplar: Modbus** (one agent, reviewed before fan-out so the pattern is fixed):
|
|
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusEquipmentTagParser.cs` —
|
|
becomes a pure `TagConfig JSON → definition` mapper invoked at deploy-time table build
|
|
(no more "is this ref a blob?" heuristics). Rename to `ModbusTagDefinitionFactory` (or
|
|
similar) to make the retirement visible.
|
|
- Driver + tests updated to resolve by RawPath.
|
|
|
|
Then **fan-out, one agent per driver** (disjoint project directories — fully parallel):
|
|
S7 (`S7EquipmentTagParser.cs` — the leading-`{` blob convention dies), AbCip, AbLegacy,
|
|
TwinCAT, FOCAS (same shape as exemplar); **OpcUaClient** (no static parser — seam is
|
|
`Driver.OpcUaClient.Contracts/NamespaceMap.cs` + `OpcUaClientDriver.cs`; the definition's
|
|
address now reads `TagConfig.nodeId`); **Galaxy** (seam is
|
|
`Driver.Galaxy/Browse/GalaxyDiscoverer.cs` + `Browse/AlarmRefBuilder.cs`; address reads
|
|
`TagConfig.attributeRef`; note `AlarmRefBuilder` feeds `ConditionId` — under v3 that is
|
|
the RawPath, keep both directions consistent).
|
|
|
|
Each driver agent's DoD: driver project + its unit tests green with refs = RawPaths and
|
|
definitions sourced from the authored-tag table; zero references to
|
|
`TagConfigIntent.FullName` remain in the driver.
|
|
|
|
**B1-WP5 — endpoint → `DeviceConfig` move (runs parallel with WP3; owns different files)**
|
|
|
|
- Contracts: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/DeviceConfigIntent.cs` reworked —
|
|
`DeviceConfig` JSON now carries host/endpoint + per-device settings for **all** drivers.
|
|
- The 4 multi-device drivers: remove the embedded `Devices[]` list from
|
|
`ModbusDriverOptions.cs`(*), `S7DriverOptions.cs`(*), `AbCipDriverOptions.cs`,
|
|
`AbLegacyDriverOptions` file, `TwinCATDriverOptions.cs`, `FocasDriverOptions` file —
|
|
(*grounding note: Modbus and S7 options also carry `Devices` lists — treat all six
|
|
options classes the same way: `DriverConfig` keeps protocol/channel-level settings only;
|
|
per-device settings move to `DeviceConfig`). Single-endpoint drivers (OpcUaClient
|
|
`EndpointUrl`, Galaxy gateway address, S7 host if single) move their endpoint into the
|
|
auto-created default device's `DeviceConfig`.
|
|
- Runtime spawning: `Runtime/Drivers/DriverInstanceActor.cs` + the artifact device-host
|
|
map in `Runtime/Drivers/DeploymentArtifact.cs` — driver instantiation now merges
|
|
`DriverConfig` + per-device `DeviceConfig` rows (this merged shape is also what Batch
|
|
2's browse modal passes as `configJson` — get the merge helper into Commons so both
|
|
reuse it: `DriverDeviceConfigMerger`).
|
|
- Golden corpus: rewrite
|
|
`tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/TagConfigGoldenCorpus.cs` +
|
|
`TagConfigCorpusParityTests.cs` to the v3 shapes (no `deviceHostAddress`, no
|
|
PascalCase `FullName`, per-device configs separate).
|
|
|
|
Coordination rule: WP5 and WP3 both touch driver projects. Split ownership per file:
|
|
WP3 owns `*EquipmentTagParser.cs`/resolver/driver read-path files; WP5 owns
|
|
`*DriverOptions.cs`/factory/spawn-path files. Where one file carries both (a driver
|
|
factory that parses options AND builds the tag table), WP5 waits for that driver's WP3
|
|
agent to merge first — the coordinator sequences per-driver.
|
|
|
|
### Wave C — pipeline integration (2 agents, then 1 integrator)
|
|
|
|
**B1-WP4 — composer / artifact / actors / validator / services rewiring**
|
|
|
|
- `OpcUaServer/AddressSpaceComposer.cs` (+ `AddressSpaceChangeClassifier.cs`) — composes
|
|
from the new schema; emits a hierarchy-only/empty composition (folders exist as data;
|
|
**no variable nodes materialize** — Batch 4). Keep the alarm-intent parse (it flows to
|
|
the artifact) but drop equipment-tag NodeId composition.
|
|
- `Runtime/Drivers/DeploymentArtifact.cs` — flatten the new shape: per-driver authored
|
|
raw tags keyed by RawPath (feeding the WP3 resolvers), device rows with merged config
|
|
(WP5), historized set keyed by RawPath (effective historian tagname = override else
|
|
RawPath).
|
|
- `ControlPlane/AdminOperations/ConfigComposer.cs` (`SnapshotAndFlattenAsync`) — snapshot
|
|
the new tables into the sealed blob; `RevisionHash` inputs updated.
|
|
- `Runtime/Drivers/DriverHostActor.cs` — the map keys change meaning, not shape:
|
|
`_nodeIdByDriverRef` / `_driverRefByNodeId` / `_alarmNodeIdByDriverRef` /
|
|
`_driverRefByAlarmNodeId` now carry RawPath as the ref string. Rename the tuple member
|
|
`FullName` → `RawPath` so no stale semantics survive. NodeId sets stay empty until
|
|
Batch 4 (the maps still exist; mux forwarding still runs).
|
|
- `Configuration/Validation/DraftValidator.cs` + `DraftSnapshot(Factory)` — new rules:
|
|
raw-name charset (no `/`, no lead/trail whitespace) at every level; **historized-tag
|
|
effective tagname ≤ 255 chars** (live-verified AVEVA limit) → deploy error prompting a
|
|
`historianTagname` override; retired rules deleted (Galaxy ExplicitFullName, equipment
|
|
NodeId collision in its old form, namespace-binding checks).
|
|
- `AdminUI/Uns/UnsTreeService.cs` + `IUnsTreeService.cs`, `Components/Pages/Uns/*`,
|
|
`Components/Shared/Uns/TagModal.razor` etc. — compile-only pass: the UNS tree keeps
|
|
Area/Line/Equipment; the Tags tab and TagModal driver-binding paths are stubbed behind
|
|
an "unavailable until v3 Batch 2/3" banner (list every stub in the PR).
|
|
- `ControlPlane/AdminOperations/AdminOperationsActor.cs` — per-tag TagConfig validation
|
|
loop updated (no FullName checks; alarm/historize checks retained).
|
|
|
|
**B1-WP6 — test-suite + harness sweep**
|
|
|
|
- Rewrite/replace the corpus tests deleted in WP2 with RawPath equivalents (resolution
|
|
per driver, artifact parity: `DeploymentArtifact*ParityTests.cs`).
|
|
- `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActor*Tests.cs`,
|
|
`DriverInstanceActor*Tests.cs`, `VirtualTags/*Tests.cs` — re-key refs to RawPaths.
|
|
- 2-node harness `tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/` — seed shapes
|
|
updated (`EquipmentNamespaceMaterializationTests.cs` will assert the *dark* address
|
|
space this batch: hierarchy nodes only / no variables; mark the full assertion as
|
|
Batch-4-pending with a skip-reason constant, don't delete it).
|
|
- `tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/` — ConfigComposer/deploy-path
|
|
shapes.
|
|
|
|
## Wave schedule + parallelization
|
|
|
|
| Wave | Packages | Agents | Parallel-safe because |
|
|
|---|---|---|---|
|
|
| A | WP1 ∥ WP2 | 2 | Disjoint: Configuration project vs Commons/driver-editor files. Coordinator lands `RawPaths.cs` + reshaped-entity contracts commit first |
|
|
| B | WP3 exemplar (Modbus) → review → WP3 fan-out (7 drivers) ∥ WP5 | up to 8 | Per-driver project directories are disjoint; WP3/WP5 per-file ownership rule above |
|
|
| C | WP4 → WP6 | 2 sequential (WP6 can start its mechanical re-keying while WP4 finishes, in a separate worktree, rebasing before merge) | WP4 is the single integration point — one agent only |
|
|
|
|
All agents in worktrees; merge order A → B → C; coordinator builds+tests the batch branch
|
|
after every merge.
|
|
|
|
## Batch verification gate
|
|
|
|
1. `dotnet build ZB.MOM.WW.OtOpcUa.slnx` and `dotnet test ZB.MOM.WW.OtOpcUa.slnx` green
|
|
(macOS-safe suites), plus the DB-backed `Configuration.Tests` against
|
|
`10.100.0.35,14330`.
|
|
2. Fresh-DB migration proof: point `OTOPCUA_CONFIG_CONNECTION` at a scratch DB,
|
|
`dotnet ef database update`, then run `SchemaComplianceTests`.
|
|
3. docker-dev: rebuild the migrator image + **both** central nodes, re-seed
|
|
(`docker-dev/seed/`), `docker compose up -d`; both nodes reach Running; seeded drivers
|
|
show Connected against the `10.100.0.35` fixtures (`lmxopcua-fix up modbus standard`
|
|
etc.); AdminUI loads (stubbed pages show their banners, nothing crashes).
|
|
4. Explicitly assert the dark address space: Client.CLI
|
|
`browse -u opc.tcp://localhost:4840 -r -d 3` shows hierarchy/no tag variables — and
|
|
the PR description says so, so nobody files it as a bug.
|