v3 Batch 1 — greenfield schema + RawPath identity rewiring #469

Merged
dohertj2 merged 39 commits from v3/batch1-schema-identity into master 2026-07-16 01:52:52 -04:00
Owner

v3 Batch 1 — greenfield schema + RawPath identity rewiring

Branch: v3/batch1-schema-identitymaster
Plan: docs/plans/2026-07-15-v3-batch1-schema-identity-plan.md · Design: docs/plans/2026-07-15-raw-uns-two-subtree-v3-design.md

Greenfield, compatibility-breaking. The config schema and every identity seam now speak RawPath
(<Folder/…>/<Driver>/<Device>/<TagGroup/…>/<Tag>). The full solution builds and all macOS-safe
tests pass; the OPC UA address space is deliberately dark (drivers connect, no tag variables
materialize) until Batch 4.

What changed

  • Schema (greenfield): new RawFolder / TagGroup / UnsTagReference; reshaped DriverInstance
    (−NamespaceId, +RawFolderId?), Tag (raw-only: req DeviceId, +TagGroupId?,
    DriverInstanceId/EquipmentId/FolderPath, kept WriteIdempotent), Equipment (−driver/device
    binding); Namespace / NamespaceKind / EquipmentImportBatch retired. Single squashed V3Initial
    migration (stored procs audited — the vestigial generation-model procs were rewritten to v3-valid
    stubs; sp_ComputeGenerationDiff diffs the new tables).
  • Identity: new Commons/Types/RawPaths.cs helper; TagConfigIntent sheds FullName;
    EquipmentTagRefResolver<TDef> is byName(RawPath)-only (blob-fallback deleted); new
    RawTagEntry(RawPath, TagConfig, WriteIdempotent, DeviceName) delivery record. All 8 drivers
    re-keyed: the per-driver *EquipmentTagParser became *TagDefinitionFactory.FromTagConfig, options
    TagsRawTags, TagConfig key normalization (nodeId / attributeRef; deviceHostAddress
    dropped).
  • Endpoint → DeviceConfig: new DriverDeviceConfigMerger (Commons) merges DriverConfig + per-device
    DeviceConfig; multi-device drivers reconstruct their Devices[] from Device rows; the artifact injects
    the merged config + rawTags at spawn.
  • Pipeline: ConfigComposer snapshots the new tables; DeploymentArtifact flattens per-driver
    RawTagEntry lists keyed by RawPath (via RawPathResolver) + emits an empty (dark) equipment-tag
    plan set; AddressSpaceComposer/walker compose folders + VirtualTags + ScriptedAlarms only;
    DriverHostActor maps re-keyed to RawPath; DraftValidator gains RawNameInvalid /
    HistorianTagnameTooLong / UnsEffectiveNameCollision.
  • AdminUI (compile-only stubs): Namespace pages → retired banners; driver pages dropped the
    pre-declared Tags editor + NamespaceId; UnsTreeService tag-authoring returns "moved to the Raw tree
    (/raw) in v3 Batch 2". (Batch 2/3 build the real /raw + reference UI.)

Verification gate (all four items)

  1. Build + test green. Full solution builds (0 errors). macOS-safe unit suites: Commons 196,
    Core 260, Core.Abstractions 134, Configuration 107, Runtime 355 (42 dark-Batch-4 skips),
    ControlPlane 78, OpcUaServer 335 (4 skips), AdminUI 507 (3 skips), + all 8 driver unit suites
    (Modbus 315, S7 264, AbCip 342, AbLegacy 213, TwinCAT 192, FOCAS 272, OpcUaClient 138, Galaxy 313)
    and the 6 driver IntegrationTests projects compile. Enum-serialization round-trip coverage preserved.
  2. Fresh-DB migration. V3Initial applies clean to a scratch DB; SchemaComplianceTests 8/8.
  3. docker-dev boot (live). Volume wiped → V3Initial applied → both central nodes reached Akka
    Up → a seeded Modbus driver spawned and connected to the 10.100.0.35:5020 fixture
    (DriverInstance MAIN-modbus: connected), deployment sealed (acks=2); AdminUI HTTP 200 on
    /, /clusters, /uns with 0 unhandled exceptions.
  4. Dark address space (Client.CLI browse). Recursive browse of opc.tcp://localhost:4840: the raw
    tag HR200 and the driver have 0 occurrences; the custom namespace ns=2 holds only the empty
    OtOpcUa root — no authored variables materialize (Batch 4 lights them up).

