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;
/// <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,
}
@@ -84,7 +84,6 @@ public sealed class DriverTypeRegistry
/// <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="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="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>
@@ -95,24 +94,11 @@ public sealed class DriverTypeRegistry
/// hybrid-formula constants, and whether process-level <c>MemoryRecycle</c> / scheduled-
/// recycle protections apply (Tier C only). Every registered driver type must declare one.
/// </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(
string TypeName,
NamespaceKindCompatibility AllowedNamespaceKinds,
string DriverConfigJsonSchema,
string? DeviceConfigJsonSchema,
string TagConfigJsonSchema,
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,
}