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
@@ -0,0 +1,135 @@
namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
/// <summary>
/// The v3 identity authority. A <c>RawPath</c> is the cluster-scoped, slash-separated raw device
/// path — <c>&lt;Folder/…&gt;/&lt;Driver&gt;/&lt;Device&gt;/&lt;TagGroup/…&gt;/&lt;Tag&gt;</c> — and is
/// the single string used at every identity seam: <c>ns=Raw</c> NodeIds, the driver wire-reference
/// (<c>EquipmentTagRefResolver</c> key), the value fan-out / write-routing maps
/// (<c>DriverHostActor</c>), the historian default tagname + mux interest key, the native-alarm
/// <c>ConditionId</c>, and <c>ctx.GetTag</c> script paths.
/// <para>
/// RawPaths are ALWAYS built through this helper — never string-concatenated ad hoc — so the
/// separator and the segment charset are enforced in exactly one place. Comparison is
/// case-sensitive ordinal everywhere (see <see cref="Comparer"/>).
/// </para>
/// </summary>
public static class RawPaths
{
/// <summary>The single path separator character. Forbidden inside any segment.</summary>
public const char Separator = '/';
/// <summary>The path separator as a string (join/split convenience).</summary>
public const string SeparatorString = "/";
/// <summary>The one comparer for RawPaths and their segments: case-sensitive ordinal.</summary>
public static StringComparer Comparer => StringComparer.Ordinal;
/// <summary>
/// Validate a single path segment. A segment must be non-empty, contain no
/// <see cref="Separator"/>, and carry no leading/trailing whitespace (the separator and
/// surrounding whitespace would corrupt the NodeId / round-trip).
/// </summary>
/// <param name="segment">The candidate segment (a folder / driver / device / group / tag name).</param>
/// <returns>A human-readable error when invalid, or <see langword="null"/> when the segment is valid.</returns>
public static string? ValidateSegment(string? segment)
{
if (string.IsNullOrEmpty(segment))
return "Name must not be empty.";
if (segment.IndexOf(Separator) >= 0)
return $"Name must not contain '{Separator}'.";
if (char.IsWhiteSpace(segment[0]) || char.IsWhiteSpace(segment[^1]))
return "Name must not have leading or trailing whitespace.";
return null;
}
/// <summary>True when <paramref name="segment"/> passes <see cref="ValidateSegment"/>.</summary>
/// <param name="segment">The candidate segment.</param>
/// <returns><see langword="true"/> when the segment is a legal RawPath segment.</returns>
public static bool IsValidSegment(string? segment) => ValidateSegment(segment) is null;
/// <summary>
/// Build a RawPath from ordered segments (root-first). Every segment is validated via
/// <see cref="ValidateSegment"/>; an invalid or empty segment list throws.
/// </summary>
/// <param name="segments">The ordered, non-empty segments (root → leaf).</param>
/// <returns>The slash-joined RawPath.</returns>
/// <exception cref="ArgumentException">A segment is invalid, or the sequence is empty.</exception>
public static string Build(IEnumerable<string> segments)
{
ArgumentNullException.ThrowIfNull(segments);
var list = segments as IReadOnlyList<string> ?? segments.ToList();
if (list.Count == 0)
throw new ArgumentException("A RawPath must have at least one segment.", nameof(segments));
for (var i = 0; i < list.Count; i++)
{
var error = ValidateSegment(list[i]);
if (error is not null)
throw new ArgumentException($"Invalid RawPath segment at index {i} ('{list[i]}'): {error}", nameof(segments));
}
return string.Join(Separator, list);
}
/// <summary>Build a RawPath from ordered segments (root-first). See <see cref="Build(IEnumerable{string})"/>.</summary>
/// <param name="segments">The ordered, non-empty segments (root → leaf).</param>
/// <returns>The slash-joined RawPath.</returns>
public static string Build(params string[] segments) => Build((IEnumerable<string>)segments);
/// <summary>
/// Append a child segment to an existing (already-built) parent RawPath. The child is
/// validated; a blank parent yields the (validated) child alone.
/// </summary>
/// <param name="parentPath">The parent RawPath (assumed already valid), or blank for a root segment.</param>
/// <param name="childSegment">The child segment to append.</param>
/// <returns>The combined RawPath.</returns>
/// <exception cref="ArgumentException">The child segment is invalid.</exception>
public static string Combine(string? parentPath, string childSegment)
{
var error = ValidateSegment(childSegment);
if (error is not null)
throw new ArgumentException($"Invalid RawPath segment ('{childSegment}'): {error}", nameof(childSegment));
return string.IsNullOrEmpty(parentPath) ? childSegment : parentPath + Separator + childSegment;
}
/// <summary>Split a RawPath into its ordered segments (root → leaf).</summary>
/// <param name="rawPath">The RawPath to split.</param>
/// <returns>The ordered segments; a single-element array for a one-segment path.</returns>
public static IReadOnlyList<string> Split(string rawPath)
{
ArgumentNullException.ThrowIfNull(rawPath);
return rawPath.Split(Separator);
}
/// <summary>The leaf (last) segment of a RawPath.</summary>
/// <param name="rawPath">The RawPath.</param>
/// <returns>The final segment.</returns>
public static string Leaf(string rawPath)
{
ArgumentNullException.ThrowIfNull(rawPath);
var idx = rawPath.LastIndexOf(Separator);
return idx < 0 ? rawPath : rawPath[(idx + 1)..];
}
/// <summary>
/// Get the parent RawPath (everything up to, not including, the leaf). Returns
/// <see langword="false"/> for a single-segment path (no parent).
/// </summary>
/// <param name="rawPath">The RawPath.</param>
/// <param name="parent">The parent RawPath when this returns <see langword="true"/>.</param>
/// <returns><see langword="true"/> when a parent exists.</returns>
public static bool TryParent(string rawPath, out string parent)
{
ArgumentNullException.ThrowIfNull(rawPath);
var idx = rawPath.LastIndexOf(Separator);
if (idx < 0)
{
parent = string.Empty;
return false;
}
parent = rawPath[..idx];
return true;
}
}
@@ -1,6 +1,12 @@
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; 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 public sealed class Device
{ {
/// <summary> /// <summary>
@@ -13,12 +13,13 @@ public sealed class DriverInstance
public required string ClusterId { get; set; } public required string ClusterId { get; set; }
/// <summary> /// <summary>
/// Logical FK to <see cref="Namespace.NamespaceId"/>. Same-cluster binding enforced by /// v3: logical FK to a containing <see cref="RawFolder.RawFolderId"/>; <see langword="null"/>
/// <c>sp_ValidateDraft</c>: Namespace.ClusterId must equal DriverInstance.ClusterId. /// = the driver sits at the cluster root of the Raw tree. Contributes the leading segments of
/// every child tag's <c>RawPath</c>.
/// </summary> /// </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; } public required string Name { get; set; }
/// <summary>Galaxy | Modbus | AbCip | AbLegacy | S7 | TwinCat | Focas | OpcUaClient</summary> /// <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> /// <summary>UUIDv4, IMMUTABLE across all generations of the same EquipmentId. Downstream-consumer join key.</summary>
public Guid EquipmentUuid { get; set; } public Guid EquipmentUuid { get; set; }
/// <summary> // v3: equipment no longer binds a driver or device. It references existing raw tags via
/// Optional logical FK to the driver providing data for this equipment. // UnsTagReference and hosts VirtualTags + ScriptedAlarms. The old DriverInstanceId / DeviceId
/// <c>null</c> = VirtualTag-only / driver-less equipment (no field driver). // binding columns are retired.
/// </summary>
public string? DriverInstanceId { get; set; }
/// <summary>Optional logical FK to a multi-device driver's device.</summary>
public string? DeviceId { get; set; }
/// <summary>Logical FK to <see cref="UnsLine.UnsLineId"/>.</summary> /// <summary>Logical FK to <see cref="UnsLine.UnsLineId"/>.</summary>
public required string UnsLineId { get; set; } 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 // Navigation
/// <summary>Gets or sets the collection of cluster nodes.</summary> /// <summary>Gets or sets the collection of cluster nodes.</summary>
public ICollection<ClusterNode> Nodes { get; set; } = []; public ICollection<ClusterNode> Nodes { get; set; } = [];
/// <summary>Gets or sets the collection of namespaces in the cluster.</summary> /// <summary>Gets or sets the collection of root-level Raw-tree folders in the cluster.</summary>
public ICollection<Namespace> Namespaces { get; set; } = []; public ICollection<RawFolder> RawFolders { get; set; } = [];
} }
@@ -3,46 +3,34 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
/// <summary> /// <summary>
/// One canonical tag (signal) in a cluster's generation. <see cref="EquipmentId"/> set ⟺ the /// v3 raw-only tag (signal). A tag is authored ONCE, under a <see cref="Device"/> in the Raw
/// tag participates in the Equipment tree. A <c>GalaxyMxGateway</c>-bound equipment tag is an /// tree; its identity is its <c>RawPath</c>
/// ordinary equipment tag (GalaxyMxGateway is a standard Equipment-kind driver) that carries its /// (<c>&lt;Folder/…&gt;/&lt;Driver&gt;/&lt;Device&gt;/&lt;TagGroup/…&gt;/&lt;Name&gt;</c>). UNS
/// Galaxy attribute reference in <c>TagConfig.FullName</c> — there is no separate alias concept. /// 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> /// </summary>
public sealed class Tag public sealed class Tag
{ {
/// <summary> /// <summary>Gets or sets the unique database row identifier for the tag (PK).</summary>
/// Gets or sets the unique database row identifier for the tag.
/// </summary>
public Guid TagRowId { get; set; } public Guid TagRowId { get; set; }
/// <summary> /// <summary>Gets or sets the stable logical tag identifier (unique fleet-wide).</summary>
/// Gets or sets the tag identifier.
/// </summary>
public required string TagId { get; set; } public required string TagId { get; set; }
/// <summary> /// <summary>v3: required logical FK to <see cref="Device.DeviceId"/>. Every tag lives under a device.</summary>
/// Gets or sets the driver instance identifier for this tag. public required string DeviceId { get; set; }
/// </summary>
public required string DriverInstanceId { get; set; }
/// <summary> /// <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> /// </summary>
public string? DeviceId { get; set; } public string? TagGroupId { get; set; }
/// <summary> /// <summary>Gets or sets the tag name. A RawPath segment: no <c>/</c>, no lead/trail whitespace.</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>
public required string Name { get; set; } 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> /// <summary>OPC UA built-in type name (Boolean / Int32 / Float / etc.).</summary>
public required string DataType { get; set; } public required string DataType { get; set; }
@@ -51,7 +39,7 @@ public sealed class Tag
/// </summary> /// </summary>
public required TagAccessLevel AccessLevel { get; set; } 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; } public bool WriteIdempotent { get; set; }
/// <summary> /// <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,
}
@@ -84,7 +84,6 @@ public sealed class DriverTypeRegistry
/// <summary>Per-driver-type metadata used by the Core, validator, and Admin UI.</summary> /// <summary>Per-driver-type metadata used by the Core, validator, and Admin UI.</summary>
/// <param name="TypeName">Driver type name (matches <c>DriverInstance.DriverType</c> column values).</param> /// <param name="TypeName">Driver type name (matches <c>DriverInstance.DriverType</c> column values).</param>
/// <param name="AllowedNamespaceKinds">Which namespace kinds this driver type may be bound to.</param>
/// <param name="DriverConfigJsonSchema">JSON Schema (Draft 2020-12) the driver's <c>DriverConfig</c> column must validate against.</param> /// <param name="DriverConfigJsonSchema">JSON Schema (Draft 2020-12) the driver's <c>DriverConfig</c> column must validate against.</param>
/// <param name="DeviceConfigJsonSchema">JSON Schema for <c>DeviceConfig</c> (multi-device drivers); null if the driver has no device layer.</param> /// <param name="DeviceConfigJsonSchema">JSON Schema for <c>DeviceConfig</c> (multi-device drivers); null if the driver has no device layer.</param>
/// <param name="TagConfigJsonSchema">JSON Schema for <c>TagConfig</c>; required for every driver since every driver has tags.</param> /// <param name="TagConfigJsonSchema">JSON Schema for <c>TagConfig</c>; required for every driver since every driver has tags.</param>
@@ -95,24 +94,11 @@ public sealed class DriverTypeRegistry
/// hybrid-formula constants, and whether process-level <c>MemoryRecycle</c> / scheduled- /// hybrid-formula constants, and whether process-level <c>MemoryRecycle</c> / scheduled-
/// recycle protections apply (Tier C only). Every registered driver type must declare one. /// recycle protections apply (Tier C only). Every registered driver type must declare one.
/// </param> /// </param>
// v3: AllowedNamespaceKinds retired with the Namespace entity — the two OPC UA namespaces (Raw/UNS)
// are implicit, so a driver type no longer declares a namespace-kind compatibility bitmask.
public sealed record DriverTypeMetadata( public sealed record DriverTypeMetadata(
string TypeName, string TypeName,
NamespaceKindCompatibility AllowedNamespaceKinds,
string DriverConfigJsonSchema, string DriverConfigJsonSchema,
string? DeviceConfigJsonSchema, string? DeviceConfigJsonSchema,
string TagConfigJsonSchema, string TagConfigJsonSchema,
DriverTier Tier); DriverTier Tier);
/// <summary>Bitmask of namespace kinds a driver type may populate.</summary>
[Flags]
public enum NamespaceKindCompatibility
{
/// <summary>Driver does not populate any namespace (invalid; should never appear in registry).</summary>
None = 0,
/// <summary>Driver may populate Equipment-kind namespaces (UNS path, Equipment rows).</summary>
Equipment = 1,
/// <summary>Driver may populate the future Simulated namespace (replay driver — not in v2.0).</summary>
Simulated = 4,
}