AdminUI Batch-1 stubs (Batch 2/3 replace)

Namespace pages (NamespaceEdit, ClusterNamespaces) → banners; the 8 driver pages' pre-declared Tags
editors + Namespace bindings removed; DriverIdentitySection Namespace dropdown removed;
UnsTreeService.CreateTagAsync/UpdateTagAsync return the "moved to /raw" failure; per-equipment tag
lists empty.

Deferred (owned by later batches, not regressions)

  • /raw project tree + reference-picker UI, dual ns=Raw/ns=UNS namespaces + tag-variable
    materialization, {{equip}} reference-relative resolution — Batches 2–4 (address space intentionally
    dark now).
  • User-facing docs beyond the Configuration.md v3 note (they describe the not-yet-built UI/address
    space) — updated as Batches 2–4 land.
  • Live driver IntegrationTests + one pre-existing AbCip_Green_AgainstSim fixture-probe failure
    (AB CIP sim handshake — environment/fixture gate, untouched).
# v3 Batch 1 — greenfield schema + RawPath identity rewiring **Branch:** `v3/batch1-schema-identity` → `master` **Plan:** `docs/plans/2026-07-15-v3-batch1-schema-identity-plan.md` · **Design:** `docs/plans/2026-07-15-raw-uns-two-subtree-v3-design.md` Greenfield, compatibility-breaking. The config schema and every identity seam now speak **RawPath** (`<Folder/…>/<Driver>/<Device>/<TagGroup/…>/<Tag>`). The full solution builds and all macOS-safe tests pass; the OPC UA address space is **deliberately dark** (drivers connect, no tag variables materialize) until Batch 4. ## What changed - **Schema (greenfield):** new `RawFolder` / `TagGroup` / `UnsTagReference`; reshaped `DriverInstance` (−`NamespaceId`, +`RawFolderId?`), `Tag` (raw-only: req `DeviceId`, +`TagGroupId?`, −`DriverInstanceId`/`EquipmentId`/`FolderPath`, kept `WriteIdempotent`), `Equipment` (−driver/device binding); `Namespace` / `NamespaceKind` / `EquipmentImportBatch` retired. Single squashed `V3Initial` migration (stored procs audited — the vestigial generation-model procs were rewritten to v3-valid stubs; `sp_ComputeGenerationDiff` diffs the new tables). - **Identity:** new `Commons/Types/RawPaths.cs` helper; `TagConfigIntent` sheds `FullName`; `EquipmentTagRefResolver<TDef>` is byName(RawPath)-only (blob-fallback deleted); new `RawTagEntry(RawPath, TagConfig, WriteIdempotent, DeviceName)` delivery record. All 8 drivers re-keyed: the per-driver `*EquipmentTagParser` became `*TagDefinitionFactory.FromTagConfig`, options `Tags` → `RawTags`, `TagConfig` key normalization (`nodeId` / `attributeRef`; `deviceHostAddress` dropped). - **Endpoint → DeviceConfig:** new `DriverDeviceConfigMerger` (Commons) merges DriverConfig + per-device DeviceConfig; multi-device drivers reconstruct their `Devices[]` from Device rows; the artifact injects the merged config + `rawTags` at spawn. - **Pipeline:** `ConfigComposer` snapshots the new tables; `DeploymentArtifact` flattens per-driver `RawTagEntry` lists keyed by RawPath (via `RawPathResolver`) + emits an empty (dark) equipment-tag plan set; `AddressSpaceComposer`/walker compose folders + VirtualTags + ScriptedAlarms only; `DriverHostActor` maps re-keyed to RawPath; `DraftValidator` gains `RawNameInvalid` / `HistorianTagnameTooLong` / `UnsEffectiveNameCollision`. - **AdminUI (compile-only stubs):** Namespace pages → retired banners; driver pages dropped the pre-declared Tags editor + NamespaceId; `UnsTreeService` tag-authoring returns "moved to the Raw tree (/raw) in v3 Batch 2". (Batch 2/3 build the real `/raw` + reference UI.) ## Verification gate (all four items) 1. **Build + test green.** Full solution builds (0 errors). macOS-safe unit suites: Commons 196, Core 260, Core.Abstractions 134, **Configuration 107**, Runtime 355 (42 dark-Batch-4 skips), ControlPlane 78, OpcUaServer 335 (4 skips), AdminUI 507 (3 skips), + all 8 driver unit suites (Modbus 315, S7 264, AbCip 342, AbLegacy 213, TwinCAT 192, FOCAS 272, OpcUaClient 138, Galaxy 313) and the 6 driver IntegrationTests projects compile. Enum-serialization round-trip coverage preserved. 2. **Fresh-DB migration.** `V3Initial` applies clean to a scratch DB; `SchemaComplianceTests` 8/8. 3. **docker-dev boot (live).** Volume wiped → `V3Initial` applied → both central nodes reached Akka Up → a seeded Modbus driver **spawned and connected** to the `10.100.0.35:5020` fixture (`DriverInstance MAIN-modbus: connected`), deployment **sealed (acks=2)**; AdminUI HTTP 200 on `/`, `/clusters`, `/uns` with 0 unhandled exceptions. 4. **Dark address space (Client.CLI browse).** Recursive browse of `opc.tcp://localhost:4840`: the raw tag `HR200` and the driver have **0 occurrences**; the custom namespace `ns=2` holds only the empty `OtOpcUa` root — no authored variables materialize (Batch 4 lights them up). ## AdminUI Batch-1 stubs (Batch 2/3 replace) Namespace pages (`NamespaceEdit`, `ClusterNamespaces`) → banners; the 8 driver pages' pre-declared Tags editors + Namespace bindings removed; `DriverIdentitySection` Namespace dropdown removed; `UnsTreeService.CreateTagAsync`/`UpdateTagAsync` return the "moved to /raw" failure; per-equipment tag lists empty. ## Deferred (owned by later batches, not regressions) - `/raw` project tree + reference-picker UI, dual `ns=Raw`/`ns=UNS` namespaces + tag-variable materialization, `{{equip}}` reference-relative resolution — Batches 2–4 (address space intentionally dark now). - User-facing docs beyond the `Configuration.md` v3 note (they describe the not-yet-built UI/address space) — updated as Batches 2–4 land. - Live driver IntegrationTests + one pre-existing `AbCip_Green_AgainstSim` fixture-probe failure (AB CIP sim handshake — environment/fixture gate, untouched).
dohertj2 added 39 commits 2026-07-16 01:51:12 -04:00
Shared-type foundation for Batch 1 fan-out (coordinator contracts commit):
- New Commons/Types/RawPaths.cs — the v3 identity authority (Build/Combine/
  Split/Leaf/TryParent/ValidateSegment, ordinal comparer).
