v3 batch1 contracts: RawPaths identity helper + reshaped greenfield entities

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.
This commit is contained in:
Joseph Doherty
2026-07-15 18:52:19 -04:00
parent 10e23f8b6d
commit cb720bb8c3
13 changed files with 265 additions and 239 deletions
@@ -1,6 +1,12 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>Per-device row for multi-device drivers (Modbus, AB CIP). Optional for single-device drivers.</summary>
/// <summary>
/// v3: a device under a <see cref="DriverInstance"/>. Now UNIVERSAL — every driver has ≥1 device
/// (single-endpoint drivers get an auto-created default device). Endpoint/connection settings live
/// in <see cref="DeviceConfig"/> (the Kepware channel/device split: <c>DriverConfig</c> keeps
/// protocol/channel-level settings; <c>DeviceConfig</c> carries host/endpoint + per-device
/// settings). The device name is a RawPath segment.
/// </summary>
public sealed class Device
{
/// <summary>
@@ -13,12 +13,13 @@ public sealed class DriverInstance
public required string ClusterId { get; set; }
/// <summary>
/// Logical FK to <see cref="Namespace.NamespaceId"/>. Same-cluster binding enforced by
/// <c>sp_ValidateDraft</c>: Namespace.ClusterId must equal DriverInstance.ClusterId.
/// v3: logical FK to a containing <see cref="RawFolder.RawFolderId"/>; <see langword="null"/>
/// = the driver sits at the cluster root of the Raw tree. Contributes the leading segments of
/// every child tag's <c>RawPath</c>.
/// </summary>
public required string NamespaceId { get; set; }
public string? RawFolderId { get; set; }
/// <summary>Gets or sets the friendly name of this driver instance.</summary>
/// <summary>Gets or sets the friendly name of this driver instance. A RawPath segment.</summary>
public required string Name { get; set; }
/// <summary>Galaxy | Modbus | AbCip | AbLegacy | S7 | TwinCat | Focas | OpcUaClient</summary>
@@ -17,14 +17,9 @@ public sealed class Equipment
/// <summary>UUIDv4, IMMUTABLE across all generations of the same EquipmentId. Downstream-consumer join key.</summary>
public Guid EquipmentUuid { get; set; }
/// <summary>
/// Optional logical FK to the driver providing data for this equipment.
/// <c>null</c> = VirtualTag-only / driver-less equipment (no field driver).
/// </summary>
public string? DriverInstanceId { get; set; }
/// <summary>Optional logical FK to a multi-device driver's device.</summary>
public string? DeviceId { get; set; }
// v3: equipment no longer binds a driver or device. It references existing raw tags via
// UnsTagReference and hosts VirtualTags + ScriptedAlarms. The old DriverInstanceId / DeviceId
// binding columns are retired.
/// <summary>Logical FK to <see cref="UnsLine.UnsLineId"/>.</summary>
public required string UnsLineId { get; set; }
@@ -1,124 +0,0 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>
/// Staged equipment-import batch per Phase 6.4 Stream B.2. Rows land in the child
/// <see cref="EquipmentImportRow"/> table under a batch header; operator reviews + either
/// drops (via <c>DropImportBatch</c>) or finalises (via <c>FinaliseImportBatch</c>) in one
/// bounded transaction. The live <c>Equipment</c> table never sees partial state.
/// </summary>
/// <remarks>
/// <para>User-scoped visibility: the preview modal only shows batches where
/// <see cref="CreatedBy"/> equals the current operator. Prevents accidental
/// cross-operator finalise during concurrent imports. An admin finalise / drop surface
/// can override this — tracked alongside the UI follow-up.</para>
///
/// <para><see cref="FinalisedAtUtc"/> stamps the moment the batch promoted from staging
/// into <c>Equipment</c>. Null = still in staging; non-null = archived / finalised.</para>
/// </remarks>
public sealed class EquipmentImportBatch
{
/// <summary>Gets or sets the unique identifier for this batch.</summary>
public Guid Id { get; set; }
/// <summary>Gets or sets the cluster identifier.</summary>
public required string ClusterId { get; set; }
/// <summary>Gets or sets the user name who created this batch.</summary>
public required string CreatedBy { get; set; }
/// <summary>Gets or sets the UTC timestamp when this batch was created.</summary>
public DateTime CreatedAtUtc { get; set; }
/// <summary>Gets or sets the total number of rows staged in this batch.</summary>
public int RowsStaged { get; set; }
/// <summary>Gets or sets the number of rows accepted in this batch.</summary>
public int RowsAccepted { get; set; }
/// <summary>Gets or sets the number of rows rejected in this batch.</summary>
public int RowsRejected { get; set; }
/// <summary>Gets or sets the UTC timestamp when this batch was finalised, or null if still in staging.</summary>
public DateTime? FinalisedAtUtc { get; set; }
/// <summary>Gets or sets the collection of staged rows in this batch.</summary>
public ICollection<EquipmentImportRow> Rows { get; set; } = [];
}
/// <summary>
/// One staged row under an <see cref="EquipmentImportBatch"/>. Mirrors the required
/// + optional columns from the CSV importer's output + an
/// <see cref="IsAccepted"/> flag + a <see cref="RejectReason"/> string the preview modal
/// renders.
/// </summary>
public sealed class EquipmentImportRow
{
/// <summary>Gets or sets the unique identifier for this row.</summary>
public Guid Id { get; set; }
/// <summary>Gets or sets the parent batch identifier.</summary>
public Guid BatchId { get; set; }
/// <summary>Gets or sets the line number in the source file.</summary>
public int LineNumberInFile { get; set; }
/// <summary>Gets or sets a value indicating whether this row was accepted.</summary>
public bool IsAccepted { get; set; }
/// <summary>Gets or sets the reason this row was rejected, if applicable.</summary>
public string? RejectReason { get; set; }
/// <summary>Gets or sets the Z tag identifier.</summary>
public required string ZTag { get; set; }
/// <summary>Gets or sets the machine code.</summary>
public required string MachineCode { get; set; }
/// <summary>Gets or sets the SAP identifier.</summary>
public required string SAPID { get; set; }
/// <summary>Gets or sets the equipment identifier.</summary>
public required string EquipmentId { get; set; }
/// <summary>Gets or sets the equipment UUID.</summary>
public required string EquipmentUuid { get; set; }
/// <summary>Gets or sets the equipment name.</summary>
public required string Name { get; set; }
/// <summary>Gets or sets the UNS area name.</summary>
public required string UnsAreaName { get; set; }
/// <summary>Gets or sets the UNS line name.</summary>
public required string UnsLineName { get; set; }
/// <summary>Gets or sets the manufacturer name.</summary>
public string? Manufacturer { get; set; }
/// <summary>Gets or sets the equipment model.</summary>
public string? Model { get; set; }
/// <summary>Gets or sets the serial number.</summary>
public string? SerialNumber { get; set; }
/// <summary>Gets or sets the hardware revision.</summary>
public string? HardwareRevision { get; set; }
/// <summary>Gets or sets the software revision.</summary>
public string? SoftwareRevision { get; set; }
/// <summary>Gets or sets the year of construction.</summary>
public string? YearOfConstruction { get; set; }
/// <summary>Gets or sets the asset location.</summary>
public string? AssetLocation { get; set; }
/// <summary>Gets or sets the manufacturer URI.</summary>
public string? ManufacturerUri { get; set; }
/// <summary>Gets or sets the device manual URI.</summary>
public string? DeviceManualUri { get; set; }
/// <summary>Gets or sets the parent batch.</summary>
public EquipmentImportBatch? Batch { get; set; }
}
@@ -1,37 +0,0 @@
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>
/// OPC UA namespace served by a cluster. Generation-versioned —
/// namespaces are content (affect what consumers see at the endpoint), not topology.
/// </summary>
public sealed class Namespace
{
/// <summary>Gets or sets the row identifier for this namespace.</summary>
public Guid NamespaceRowId { get; set; }
/// <summary>Stable logical ID, e.g. "LINE3-OPCUA-equipment". Globally unique in v2.</summary>
public required string NamespaceId { get; set; }
/// <summary>Gets or sets the cluster identifier.</summary>
public required string ClusterId { get; set; }
/// <summary>Gets or sets the namespace kind.</summary>
public required NamespaceKind Kind { get; set; }
/// <summary>E.g. "urn:zb:warsaw-west:equipment". Unique fleet-wide per generation.</summary>
public required string NamespaceUri { get; set; }
/// <summary>Gets or sets a value indicating whether the namespace is enabled.</summary>
public bool Enabled { get; set; } = true;
/// <summary>Gets or sets optional notes about the namespace.</summary>
public string? Notes { get; set; }
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
/// <summary>Gets or sets the associated server cluster.</summary>
public ServerCluster? Cluster { get; set; }
}
@@ -0,0 +1,30 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>
/// v3 Raw-tree driver-organizing folder. Nestable (Kepware-style) directly under a cluster or
/// under another <see cref="RawFolder"/>. Folders group <see cref="DriverInstance"/> rows; they
/// contribute the leading segments of a driver's <c>RawPath</c>.
/// </summary>
public sealed class RawFolder
{
/// <summary>Gets or sets the database row identifier (PK).</summary>
public Guid RawFolderRowId { get; set; }
/// <summary>Stable logical ID, unique fleet-wide.</summary>
public required string RawFolderId { get; set; }
/// <summary>Logical FK to <see cref="ServerCluster.ClusterId"/> — the folder is cluster-rooted.</summary>
public required string ClusterId { get; set; }
/// <summary>Logical FK to a parent <see cref="RawFolder.RawFolderId"/>; <see langword="null"/> = root under the cluster.</summary>
public string? ParentRawFolderId { get; set; }
/// <summary>Sibling-unique folder name. A RawPath segment: no <c>/</c>, no lead/trail whitespace.</summary>
public required string Name { get; set; }
/// <summary>Sibling display ordering.</summary>
public int SortOrder { get; set; }
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}
@@ -47,6 +47,6 @@ public sealed class ServerCluster
// Navigation
/// <summary>Gets or sets the collection of cluster nodes.</summary>
public ICollection<ClusterNode> Nodes { get; set; } = [];
/// <summary>Gets or sets the collection of namespaces in the cluster.</summary>
public ICollection<Namespace> Namespaces { get; set; } = [];
/// <summary>Gets or sets the collection of root-level Raw-tree folders in the cluster.</summary>
public ICollection<RawFolder> RawFolders { get; set; } = [];
}
@@ -3,46 +3,34 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>
/// One canonical tag (signal) in a cluster's generation. <see cref="EquipmentId"/> set ⟺ the
/// tag participates in the Equipment tree. A <c>GalaxyMxGateway</c>-bound equipment tag is an
/// ordinary equipment tag (GalaxyMxGateway is a standard Equipment-kind driver) that carries its
/// Galaxy attribute reference in <c>TagConfig.FullName</c> — there is no separate alias concept.
/// v3 raw-only tag (signal). A tag is authored ONCE, under a <see cref="Device"/> in the Raw
/// tree; its identity is its <c>RawPath</c>
/// (<c>&lt;Folder/…&gt;/&lt;Driver&gt;/&lt;Device&gt;/&lt;TagGroup/…&gt;/&lt;Name&gt;</c>). UNS
/// surfaces it by reference (<see cref="UnsTagReference"/>), never by re-authoring. The tag's
/// driver-specific address lives inside <see cref="TagConfig"/> as an ordinary field (what the
/// driver dials), not as system identity.
/// </summary>
public sealed class Tag
{
/// <summary>
/// Gets or sets the unique database row identifier for the tag.
/// </summary>
/// <summary>Gets or sets the unique database row identifier for the tag (PK).</summary>
public Guid TagRowId { get; set; }
/// <summary>
/// Gets or sets the tag identifier.
/// </summary>
/// <summary>Gets or sets the stable logical tag identifier (unique fleet-wide).</summary>
public required string TagId { get; set; }
/// <summary>
/// Gets or sets the driver instance identifier for this tag.
/// </summary>
public required string DriverInstanceId { get; set; }
/// <summary>v3: required logical FK to <see cref="Device.DeviceId"/>. Every tag lives under a device.</summary>
public required string DeviceId { get; set; }
/// <summary>
/// Gets or sets the device identifier.
/// Optional logical FK to a containing <see cref="TagGroup.TagGroupId"/>;
/// <see langword="null"/> = the tag sits directly under its device. Contributes the middle
/// segments of the tag's <c>RawPath</c>.
/// </summary>
public string? DeviceId { get; set; }
public string? TagGroupId { get; set; }
/// <summary>
/// Set when the tag belongs to an Equipment; NULL for FolderPath-scoped (namespace-root) tags.
/// </summary>
public string? EquipmentId { get; set; }
/// <summary>
/// Gets or sets the tag name.
/// </summary>
/// <summary>Gets or sets the tag name. A RawPath segment: no <c>/</c>, no lead/trail whitespace.</summary>
public required string Name { get; set; }
/// <summary>Only used when <see cref="EquipmentId"/> is NULL (FolderPath-scoped namespace tag).</summary>
public string? FolderPath { get; set; }
/// <summary>OPC UA built-in type name (Boolean / Int32 / Float / etc.).</summary>
public required string DataType { get; set; }
@@ -51,7 +39,7 @@ public sealed class Tag
/// </summary>
public required TagAccessLevel AccessLevel { get; set; }
/// <summary>Opt-in for write retry eligibility.</summary>
/// <summary>Opt-in for write retry eligibility (R2 resilience contract — retained in v3).</summary>
public bool WriteIdempotent { get; set; }
/// <summary>
@@ -0,0 +1,30 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>
/// v3 Raw-tree tag-organizing group. Nestable (Kepware-style) directly under a
/// <see cref="Device"/> or under another <see cref="TagGroup"/>. Groups contribute the middle
/// segments of a tag's <c>RawPath</c> (between the device and the tag name).
/// </summary>
public sealed class TagGroup
{
/// <summary>Gets or sets the database row identifier (PK).</summary>
public Guid TagGroupRowId { get; set; }
/// <summary>Stable logical ID, unique fleet-wide.</summary>
public required string TagGroupId { get; set; }
/// <summary>Logical FK to <see cref="Device.DeviceId"/> — the group lives under a device.</summary>
public required string DeviceId { get; set; }
/// <summary>Logical FK to a parent <see cref="TagGroup.TagGroupId"/>; <see langword="null"/> = root under the device.</summary>
public string? ParentTagGroupId { get; set; }
/// <summary>Sibling-unique group name. A RawPath segment: no <c>/</c>, no lead/trail whitespace.</summary>
public required string Name { get; set; }
/// <summary>Sibling display ordering.</summary>
public int SortOrder { get; set; }
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}
@@ -0,0 +1,35 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary>
/// v3 UNS projection row: a reference from an <see cref="Equipment"/> to an existing raw
/// <see cref="Tag"/>. UNS authoring is reference-only — equipment no longer authors
/// <c>TagConfig</c> or binds a driver; it points at raw tags (optionally under a display-name
/// override). The backing raw tag is the single value source; the UNS node fans out from it.
/// </summary>
public sealed class UnsTagReference
{
/// <summary>Gets or sets the database row identifier (PK).</summary>
public Guid UnsTagReferenceRowId { get; set; }
/// <summary>Stable logical ID, unique fleet-wide.</summary>
public required string UnsTagReferenceId { get; set; }
/// <summary>Logical FK to <see cref="Equipment.EquipmentId"/> — the referencing equipment.</summary>
public required string EquipmentId { get; set; }
/// <summary>Logical FK to <see cref="Tag.TagId"/> — the backing raw tag (same cluster as the equipment).</summary>
public required string TagId { get; set; }
/// <summary>
/// Optional UNS display-name override; <see langword="null"/> = use the raw tag's <c>Name</c>.
/// The effective name must be unique within the equipment across references, VirtualTags, and
/// ScriptedAlarms (enforced at authoring and at the deploy gate).
/// </summary>
public string? DisplayNameOverride { get; set; }
/// <summary>Sibling display ordering within the equipment's Tags list.</summary>
public int SortOrder { get; set; }
/// <summary>Optimistic concurrency token for last-write-wins detection in the v2 live-edit model.</summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}
@@ -1,19 +0,0 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
/// <summary>OPC UA namespace kind. One of each kind per cluster per generation.</summary>
public enum NamespaceKind
{
/// <summary>
/// Equipment namespace — raw signals from native-protocol drivers (Modbus, AB CIP, AB Legacy,
/// S7, TwinCAT, FOCAS, and OpcUaClient when gatewaying raw equipment). UNS 5-level hierarchy
/// applies.
/// </summary>
Equipment,
/// <summary>
/// Reserved for future replay driver per handoff §"Digital Twin Touchpoints" — not populated
/// in v2.0 but enum value reserved so the schema does not need to change when the replay
/// driver lands.
/// </summary>
Simulated,
}