- New entities RawFolder, TagGroup, UnsTagReference.
- Reshape DriverInstance (drop NamespaceId, add nullable RawFolderId),
  Tag (require DeviceId, add nullable TagGroupId, drop DriverInstanceId/
  EquipmentId/FolderPath, keep WriteIdempotent), Equipment (drop
  DriverInstanceId/DeviceId), Device (doc: now universal), ServerCluster
  (nav Namespaces -> RawFolders).
- Delete Namespace, EquipmentImportBatch(+Row), NamespaceKind enum.
- DriverTypeMetadata: drop AllowedNamespaceKinds + NamespaceKindCompatibility.

Solution is intentionally red below the foundation projects until Wave C
integrates. Commons + Core.Abstractions compile green.
DbContext (OtOpcUaConfigDbContext):
- Drop Namespace/EquipmentImportBatch/EquipmentImportRow DbSets + Configure methods.
- Add RawFolder/TagGroup/UnsTagReference DbSets + Configure methods with sibling-name
  filtered unique indexes over nullable parents (two-filtered pattern per level).
- Reshape DriverInstance (RawFolderId + UX_DriverInstance_Folder_Name/_ClusterRoot_Name),
  Tag (required DeviceId, nullable TagGroupId, UX_Tag_Group_Name/_Device_Name, IX_Tag_Device),
  Equipment (drop DriverInstanceId/DeviceId + IX_Equipment_Driver), Device (UX_Device_Driver_Name).

Migration squash -> V3Initial:
- Delete the whole Migrations/ folder; scaffold one V3Initial (applies clean to a fresh DB).
- Port the hand-written procs + AuthorizationGrants into V3Initial.StoredProcedures.cs. The v1
  generation-model procs (ConfigGeneration/GenerationId, dead since V2HostingAlignment) are
  rewritten as v3-schema-valid retirement stubs that preserve the two-role EXECUTE-grant surface
  + AuthorizationTests behaviors; sp_ComputeGenerationDiff now references the v3 tables;
  sp_ReleaseExternalIdReservation ported verbatim.

Compile-fixes for the build gate (Wave-C-owned, minimal + TODO(v3 WP4) markers):
- DraftSnapshot/DraftSnapshotFactory: drop the retired Namespaces list.
- DraftValidator: delete namespace-binding + Galaxy-FullName rules; collision rule now VirtualTag-only.

Seeds -> v3 schema:
- seed-clusters.sql summary SELECTs; all 5 scripts/smoke/seed-*.sql rewritten (DriverInstance
  RawFolderId, Device+DeviceConfig endpoint, Tag DeviceId, UnsTagReference; no ConfigGeneration/
  Namespace/sp_PublishGeneration). Verified: all seeds apply clean against a fresh V3Initial DB.

Tests (Configuration.Tests, 107 pass):
- SchemaComplianceTests rewritten to v3 tables + filtered indexes.
- New RawSchemaIntegrityTests: DB-enforced sibling-name uniqueness per raw level + UnsTagReference
  (Equipment,Tag) uniqueness.
- Delete DraftValidatorGalaxyFullNameCorpusTests (WP2/WP6 replaces); fix DraftValidatorTests +
  DraftSnapshotFactoryTests to v3 shapes; repoint scripting-migration existence test to V3Initial.
- EquipmentTagRefResolver<TDef> re-keyed to RawPath: byName(RawPath)-only lookup,
  blob-parse fallback deleted (a miss is a miss). Clear() retained as documented no-op.
- New RawTagEntry(RawPath, TagConfig, WriteIdempotent) in Core.Abstractions — the
  artifact->driver tag-delivery unit both WP5 (factory consumer) and WP4 (artifact
  producer) code against.
- EquipmentNodeWalker greened for the dark Batch-1 address space: drop the raw-tag
  emission path (relied on removed Tag.EquipmentId/FullName); keep Area/Line/Equipment
  folders + VirtualTags + ScriptedAlarms. Raw-tag reference materialization is Batch 4.

Commons, Core.Abstractions, Configuration, Core all build green.
Wave-B EXEMPLAR. Retire the pre-declared/blob-parse tag model; the driver now
builds its RawPath -> definition table from the artifact's RawTagEntry set via a
pure mapper.

- Contracts: rename ModbusEquipmentTagParser -> ModbusTagDefinitionFactory;
  TryParse(reference) -> FromTagConfig(tagConfig, rawPath) (def.Name = rawPath,
  no leading-brace heuristic); add inverse ToTagConfig(def) serializer; keep all
  strict-enum reads + guards; also read stringByteOrder/deadband/coalesceProhibited
  so a RawTagEntry round-trips the full authored def. Inspect() unchanged.
- Options: Tags(IReadOnlyList<ModbusTagDefinition>) -> RawTags(IReadOnlyList<RawTagEntry>).
- Driver: _tagsByName -> _tagsByRawPath (Ordinal); resolver byRawPath-only; build
  the table from RawTags at Initialize threading entry.WriteIdempotent onto each def;
  Discover/Teardown updated.
- Factory: remove ModbusTagDto/BuildTag/ValidateStringLength + ConfigDto.Tags; bind
  RawTags from driver-config JSON.
- CLI ModbusCommandBase.BuildOptions: serialise typed defs -> RawTagEntry via ToTagConfig.
- Tests: migrate Tags= -> RawTags via ModbusRawTags.Entries helper; parser tests ->
  FromTagConfig(rawPath); factory String-length/enum tests re-seated on the mapper seam.
- Propagated rename to ControlPlane EquipmentTagConfigInspector (outside Modbus projects).

Modbus.Tests 315/315, Addressing.Tests 161/161, Cli.Tests 72/72 green.
Docker-gated Driver.Modbus.IntegrationTests left untouched (won't compile against
the new API; expected).
- Rename AbLegacyEquipmentTagParser → AbLegacyTagDefinitionFactory; TryParse →
  FromTagConfig(tagConfig, rawPath, out def). Drop the leading-{ heuristic and the
  deviceHostAddress key; def.Name = rawPath. Add inverse ToTagConfig. Keep strict-enum
  guards + Inspect.
- Options: Tags(IReadOnlyList<AbLegacyTagDefinition>) → RawTags(IReadOnlyList<RawTagEntry>).
- Driver: _tagsByRawPath (Ordinal); byName-only resolver; build table from _options.RawTags
  via FromTagConfig, threading WriteIdempotent + resolving RawTagEntry.DeviceName to the
  owning device host (ResolveDeviceHost; TODO(v3 WaveC) for the live Device-row host).
  DiscoverAsync + family-profile validation iterate the built table. Probe picks its address
  from RawTags via the mapper.
- Factory: retire the pre-declared tag DTO path; bind List<RawTagEntry>? RawTags; keep Devices.
- Cli BuildOptions: deliver typed defs as RawTagEntry via ToTagConfig.
- Tests: AbLegacyRawTags helper; migrate Tags= → RawTags; parser tests → FromTagConfig;
  equipment-ref driver/ResolveHost tests re-authored as RawTagEntry + read by RawPath.

Driver+Contracts+Cli green; 213 driver + 36 Cli tests pass.
Rename S7EquipmentTagParser -> S7TagDefinitionFactory; TryParse(reference)
-> FromTagConfig(tagConfig, rawPath, out def) (drops the leading-{ heuristic,
keys the def by RawPath, adds inverse ToTagConfig). Replace S7DriverOptions.Tags
(pre-declared S7TagDefinition list) with RawTags (IReadOnlyList<RawTagEntry>);
the driver builds its RawPath->def table from RawTags at Initialize via the
factory (skip+log on miss), resolver is byName-only, DiscoverAsync + init guards
+ address pre-parse now run off that table. Factory retires the pre-declared DTO
tag path (RawTags binding only). Cli BuildOptions serialises typed defs to
RawTagEntry. Tests migrated to a S7RawTags helper + FromTagConfig; parser test
files renamed. Coordinator's EquipmentTagConfigInspector S7 rename left to Wave-B
coordinator; Driver.S7.IntegrationTests (Docker-gated) left red per scope.

Driver.S7 + Cli build green; Driver.S7.Tests 264/264, S7.Cli.Tests 49/49.
- Rename TwinCATEquipmentTagParser -> TwinCATTagDefinitionFactory; TryParse ->
  FromTagConfig(tagConfig, rawPath, out def). Drop leading-'{' heuristic + the
  deviceHostAddress key; def.Name = rawPath. Keep strict-enum + Inspect; add a
  Structure guard (sole tag path now) + inverse ToTagConfig.
- Options: Tags -> RawTags (RawTagEntry); keep Devices + protocol fields.
- Driver: _tagsByRawPath (Ordinal); byName-only resolver; build table from
  _options.RawTags via FromTagConfig, threading WriteIdempotent + DeviceName.
  Multi-device: ResolveDeviceHost matches entry.DeviceName against options.Devices
  (by name, then host) -> def.DeviceHostAddress (TODO(v3 WaveC): live host from the
  Device row's DeviceConfig). DiscoverAsync now emits from the table.
- Factory: retire pre-declared tags[] DTO path; bind rawTags; keep Devices.
- Cli: BuildOptions serialises typed defs -> RawTagEntry via ToTagConfig.
- Tests: TwinCATRawTags helper; migrate Tags= -> RawTags=; rename parser tests to
  FromTagConfig; equipment/resolve-host/array tests delivered via RawTags.

Gate: Contracts + Driver build green; Driver.TwinCAT.Tests 192 pass, Cli.Tests 70 pass.
NamespaceMap now carries a RawPath -> upstream node id table built from the
deployed RawTagEntry list (reads each tag's TagConfig.nodeId, threads
WriteIdempotent). Under v3 the read/write/subscribe/history reference handed to
the driver is the tag's RawPath identity; the driver resolves it in two stages:
RawPath -> nodeId string (instance TryResolve) -> live NodeId re-bound against
the session (static TryResolve). Alarm ConditionId + event-history sourceName
stay on the direct session parse (they are upstream node ids, not RawPaths).

- Options: add IReadOnlyList<RawTagEntry> RawTags (Contracts now refs Core.Abstractions).
- Factory: RawTags binds straight into options (no separate DTO); EndpointUrl kept.
- Browser: unchanged (emits neutral BrowseNode DTOs, no TagConfig FullName key).
- Tests: 138 green; new RawPath resolution + factory-binding coverage; migrated
  the stale-session ReadRaw test to author a resolving RawTag.

Contracts + Driver + Browser build clean (0 warn). Wave C wires endpoint->DeviceConfig
and the deploy artifact that populates RawTags.
Wave-B driver re-keying. Under v3 the server addresses Galaxy nodes by RawPath
and the driver dials the Galaxy attributeRef (tag_name.AttributeName). The
dialled address moved into TagConfig under the camelCase key `attributeRef`
(renamed from the pre-v3 PascalCase FullName) and rides on RawTagEntry.

Options + factory:
- GalaxyDriverOptions gains `IReadOnlyList<RawTagEntry> RawTags` (Contracts now
  references the zero-dep Core.Abstractions leaf for RawTagEntry — same as
  Modbus.Contracts). Factory binds `List<RawTagEntry>? RawTags` from DriverConfig.

Boundary translation (built once from options.RawTags):
- RawPath -> attributeRef (forward dial) on Read / Write / Subscribe / Ack.
- attributeRef -> RawPath (reverse) on the value fan-out (OnDataChange) and the
  alarm ConditionId — so the alarm resolves back to the same RawPath the value
  path surfaces. Both legs fall back to identity on a miss, so the live-browse
  discovery path (no authored tags) and synthetic alarm sub-refs
  (attributeRef.InAlarm) dial straight through unchanged.
- WriteAsync dials the attributeRef but resolves security via the RawPath-keyed
  map the discoverer captured (reverse-maps in the resolver closure).

Discovery + alarms:
- GalaxyDiscoverer emits FullName = RawPath for authored attributes (falls back
  to attributeRef), threads RawTagEntry.WriteIdempotent onto DriverAttributeInfo,
  and gains an optional attributeRef -> RawTagEntry resolver ctor param.
- AlarmRefBuilder.Build(conditionReference, dialReference): SourceName/ConditionId
  = RawPath identity; the five live-state/ack sub-refs dial off the attributeRef.

ReinitializeAsync equivalence now compares the session-shape sections field-wise,
excluding RawTags (address-space state re-applied via rediscovery, and a record's
synthesized equality would compare the lists by reference).

Tests: +9 (discoverer RawPath/WriteIdempotent + alarm identity; factory RawTags
binding; driver read/write/subscribe/alarm boundary translation). Existing 304
preserved via identity fallback. 313 pass, 5 live-gated skips.
Apply the reviewed Modbus exemplar to the AbCip multi-device driver:
- Mapper AbCipEquipmentTagParser -> AbCipTagDefinitionFactory; TryParse -> FromTagConfig(tagConfig, rawPath, out def); drop leading-{ heuristic + the deviceHostAddress key; add inverse ToTagConfig; round-trip arrays/members/safety for coverage.
- Options: Tags (typed) -> RawTags (RawTagEntry list); Devices list kept.
- Driver: byName-only resolver over _tagsByRawPath (Ordinal); table built from RawTags via FromTagConfig, threading entry.DeviceName (device routing key) + entry.WriteIdempotent onto the def. Multi-device routing keys on DeviceName-or-host via a _devicesByRoutingKey index; discovery groups _declaredTags by RoutesToDevice.
- Factory: retire the pre-declared Tags DTO/BuildTag; bind List<RawTagEntry> RawTags.
- Probe: derive probe tag path from RawTags via the mapper.
- Cli BuildOptions projects typed tags -> RawTagEntry (ToTagConfig).
- Tests: AbCipRawTags helper (typed def -> RawTagEntry); parser tests -> FromTagConfig; blob-fallback driver tests rewritten to author via RawTags + read by RawPath. Driver.AbCip.Tests 342/342, Cli.Tests 42/42.

TODO(v3 WaveC): DeviceName becomes a pure logical name once the device connection host moves to the Device row's DeviceConfig.
- Rename FocasEquipmentTagParser -> FocasTagDefinitionFactory; TryParse ->
  FromTagConfig(tagConfig, rawPath, out def). Drop leading-{ heuristic + the
  deviceHostAddress blob key; def.Name = rawPath. Add inverse ToTagConfig.
  writable: absent -> read-only, explicit true honoured (note preserved).
- FocasDriverOptions.Tags -> RawTags (IReadOnlyList<RawTagEntry>).
- FocasDriver: _tagsByRawPath (Ordinal); byName-only resolver; build table from
  RawTags via FromTagConfig; multi-device routing threads RawTagEntry.DeviceName
  -> ResolveDeviceHost match against options.Devices -> def.DeviceHostAddress
  (TODO v3 WaveC: live host from Device row DeviceConfig). Tolerant per-tag
  skip+log (mapper/address/matrix miss -> BadNodeIdUnknown); unknown device
  fails init fast.
- Factory: bind List<RawTagEntry> RawTags; retire FocasTagDto/ParseDataType.
- CLI FocasCommandBase.BuildOptions -> RawTags via ToTagConfig + DeviceName.
- Tests: FocasRawTags helper; migrate Tags= -> RawTags=; rename parser tests to
  FromTagConfig; rewrite retired blob-ref tests (equipment-tag/capability-gate/
  resolve-host) onto authored RawPath. 272 driver + 52 CLI green.
- ConfigComposer: snapshot v3 tables (RawFolders/TagGroups/UnsTagReferences),
  drop the retired Namespaces snapshot; RevisionHash follows the new blob.
- DeploymentArtifact: compute each raw tag's RawPath (RawFolder->Driver->Device->
  TagGroup ancestry via shared RawPathResolver); per-driver RawTags + merged
  DriverConfig+DeviceConfig config injected into DriverInstanceSpec via
  DriverDeviceConfigMerger; ParseComposition emits an empty EquipmentTags set
  (DARK until Batch 4); EquipmentNode driver/device hooks always null; cluster
  scoping attributes equipment by UNS line only.
- AddressSpaceComposer: rewritten to the v3 shape (no Namespace/NamespaceKind,
  no dropped Tag/Equipment fields); emits empty EquipmentTags (parity mirror).
- DriverHostActor: fan-out/write map tuple member FullName -> RawPath.
- Commons: new RawPathResolver (shared identity authority) + DriverDeviceConfigMerger
  (single/multi-device endpoint+RawTags merge).
- DraftValidator: v3 rules — raw-name charset (RawPaths.ValidateSegment),
  historized effective-tagname <=255, UNS effective-leaf uniqueness across
  UnsTagReference/VirtualTag/ScriptedAlarm; DraftSnapshot(+Factory) carry the new tables.
- AdminOperationsActor: tag-config inspection resolves driver via Device (no
  EquipmentId/DriverInstanceId on Tag).
- Tests: RawPathResolver/merger unit tests (Commons.Tests, green) +
  DeploymentArtifact RawPath/dark test (Runtime.Tests, awaits WP6 corpus fixes).
Replace pre-declared options.Tags (typed <Driver>TagDefinition lists) with
options.RawTags (IReadOnlyList<RawTagEntry>) across all 6 Docker-gated driver
integration suites so they COMPILE against the v3 driver API.

Each project gets a local <Driver>RawTags helper mirroring the unit-test
project's helper: it serialises a typed def back to its TagConfig blob via the
driver's <Driver>TagDefinitionFactory.ToTagConfig and packages it as a
RawTagEntry (RawPath = def.Name; DeviceName = def.DeviceHostAddress for the
multi-device drivers AbCip/AbLegacy/TwinCAT/FOCAS). Test intent (addresses,
data types, device routing, assertions) is unchanged — only the
tag-authoring/config-delivery shape was migrated.

Docker-gated: these still skip at runtime without fixtures; COMPILE is the gate.
Namespace entity + NamespaceKind retired; DriverInstance dropped NamespaceId;
Tag is raw-only (no EquipmentId/DriverInstanceId/FullName/FolderPath); driver
options replaced pre-declared Tags with RawTags. Stub the retired surfaces so
the AdminUI + Host build green; real Raw/UNS authoring arrives in v3 Batch 2/3.

- ClusterNamespaces/NamespaceEdit: bodies replaced with 'Namespaces retired' banner
- DriverIdentitySection: dropped Namespace dropdown + NamespaceId model field
- 8 driver pages: dropped Namespace binding; retired the pre-declared Tags editor
  (replaced with a '/raw Batch 2' note); multi-device pages keep their Devices editor
- UnsTreeService: equipment-tag counts/list -> empty; Create/UpdateTag -> failure
  result ('Tag authoring moved to the Raw tree (/raw) in v3 Batch 2'); Namespace/
  driver-binding queries removed
- IScriptTagCatalog: project surviving Tag columns only (path from TagConfig FullName, else Name)
- OpcUaClientTagConfigEditor: rebind stale FullName -> model's NodeId
- ClusterDrivers: drop retired NamespaceId column

AdminUI + Host build 0/0. AdminUI.Tests / Host.IntegrationTests remain red (WP6).
Fix TESTS only (production already green) so the four in-scope test projects
compile + pass under the v3 dark address space:

- Core.Abstractions.Tests: drop AllowedNamespaceKinds/NamespaceKindCompatibility
  from DriverTypeMetadata; rewrite EquipmentTagRefResolver tests to the single-func
  RawPath-lookup contract (no parseRef/transient cache).
- Core.Tests: rewrite EquipmentNodeWalkerTests to EquipmentNamespaceContent(Areas,
  Lines,Equipment,VirtualTags?,ScriptedAlarms?) — folders + VirtualTags +
  ScriptedAlarms only; raw-tag variables dark (skipped Batch-4 placeholder). Drop
  retired Equipment.DriverInstanceId.
- Runtime.Tests: rewrite golden corpus + TagConfigCorpusParityTests to the v3
  RawPath/RawTagEntry round-trip (byte + TagConfigIntent parity; EquipmentTags dark).
  Salvage VirtualTag + ScriptedAlarm artifact parity to the new Compose signature.
  Retire the equipment-tag-materialization parity files (Array/Historize/Alias) and
  the equipment-device-binding DeviceHost parity to documented skipped placeholders.
  Skip 34 dark equipment-tag routing/value/alarm/write/discovery actor tests with a
  shared DarkAddressSpaceReasons constant. Re-key cluster-scoped tests to v3 UNS
  line->area attribution.
- ControlPlane.Tests: ConfigComposer round-trips re-keyed to RawFolder/Device (no
  Namespaces); tag-config gate resolves driver via Device; deploy-gate collision test
  re-keyed to UnsEffectiveNameCollision; drop retired BadCrossClusterNamespaceBinding
  case.
Migrate the server composer/applier + 2-node harness tests off the retired
Namespace/NamespaceKind + equipment→driver/device binding schema onto the v3
greenfield shape (raw-only Tag; DriverInstance-RawFolderId; equipment via UnsLine).

OpcUaServer.Tests (134 build errors -> green, 335 pass / 4 skip):
- LIVE (migrated, passing): VirtualTag-historize + ScriptedAlarm composition,
  composer purity/hierarchy, applier MaterialiseHierarchy + MaterialiseEquipmentTags
  idempotency (hand-fed applier, not dark).
- SKIP dark-until-Batch-4: {{equip}} token expansion, applier equipment-namespace
  E2E (equipment-tag variable materialization).
- RETIRE-with-comment / skip: alias-tag (Tag.EquipmentId binding gone) + device-host
  equipment binding (architecturally removed).
- Add local DarkAddressSpaceReasons Skip-reason constants (mirrors Runtime.Tests).

Host.IntegrationTests (green, in-memory): drop deleted-Namespace seeds from
MultiClusterScoping + DriverReconnect; reshape EquipmentNamespaceMaterialization to
assert the DARK contract (folder nodes decode, EquipmentTags empty despite a seeded
v3 raw tag) with the full raw-tag->EquipmentTag materialization kept as a
Batch-4-pending Skip.

Tests-only; no src/ changes.
Migrate tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests (was 358 build errors) to
the v3 Raw-only Tag schema and the Batch-1 AdminUI stubs. Production untouched.

- Driver-page serialization tests: drop the retired pre-declared Tags editor
  (page *TagRow types + DriverOptions.Tags); multi-device pages keep their
  Devices editor (ToOptions(devices)). Enum-serialization round-trip coverage
  (CpuType/ModbusFamily/MelsecFamily/AbCipPlcFamily/AbLegacyPlcFamily/FocasCncSeries)
  preserved intact.
- UnsTreeTestDb: reseed to v3 (Device->Tag raw slice, no EquipmentId/DriverInstanceId
  on Tag, no Namespace).
- UnsTreeService tag tests: assert Batch-1 stubs (CreateTag/UpdateTag refusal,
  empty tag/tag-driver lists); DeleteTag stays live against a raw Tag.
- Equipment #122 guard retained (validates input driver vs line cluster); drop the
  retired persisted-binding assertions + NamespaceId seeds. Area/Line #122 driver-
  orphan guard retired -> assert cross-cluster move now succeeds.
- DeleteCluster: drop the deleted-Namespace child-check test.
- OpcUaClientTagConfigModel/TagConfigValidator: address key FullName -> nodeId.
- ScriptTagCatalog: project surviving Name/DataType/TagConfig; DriverInstanceId null.
- VirtualTag {{equip}} equipment-tag-derived base is dark -> 3 tests skipped (Batch-3).

Result: build green; 507 passed / 3 skipped / 0 failed.
WP1 wrote these against its interim VirtualTag-only collision rule; WP4a replaced it
with the broader UnsEffectiveNameCollision rule. Neither test-sweep scope covered these
two Configuration.Tests files, so the assertions went stale. Configuration.Tests 107/107.
docs(v3): Batch 1 schema/identity note in Configuration.md + PR description
v2-ci / build (pull_request) Successful in 3m31s
v2-ci / unit-tests (pull_request) Failing after 11m16s
080e958d94
dohertj2 merged commit f121f8ca16 into master 2026-07-16 01:52:52 -04:00
Sign in to join this conversation.