refactor(opcuaserver): rename Phase7* address-space pipeline to AddressSpace*
v2-ci / build (push) Failing after 37s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

The OPC UA address-space build pipeline was named after a v2-roadmap
milestone number rather than its domain. Rename the family to describe
what it does (build/diff/apply the OPC UA address space):

  Phase7Composer          -> AddressSpaceComposer
  Phase7CompositionResult -> AddressSpaceComposition
  Phase7Planner           -> AddressSpacePlanner
  Phase7Plan              -> AddressSpacePlan
  Phase7Applier           -> AddressSpaceApplier
  Phase7ApplyOutcome      -> AddressSpaceApplyOutcome

The 9 Phase7*Tests suites follow suit; Phase7ScriptingEntitiesTests ->
ScriptingEntitiesTests (it tests the scripting migration, not the
pipeline). Log-message prefixes move to the new class names.

Pure mechanical rename, no behavioral change. EF migration classes/IDs
(AddPhase7ScriptingTables, ExtendComputeGenerationDiffWithPhase7) are
immutable and left untouched, as are historical design docs.

Build clean; OpcUaServer 261/261, Runtime 272/272, ScriptingEntities
12/12 green.
This commit is contained in:
Joseph Doherty
2026-06-18 19:16:28 -04:00
parent 6af54ac935
commit 40e8a23e7c
44 changed files with 364 additions and 364 deletions
@@ -73,7 +73,7 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
/// <summary>Forwards an in-place tag-attribute update (F10b) to the inner sink when it supports the /// <summary>Forwards an in-place tag-attribute update (F10b) to the inner sink when it supports the
/// surgical capability. Returns false otherwise — before the real <c>SdkAddressSpaceSink</c> is /// surgical capability. Returns false otherwise — before the real <c>SdkAddressSpaceSink</c> is
/// swapped in (inner is still the null sink), or any inner sink that isn't surgical — so the caller /// swapped in (inner is still the null sink), or any inner sink that isn't surgical — so the caller
/// (Phase7Applier) falls back to a full rebuild. Without this forward the surgical optimization is /// (AddressSpaceApplier) falls back to a full rebuild. Without this forward the surgical optimization is
/// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink.</summary> /// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink.</summary>
/// <param name="variableNodeId">The node ID of the variable to update in place.</param> /// <param name="variableNodeId">The node ID of the variable to update in place.</param>
/// <param name="writable">Whether the node should be read/write.</param> /// <param name="writable">Whether the node should be read/write.</param>
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
/// Single source of truth for equipment-namespace OPC UA NodeId strings. The variable NodeId is /// Single source of truth for equipment-namespace OPC UA NodeId strings. The variable NodeId is
/// FOLDER-SCOPED (<c>{parent}/{Name}</c>), NOT the driver-side FullName — a driver wire ref is not /// FOLDER-SCOPED (<c>{parent}/{Name}</c>), NOT the driver-side FullName — a driver wire ref is not
/// unique across identical machines, so FullName-as-NodeId would collide in the sink. Used by the /// unique across identical machines, so FullName-as-NodeId would collide in the sink. Used by the
/// materialiser (Phase7Applier), the VirtualTag publish map, and the driver live-value router so all /// materialiser (AddressSpaceApplier), the VirtualTag publish map, and the driver live-value router so all
/// three agree on the exact NodeId a variable was placed at. /// three agree on the exact NodeId a variable was placed at.
/// </summary> /// </summary>
public static class EquipmentNodeIds public static class EquipmentNodeIds
@@ -31,7 +31,7 @@ public interface IOpcUaAddressSpaceSink
/// Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients /// Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients
/// can browse it as a proper condition (with basic Active/Ack state). The node id equals the /// can browse it as a proper condition (with basic Active/Ack state). The node id equals the
/// alarm node id (the ScriptedAlarmId) so subsequent <see cref="WriteAlarmCondition"/> calls update /// alarm node id (the ScriptedAlarmId) so subsequent <see cref="WriteAlarmCondition"/> calls update
/// it. Used by <c>Phase7Applier.MaterialiseScriptedAlarms</c>. Idempotent. /// it. Used by <c>AddressSpaceApplier.MaterialiseScriptedAlarms</c>. Idempotent.
/// </summary> /// </summary>
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId); becomes the condition's NodeId.</param> /// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId); becomes the condition's NodeId.</param>
/// <param name="equipmentNodeId">The equipment folder node ID the condition parents under.</param> /// <param name="equipmentNodeId">The equipment folder node ID the condition parents under.</param>
@@ -44,7 +44,7 @@ public interface IOpcUaAddressSpaceSink
void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false); void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false);
/// <summary> /// <summary>
/// Ensure a folder node exists under the given parent. Used by <c>Phase7Applier</c> to /// Ensure a folder node exists under the given parent. Used by <c>AddressSpaceApplier</c> to
/// materialise the UNS Area/Line/Equipment hierarchy in the address space. When /// materialise the UNS Area/Line/Equipment hierarchy in the address space. When
/// <paramref name="parentNodeId"/> is null the folder is parented under the namespace /// <paramref name="parentNodeId"/> is null the folder is parented under the namespace
/// root. Idempotent: calling twice with the same id is safe. /// root. Idempotent: calling twice with the same id is safe.
@@ -58,7 +58,7 @@ public interface IOpcUaAddressSpaceSink
/// Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under /// Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under
/// <paramref name="parentFolderNodeId"/> (or the namespace root when null). Created with /// <paramref name="parentFolderNodeId"/> (or the namespace root when null). Created with
/// Bad quality + null value; subsequent <see cref="WriteValue"/> calls update both. /// Bad quality + null value; subsequent <see cref="WriteValue"/> calls update both.
/// Used by <c>Phase7Applier</c> to materialise equipment-namespace tags ahead of any /// Used by <c>AddressSpaceApplier</c> to materialise equipment-namespace tags ahead of any
/// driver-side subscribe so OPC UA clients can browse them. Idempotent. /// driver-side subscribe so OPC UA clients can browse them. Idempotent.
/// </summary> /// </summary>
/// <param name="variableNodeId">The OPC UA node ID for the variable.</param> /// <param name="variableNodeId">The OPC UA node ID for the variable.</param>
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa; namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
/// <summary>Optional capability on an address-space sink: surgical in-place attribute updates on an /// <summary>Optional capability on an address-space sink: surgical in-place attribute updates on an
/// EXISTING variable node, used by Phase7Applier to avoid a full RebuildAddressSpace for pure-property /// EXISTING variable node, used by AddressSpaceApplier to avoid a full RebuildAddressSpace for pure-property
/// tag changes (Writable / Historizing). A sink that does not implement it ⇒ caller falls back to a /// tag changes (Writable / Historizing). A sink that does not implement it ⇒ caller falls back to a
/// full rebuild (safe default).</summary> /// full rebuild (safe default).</summary>
public interface ISurgicalAddressSpaceSink public interface ISurgicalAddressSpaceSink
@@ -88,7 +88,7 @@ public static class EquipmentScriptPaths
/// <summary> /// <summary>
/// Distinct <c>ctx.GetTag("ref")</c> string literals in first-seen order — the /// Distinct <c>ctx.GetTag("ref")</c> string literals in first-seen order — the
/// dependency refs the <c>VirtualTagActor</c> subscribes to. The single shared copy /// dependency refs the <c>VirtualTagActor</c> subscribes to. The single shared copy
/// formerly duplicated in <c>Phase7Composer</c> + <c>DeploymentArtifact</c>. GetTag /// formerly duplicated in <c>AddressSpaceComposer</c> + <c>DeploymentArtifact</c>. GetTag
/// only (writes are not dependencies). /// only (writes are not dependencies).
/// </summary> /// </summary>
/// <param name="scriptSource">The (already substituted) script source.</param> /// <param name="scriptSource">The (already substituted) script source.</param>
@@ -112,7 +112,7 @@ public static class EquipmentScriptPaths
/// UNION the distinct <c>{TagPath}</c> token paths referenced in the message template (first-seen /// UNION the distinct <c>{TagPath}</c> token paths referenced in the message template (first-seen
/// order, appended after the predicate reads, trimmed + non-empty). The reserved /// order, appended after the predicate reads, trimmed + non-empty). The reserved
/// <c>{{equip}}</c> double-brace form is excluded by the token regex. Deterministic so the live /// <c>{{equip}}</c> double-brace form is excluded by the token regex. Deterministic so the live
/// composer (<c>Phase7Composer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>) /// composer (<c>AddressSpaceComposer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>)
/// produce the exact same ordered list — the byte-parity contract <c>EquipmentScriptedAlarmPlan</c> /// produce the exact same ordered list — the byte-parity contract <c>EquipmentScriptedAlarmPlan</c>
/// equality depends on. Scripted alarms do NOT use <c>{{equip}}</c> substitution (only virtual /// equality depends on. Scripted alarms do NOT use <c>{{equip}}</c> substitution (only virtual
/// tags do) — pass the predicate source as-is. /// tags do) — pass the predicate source as-is.
@@ -54,7 +54,7 @@ public static class DraftValidator
} }
// Minimal reader for the top-level "FullName" string in a tag's schemaless TagConfig JSON // Minimal reader for the top-level "FullName" string in a tag's schemaless TagConfig JSON
// (mirrors Phase7Composer.ExtractTagFullName — a small local copy, consistent with this codebase // (mirrors AddressSpaceComposer.ExtractTagFullName — a small local copy, consistent with this codebase
// where the composer keeps its own). // where the composer keeps its own).
private static string? ExtractTagConfigFullName(string? tagConfig) private static string? ExtractTagConfigFullName(string? tagConfig)
{ {
@@ -139,7 +139,7 @@
@* Driver-agnostic server-side HistoryRead intent. Distinct from the native-alarm @* Driver-agnostic server-side HistoryRead intent. Distinct from the native-alarm
"Historize to AVEVA" toggle below: THIS gates TAG-VALUE history (root keys "Historize to AVEVA" toggle below: THIS gates TAG-VALUE history (root keys
`isHistorized` / `historianTagname`, read by Phase7Composer.ExtractTagHistorize), `isHistorized` / `historianTagname`, read by AddressSpaceComposer.ExtractTagHistorize),
merged onto the canonical TagConfig via the pure TagHistorizeConfig seam so it merged onto the canonical TagConfig via the pure TagHistorizeConfig seam so it
composes with the typed editor's driver-specific fields (both preserve unknown keys). composes with the typed editor's driver-specific fields (both preserve unknown keys).
Shown for EVERY driver once one is picked. *@ Shown for EVERY driver once one is picked. *@
@@ -41,15 +41,15 @@ public sealed record ScriptTagInfo(string Path, string Kind, string DataType, st
/// <para> /// <para>
/// <b>Fidelity over breadth.</b> Verified: the live runtime resolves a <c>ctx.GetTag("X")</c> /// <b>Fidelity over breadth.</b> Verified: the live runtime resolves a <c>ctx.GetTag("X")</c>
/// literal against the driver <c>FullName</c> — the resolution chain is /// literal against the driver <c>FullName</c> — the resolution chain is
/// <c>Phase7Composer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals /// <c>AddressSpaceComposer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals
/// into <c>EquipmentVirtualTagPlan.DependencyRefs</c> /// into <c>EquipmentVirtualTagPlan.DependencyRefs</c>
/// (<c>src/Server/…OpcUaServer/Phase7Composer.cs</c>); those become /// (<c>src/Server/…OpcUaServer/AddressSpaceComposer.cs</c>); those become
/// <c>VirtualTagActor._dependencyRefs</c>, registered with the /// <c>VirtualTagActor._dependencyRefs</c>, registered with the
/// <c>DependencyMuxActor</c>, whose <c>_byRef</c> map is keyed by /// <c>DependencyMuxActor</c>, whose <c>_byRef</c> map is keyed by
/// <c>DriverInstanceActor.AttributeValuePublished.FullReference</c> /// <c>DriverInstanceActor.AttributeValuePublished.FullReference</c>
/// (<c>src/Server/…Runtime/VirtualTags/DependencyMuxActor.cs:97</c>) — and that /// (<c>src/Server/…Runtime/VirtualTags/DependencyMuxActor.cs:97</c>) — and that
/// <c>FullReference</c> is the <c>FullName</c> field extracted from <c>Tag.TagConfig</c> /// <c>FullReference</c> is the <c>FullName</c> field extracted from <c>Tag.TagConfig</c>
/// (see <c>Phase7Composer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>). /// (see <c>AddressSpaceComposer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>).
/// The UNS-path engine (<c>Core.VirtualTags.VirtualTagEngine</c>, keyed by a slash-joined /// The UNS-path engine (<c>Core.VirtualTags.VirtualTagEngine</c>, keyed by a slash-joined
/// <c>Enterprise/Site/Area/Line/Equipment/TagName</c>) is dormant — it is NOT wired into the /// <c>Enterprise/Site/Area/Line/Equipment/TagName</c>) is dormant — it is NOT wired into the
/// host — so UNS browse paths never resolve at runtime and are intentionally NOT suggested. /// host — so UNS browse paths never resolve at runtime and are intentionally NOT suggested.
@@ -175,7 +175,7 @@ public sealed class ScriptTagCatalog(IDbContextFactory<OtOpcUaConfigDbContext> d
/// <summary> /// <summary>
/// Extracts the driver-side full reference from a <c>Tag.TagConfig</c> JSON blob — the /// Extracts the driver-side full reference from a <c>Tag.TagConfig</c> JSON blob — the
/// top-level <c>FullName</c> string every shipped driver stores. Mirrors /// top-level <c>FullName</c> string every shipped driver stores. Mirrors
/// <c>EquipmentNodeWalker.ExtractFullName</c> / <c>Phase7Composer.ExtractTagFullName</c> /// <c>EquipmentNodeWalker.ExtractFullName</c> / <c>AddressSpaceComposer.ExtractTagFullName</c>
/// (AdminUI does not reference those assemblies). Falls back to the raw blob when it is not /// (AdminUI does not reference those assemblies). Falls back to the raw blob when it is not
/// a JSON object carrying a string <c>FullName</c>. /// a JSON object carrying a string <c>FullName</c>.
/// </summary> /// </summary>
@@ -223,7 +223,7 @@ public sealed class ScriptAnalysisService
if (inv.Expression is not MemberAccessExpressionSyntax ma) return false; if (inv.Expression is not MemberAccessExpressionSyntax ma) return false;
// Receiver guard: only ctx.GetTag(...) / ctx.SetVirtualTag(...) are real tag-path calls. Mirrors the // Receiver guard: only ctx.GetTag(...) / ctx.SetVirtualTag(...) are real tag-path calls. Mirrors the
// runtime harvest (EquipmentScriptPaths.GetTagRefRegex is syntactically `ctx`-anchored), so the editor // runtime harvest (EquipmentScriptPaths.GetTagRefRegex is syntactically `ctx`-anchored), so the editor
// offers tag completions/hover for exactly what Phase7Composer harvests — not an unrelated x.GetTag(...). // offers tag completions/hover for exactly what AddressSpaceComposer harvests — not an unrelated x.GetTag(...).
if (ma.Expression is not IdentifierNameSyntax { Identifier.ValueText: "ctx" }) return false; if (ma.Expression is not IdentifierNameSyntax { Identifier.ValueText: "ctx" }) return false;
var method = ma.Name.Identifier.ValueText; var method = ma.Name.Identifier.ValueText;
if (method is not ("GetTag" or "SetVirtualTag")) return false; if (method is not ("GetTag" or "SetVirtualTag")) return false;
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// the driver reads against). Preserves unrecognised JSON keys across a load→save.</summary> /// the driver reads against). Preserves unrecognised JSON keys across a load→save.</summary>
/// <remarks> /// <remarks>
/// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker /// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker
/// (<c>Phase7Composer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a /// (<c>AddressSpaceComposer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
/// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact /// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
/// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> / /// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> /
/// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam /// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// Typed working model for the optional native-alarm <c>alarm</c> sub-object inside a tag's /// Typed working model for the optional native-alarm <c>alarm</c> sub-object inside a tag's
/// <c>TagConfig</c> JSON. A tag whose <c>TagConfig</c> carries an <c>alarm</c> object is materialised /// <c>TagConfig</c> JSON. A tag whose <c>TagConfig</c> carries an <c>alarm</c> object is materialised
/// as an OPC UA Part 9 condition (rather than a value variable); the fields here mirror what the /// as an OPC UA Part 9 condition (rather than a value variable); the fields here mirror what the
/// server's <c>Phase7Composer.ExtractTagAlarm</c> / <c>DeploymentArtifact.ExtractTagAlarm</c> parse. /// server's <c>AddressSpaceComposer.ExtractTagAlarm</c> / <c>DeploymentArtifact.ExtractTagAlarm</c> parse.
/// ///
/// <para> /// <para>
/// <see cref="HistorizeToAveva"/> is the per-condition opt-out of the DURABLE AVEVA historian write /// <see cref="HistorizeToAveva"/> is the per-condition opt-out of the DURABLE AVEVA historian write
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// Preserves unrecognised JSON keys across a load→save.</summary> /// Preserves unrecognised JSON keys across a load→save.</summary>
/// <remarks> /// <remarks>
/// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker /// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker
/// (<c>Phase7Composer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a /// (<c>AddressSpaceComposer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
/// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact /// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
/// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> / /// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> /
/// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam /// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// Pure, driver-agnostic merge helper for the two server-side HistoryRead intent keys at the ROOT of a /// Pure, driver-agnostic merge helper for the two server-side HistoryRead intent keys at the ROOT of a
/// tag's <c>TagConfig</c> JSON: <c>isHistorized</c> (camelCase bool — omit/false default) and /// tag's <c>TagConfig</c> JSON: <c>isHistorized</c> (camelCase bool — omit/false default) and
/// <c>historianTagname</c> (camelCase string override — omit when blank). These map to what the server's /// <c>historianTagname</c> (camelCase string override — omit when blank). These map to what the server's
/// <c>Phase7Composer.ExtractTagHistorize</c> reads (see <c>docs/Historian.md</c>). /// <c>AddressSpaceComposer.ExtractTagHistorize</c> reads (see <c>docs/Historian.md</c>).
/// ///
/// <para> /// <para>
/// This is the TagModal-merge seam: the TagModal owns the canonical TagConfig JSON; the driver's typed /// This is the TagModal-merge seam: the TagModal owns the canonical TagConfig JSON; the driver's typed
@@ -4,9 +4,9 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary> /// <summary>
/// Side-effecting orchestrator over <see cref="Phase7Plan"/>. Drives an /// Side-effecting orchestrator over <see cref="AddressSpacePlan"/>. Drives an
/// <see cref="IOpcUaAddressSpaceSink"/> to materialise the diff between two /// <see cref="IOpcUaAddressSpaceSink"/> to materialise the diff between two
/// <see cref="Phase7CompositionResult"/> snapshots: /// <see cref="AddressSpaceComposition"/> snapshots:
/// ///
/// <list type="bullet"> /// <list type="bullet">
/// <item>RemovedEquipment / RemovedAlarms — write Bad-quality on every removed /// <item>RemovedEquipment / RemovedAlarms — write Bad-quality on every removed
@@ -23,15 +23,15 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// production binds a real SDK sink, dev/Mac binds <see cref="NullOpcUaAddressSpaceSink"/>, /// production binds a real SDK sink, dev/Mac binds <see cref="NullOpcUaAddressSpaceSink"/>,
/// and tests can capture every call. /// and tests can capture every call.
/// </summary> /// </summary>
public sealed class Phase7Applier public sealed class AddressSpaceApplier
{ {
private readonly IOpcUaAddressSpaceSink _sink; private readonly IOpcUaAddressSpaceSink _sink;
private readonly ILogger<Phase7Applier> _logger; private readonly ILogger<AddressSpaceApplier> _logger;
/// <summary>Initializes a new instance of the Phase7Applier class.</summary> /// <summary>Initializes a new instance of the AddressSpaceApplier class.</summary>
/// <param name="sink">The OPC UA address space sink to apply changes to.</param> /// <param name="sink">The OPC UA address space sink to apply changes to.</param>
/// <param name="logger">The logger instance.</param> /// <param name="logger">The logger instance.</param>
public Phase7Applier(IOpcUaAddressSpaceSink sink, ILogger<Phase7Applier> logger) public AddressSpaceApplier(IOpcUaAddressSpaceSink sink, ILogger<AddressSpaceApplier> logger)
{ {
ArgumentNullException.ThrowIfNull(sink); ArgumentNullException.ThrowIfNull(sink);
ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(logger);
@@ -44,15 +44,15 @@ public sealed class Phase7Applier
/// callers (OpcUaPublishActor) can correlate the work back to the originating deployment. /// callers (OpcUaPublishActor) can correlate the work back to the originating deployment.
/// </summary> /// </summary>
/// <param name="plan">The plan to apply.</param> /// <param name="plan">The plan to apply.</param>
/// <returns>A Phase7ApplyOutcome summarizing the applied changes.</returns> /// <returns>A AddressSpaceApplyOutcome summarizing the applied changes.</returns>
public Phase7ApplyOutcome Apply(Phase7Plan plan) public AddressSpaceApplyOutcome Apply(AddressSpacePlan plan)
{ {
ArgumentNullException.ThrowIfNull(plan); ArgumentNullException.ThrowIfNull(plan);
if (plan.IsEmpty) if (plan.IsEmpty)
{ {
_logger.LogDebug("Phase7Applier: plan is empty; skipping sink writes"); _logger.LogDebug("AddressSpaceApplier: plan is empty; skipping sink writes");
return new Phase7ApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false); return new AddressSpaceApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
} }
var ts = DateTime.UtcNow; var ts = DateTime.UtcNow;
@@ -68,7 +68,7 @@ public sealed class Phase7Applier
removedCount++; removedCount++;
} }
// Removed equipment tags / VirtualTags are plain variable nodes (no Part 9 condition to write // Removed equipment tags / VirtualTags are plain variable nodes (no Part 9 condition to write
// before tear-down), but they ARE real removals — count them so Phase7ApplyOutcome.RemovedNodes // before tear-down), but they ARE real removals — count them so AddressSpaceApplyOutcome.RemovedNodes
// is accurate on a removed-tag-only deploy, which now reaches the rebuild path below. // is accurate on a removed-tag-only deploy, which now reaches the rebuild path below.
removedCount += plan.RemovedEquipmentTags.Count + plan.RemovedEquipmentVirtualTags.Count; removedCount += plan.RemovedEquipmentTags.Count + plan.RemovedEquipmentVirtualTags.Count;
@@ -136,7 +136,7 @@ public sealed class Phase7Applier
try { ok = surgical.UpdateTagAttributes(nodeId, writable, historian); } try { ok = surgical.UpdateTagAttributes(nodeId, writable, historian); }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Phase7Applier: surgical UpdateTagAttributes threw for {Node}", nodeId); _logger.LogError(ex, "AddressSpaceApplier: surgical UpdateTagAttributes threw for {Node}", nodeId);
ok = false; ok = false;
} }
if (!ok) { allApplied = false; break; } if (!ok) { allApplied = false; break; }
@@ -152,10 +152,10 @@ public sealed class Phase7Applier
} }
_logger.LogInformation( _logger.LogInformation(
"Phase7Applier: applied plan (added={Added}, removed={Removed}, changed={Changed}, surgicalTags={Surgical}, rebuild={Rebuild})", "AddressSpaceApplier: applied plan (added={Added}, removed={Removed}, changed={Changed}, surgicalTags={Surgical}, rebuild={Rebuild})",
addedCount, removedCount, changedCount, rebuilt ? 0 : surgicalTagDeltas.Count, rebuilt); addedCount, removedCount, changedCount, rebuilt ? 0 : surgicalTagDeltas.Count, rebuilt);
return new Phase7ApplyOutcome(removedCount, addedCount, changedCount, rebuilt); return new AddressSpaceApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
} }
private void SafeRebuild() private void SafeRebuild()
@@ -166,7 +166,7 @@ public sealed class Phase7Applier
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Phase7Applier: sink.RebuildAddressSpace threw"); _logger.LogError(ex, "AddressSpaceApplier: sink.RebuildAddressSpace threw");
} }
} }
@@ -178,7 +178,7 @@ public sealed class Phase7Applier
/// present, so re-applies are cheap. /// present, so re-applies are cheap.
/// </summary> /// </summary>
/// <param name="composition">The composition result containing the hierarchy to materialise.</param> /// <param name="composition">The composition result containing the hierarchy to materialise.</param>
public void MaterialiseHierarchy(Phase7CompositionResult composition) public void MaterialiseHierarchy(AddressSpaceComposition composition)
{ {
ArgumentNullException.ThrowIfNull(composition); ArgumentNullException.ThrowIfNull(composition);
@@ -198,7 +198,7 @@ public sealed class Phase7Applier
} }
_logger.LogInformation( _logger.LogInformation(
"Phase7Applier: hierarchy materialised (areas={Areas}, lines={Lines}, equipment={Equipment})", "AddressSpaceApplier: hierarchy materialised (areas={Areas}, lines={Lines}, equipment={Equipment})",
composition.UnsAreas.Count, composition.UnsLines.Count, composition.EquipmentNodes.Count); composition.UnsAreas.Count, composition.UnsLines.Count, composition.EquipmentNodes.Count);
} }
@@ -223,7 +223,7 @@ public sealed class Phase7Applier
/// </para> /// </para>
/// </summary> /// </summary>
/// <param name="composition">The composition result containing the equipment tags to materialise.</param> /// <param name="composition">The composition result containing the equipment tags to materialise.</param>
public void MaterialiseEquipmentTags(Phase7CompositionResult composition) public void MaterialiseEquipmentTags(AddressSpaceComposition composition)
{ {
ArgumentNullException.ThrowIfNull(composition); ArgumentNullException.ThrowIfNull(composition);
if (composition.EquipmentTags.Count == 0) return; if (composition.EquipmentTags.Count == 0) return;
@@ -276,7 +276,7 @@ public sealed class Phase7Applier
} }
_logger.LogInformation( _logger.LogInformation(
"Phase7Applier: equipment tags materialised (tags={Tags}, equipment={Equipment})", "AddressSpaceApplier: equipment tags materialised (tags={Tags}, equipment={Equipment})",
composition.EquipmentTags.Count, composition.EquipmentTags.Count,
composition.EquipmentTags.Select(t => t.EquipmentId).Distinct(StringComparer.Ordinal).Count()); composition.EquipmentTags.Select(t => t.EquipmentId).Distinct(StringComparer.Ordinal).Count());
} }
@@ -294,7 +294,7 @@ public sealed class Phase7Applier
/// Idempotent (per-variable idempotency relies on the sink's own <c>EnsureVariable</c>). /// Idempotent (per-variable idempotency relies on the sink's own <c>EnsureVariable</c>).
/// </summary> /// </summary>
/// <param name="composition">The composition result containing the equipment VirtualTags to materialise.</param> /// <param name="composition">The composition result containing the equipment VirtualTags to materialise.</param>
public void MaterialiseEquipmentVirtualTags(Phase7CompositionResult composition) public void MaterialiseEquipmentVirtualTags(AddressSpaceComposition composition)
{ {
ArgumentNullException.ThrowIfNull(composition); ArgumentNullException.ThrowIfNull(composition);
if (composition.EquipmentVirtualTags.Count == 0) return; if (composition.EquipmentVirtualTags.Count == 0) return;
@@ -328,7 +328,7 @@ public sealed class Phase7Applier
} }
_logger.LogInformation( _logger.LogInformation(
"Phase7Applier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})", "AddressSpaceApplier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})",
composition.EquipmentVirtualTags.Count, composition.EquipmentVirtualTags.Count,
composition.EquipmentVirtualTags.Select(v => v.EquipmentId).Distinct(StringComparer.Ordinal).Count()); composition.EquipmentVirtualTags.Select(v => v.EquipmentId).Distinct(StringComparer.Ordinal).Count());
} }
@@ -343,7 +343,7 @@ public sealed class Phase7Applier
/// <c>MaterialiseAlarmCondition</c> re-creates cleanly on re-apply). /// <c>MaterialiseAlarmCondition</c> re-creates cleanly on re-apply).
/// </summary> /// </summary>
/// <param name="composition">The composition result containing the scripted alarms to materialise.</param> /// <param name="composition">The composition result containing the scripted alarms to materialise.</param>
public void MaterialiseScriptedAlarms(Phase7CompositionResult composition) public void MaterialiseScriptedAlarms(AddressSpaceComposition composition)
{ {
ArgumentNullException.ThrowIfNull(composition); ArgumentNullException.ThrowIfNull(composition);
if (composition.EquipmentScriptedAlarms.Count == 0) return; if (composition.EquipmentScriptedAlarms.Count == 0) return;
@@ -357,7 +357,7 @@ public sealed class Phase7Applier
} }
_logger.LogInformation( _logger.LogInformation(
"Phase7Applier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})", "AddressSpaceApplier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
materialised, materialised,
composition.EquipmentScriptedAlarms.Where(a => a.Enabled) composition.EquipmentScriptedAlarms.Where(a => a.Enabled)
.Select(a => a.EquipmentId).Distinct(StringComparer.Ordinal).Count()); .Select(a => a.EquipmentId).Distinct(StringComparer.Ordinal).Count());
@@ -366,13 +366,13 @@ public sealed class Phase7Applier
private void SafeEnsureFolder(string nodeId, string? parentNodeId, string displayName) private void SafeEnsureFolder(string nodeId, string? parentNodeId, string displayName)
{ {
try { _sink.EnsureFolder(nodeId, parentNodeId, displayName); } try { _sink.EnsureFolder(nodeId, parentNodeId, displayName); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureFolder threw for {Node}", nodeId); } catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureFolder threw for {Node}", nodeId); }
} }
private void SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) private void SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
{ {
try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); } try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureVariable threw for {Node}", nodeId); } catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureVariable threw for {Node}", nodeId); }
} }
// A VirtualTag's materialised OPC UA node (MaterialiseEquipmentVirtualTags) is derived ONLY from // A VirtualTag's materialised OPC UA node (MaterialiseEquipmentVirtualTags) is derived ONLY from
@@ -381,7 +381,7 @@ public sealed class Phase7Applier
// so a delta changing ONLY those three leaves a byte-identical node and needs no address-space rebuild. // so a delta changing ONLY those three leaves a byte-identical node and needs no address-space rebuild.
// Whitelist-of-may-differ via `with` + the record's custom Equals: any OTHER field difference (current // Whitelist-of-may-differ via `with` + the record's custom Equals: any OTHER field difference (current
// or future) makes the override unequal → falls back to a full rebuild (safe default). // or future) makes the override unequal → falls back to a full rebuild (safe default).
private static bool VtagDeltaIsNodeIrrelevant(Phase7Plan.EquipmentVirtualTagDelta d) => private static bool VtagDeltaIsNodeIrrelevant(AddressSpacePlan.EquipmentVirtualTagDelta d) =>
(d.Previous with (d.Previous with
{ {
Expression = d.Current.Expression, Expression = d.Current.Expression,
@@ -394,7 +394,7 @@ public sealed class Phase7Applier
// ISurgicalAddressSpaceSink.UpdateTagAttributes, avoiding a full rebuild (preserving subscriptions). // ISurgicalAddressSpaceSink.UpdateTagAttributes, avoiding a full rebuild (preserving subscriptions).
// DataType / IsArray / ArrayLength / FullName / DriverInstanceId / identity / alarm differences fall // DataType / IsArray / ArrayLength / FullName / DriverInstanceId / identity / alarm differences fall
// through to a rebuild — the override-unequal default also covers any future field. // through to a rebuild — the override-unequal default also covers any future field.
private static bool TagDeltaIsSurgicalEligible(Phase7Plan.EquipmentTagDelta d) => private static bool TagDeltaIsSurgicalEligible(AddressSpacePlan.EquipmentTagDelta d) =>
d.Previous.Alarm is null && d.Current.Alarm is null && d.Previous.Alarm is null && d.Current.Alarm is null &&
(d.Previous with (d.Previous with
{ {
@@ -418,18 +418,18 @@ public sealed class Phase7Applier
private void SafeWriteAlarmCondition(string nodeId, AlarmConditionSnapshot state, DateTime ts) private void SafeWriteAlarmCondition(string nodeId, AlarmConditionSnapshot state, DateTime ts)
{ {
try { _sink.WriteAlarmCondition(nodeId, state, ts); } try { _sink.WriteAlarmCondition(nodeId, state, ts); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: WriteAlarmCondition threw for {Node}", nodeId); } catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: WriteAlarmCondition threw for {Node}", nodeId); }
} }
private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative) private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative)
{ {
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); } try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); }
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); } catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
} }
} }
/// <summary>Summary of one apply pass. Useful for tests + audit-log entries on the deploy path.</summary> /// <summary>Summary of one apply pass. Useful for tests + audit-log entries on the deploy path.</summary>
public sealed record Phase7ApplyOutcome( public sealed record AddressSpaceApplyOutcome(
int RemovedNodes, int RemovedNodes,
int AddedNodes, int AddedNodes,
int ChangedNodes, int ChangedNodes,
@@ -6,11 +6,11 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary>Outcome of <see cref="Phase7Composer.Compose"/> — pure value tuple, no side effects. /// <summary>Outcome of <see cref="AddressSpaceComposer.Compose"/> — pure value tuple, no side effects.
/// <see cref="UnsAreas"/> + <see cref="UnsLines"/> carry the UNS topology so the applier can /// <see cref="UnsAreas"/> + <see cref="UnsLines"/> carry the UNS topology so the applier can
/// materialise the Area/Line/Equipment folder hierarchy in the address space; equipment carries /// materialise the Area/Line/Equipment folder hierarchy in the address space; equipment carries
/// its parent line id so the applier knows where to hang each equipment folder.</summary> /// its parent line id so the applier knows where to hang each equipment folder.</summary>
public sealed record Phase7CompositionResult( public sealed record AddressSpaceComposition(
IReadOnlyList<UnsAreaProjection> UnsAreas, IReadOnlyList<UnsAreaProjection> UnsAreas,
IReadOnlyList<UnsLineProjection> UnsLines, IReadOnlyList<UnsLineProjection> UnsLines,
IReadOnlyList<EquipmentNode> EquipmentNodes, IReadOnlyList<EquipmentNode> EquipmentNodes,
@@ -21,7 +21,7 @@ public sealed record Phase7CompositionResult(
/// <param name="equipmentNodes">The equipment nodes.</param> /// <param name="equipmentNodes">The equipment nodes.</param>
/// <param name="driverInstancePlans">The driver instance plans.</param> /// <param name="driverInstancePlans">The driver instance plans.</param>
/// <param name="scriptedAlarmPlans">The scripted alarm plans.</param> /// <param name="scriptedAlarmPlans">The scripted alarm plans.</param>
public Phase7CompositionResult( public AddressSpaceComposition(
IReadOnlyList<EquipmentNode> equipmentNodes, IReadOnlyList<EquipmentNode> equipmentNodes,
IReadOnlyList<DriverInstancePlan> driverInstancePlans, IReadOnlyList<DriverInstancePlan> driverInstancePlans,
IReadOnlyList<ScriptedAlarmPlan> scriptedAlarmPlans) IReadOnlyList<ScriptedAlarmPlan> scriptedAlarmPlans)
@@ -32,7 +32,7 @@ public sealed record Phase7CompositionResult(
/// <summary> /// <summary>
/// Equipment-namespace tags — a <see cref="Tag"/> with non-null <see cref="Tag.EquipmentId"/> /// Equipment-namespace tags — a <see cref="Tag"/> with non-null <see cref="Tag.EquipmentId"/>
/// in an <c>Equipment</c>-kind namespace. <c>Phase7Applier.MaterialiseEquipmentTags</c> /// in an <c>Equipment</c>-kind namespace. <c>AddressSpaceApplier.MaterialiseEquipmentTags</c>
/// materialises each as a Variable under its existing equipment folder. Declared as an /// materialises each as a Variable under its existing equipment folder. Declared as an
/// init-only member defaulting to empty (rather than a positional parameter) so every existing /// init-only member defaulting to empty (rather than a positional parameter) so every existing
/// convenience constructor + call site keeps compiling unchanged; new producers set it via /// convenience constructor + call site keeps compiling unchanged; new producers set it via
@@ -66,7 +66,7 @@ public sealed record ScriptedAlarmPlan(string ScriptedAlarmId, string EquipmentI
/// One Equipment-namespace tag from a <see cref="Tag"/> row whose <see cref="Tag.EquipmentId"/> /// One Equipment-namespace tag from a <see cref="Tag"/> row whose <see cref="Tag.EquipmentId"/>
/// is non-null and whose owning driver's namespace is <c>Equipment</c>-kind. Carries the stable /// is non-null and whose owning driver's namespace is <c>Equipment</c>-kind. Carries the stable
/// <see cref="TagId"/> (diff identity), the parent <see cref="EquipmentId"/> folder (already /// <see cref="TagId"/> (diff identity), the parent <see cref="EquipmentId"/> folder (already
/// materialised by <c>Phase7Applier.MaterialiseHierarchy</c>) the variable hangs under, the /// materialised by <c>AddressSpaceApplier.MaterialiseHierarchy</c>) the variable hangs under, the
/// optional <see cref="FolderPath"/> sub-folder, the leaf <see cref="Name"/> display, the OPC UA /// optional <see cref="FolderPath"/> sub-folder, the leaf <see cref="Name"/> display, the OPC UA
/// <see cref="DataType"/>, and the driver-side <see cref="FullName"/> reference (extracted from /// <see cref="DataType"/>, and the driver-side <see cref="FullName"/> reference (extracted from
/// <c>Tag.TagConfig</c>) the later values milestone routes reads/writes by. The variable's NodeId /// <c>Tag.TagConfig</c>) the later values milestone routes reads/writes by. The variable's NodeId
@@ -117,7 +117,7 @@ public sealed record EquipmentTagAlarmInfo(string AlarmType, int Severity, bool?
/// <summary> /// <summary>
/// One Equipment-namespace VirtualTag from a <see cref="VirtualTag"/> row (joined to its /// One Equipment-namespace VirtualTag from a <see cref="VirtualTag"/> row (joined to its
/// <see cref="Script"/> for the expression). The VirtualTag value analogue of /// <see cref="Script"/> for the expression). The VirtualTag value analogue of
/// <see cref="EquipmentTagPlan"/>: <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c> /// <see cref="EquipmentTagPlan"/>: <c>AddressSpaceApplier.MaterialiseEquipmentVirtualTags</c>
/// materialises each as a Variable under its equipment folder with a folder-scoped NodeId /// materialises each as a Variable under its equipment folder with a folder-scoped NodeId
/// (<c>EquipmentId/Name</c>, or <c>EquipmentId/FolderPath/Name</c> when a sub-folder is set), /// (<c>EquipmentId/Name</c>, or <c>EquipmentId/FolderPath/Name</c> when a sub-folder is set),
/// and <c>VirtualTagHostActor</c> spawns a <c>VirtualTagActor</c> per plan that evaluates /// and <c>VirtualTagHostActor</c> spawns a <c>VirtualTagActor</c> per plan that evaluates
@@ -252,19 +252,19 @@ public sealed record EquipmentScriptedAlarmPlan(
/// startup (Task 53) consumes the result and hands it to the node-manager factory. /// startup (Task 53) consumes the result and hands it to the node-manager factory.
/// ///
/// #85 — the composer now carries UNS topology (<see cref="UnsAreaProjection"/> + /// #85 — the composer now carries UNS topology (<see cref="UnsAreaProjection"/> +
/// <see cref="UnsLineProjection"/>) so <c>Phase7Applier</c> can build the /// <see cref="UnsLineProjection"/>) so <c>AddressSpaceApplier</c> can build the
/// <c>Area/Line/Equipment</c> folder hierarchy in the SDK's address space. The legacy /// <c>Area/Line/Equipment</c> folder hierarchy in the SDK's address space. The legacy
/// <c>EquipmentNodeWalker</c> integration that did this server-side is fully replaced by the /// <c>EquipmentNodeWalker</c> integration that did this server-side is fully replaced by the
/// (composer → applier → sink → node manager) chain. /// (composer → applier → sink → node manager) chain.
/// </summary> /// </summary>
public static class Phase7Composer public static class AddressSpaceComposer
{ {
/// <summary>Convenience overload for legacy callers + tests that don't supply UNS topology or tags.</summary> /// <summary>Convenience overload for legacy callers + tests that don't supply UNS topology or tags.</summary>
/// <param name="equipment">The equipment.</param> /// <param name="equipment">The equipment.</param>
/// <param name="driverInstances">The driver instances.</param> /// <param name="driverInstances">The driver instances.</param>
/// <param name="scriptedAlarms">The scripted alarms.</param> /// <param name="scriptedAlarms">The scripted alarms.</param>
/// <returns>The composition result.</returns> /// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose( public static AddressSpaceComposition Compose(
IReadOnlyList<Equipment> equipment, IReadOnlyList<Equipment> equipment,
IReadOnlyList<DriverInstance> driverInstances, IReadOnlyList<DriverInstance> driverInstances,
IReadOnlyList<ScriptedAlarm> scriptedAlarms) => IReadOnlyList<ScriptedAlarm> scriptedAlarms) =>
@@ -278,7 +278,7 @@ public static class Phase7Composer
/// <param name="driverInstances">The driver instances.</param> /// <param name="driverInstances">The driver instances.</param>
/// <param name="scriptedAlarms">The scripted alarms.</param> /// <param name="scriptedAlarms">The scripted alarms.</param>
/// <returns>The composition result.</returns> /// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose( public static AddressSpaceComposition Compose(
IReadOnlyList<UnsArea> unsAreas, IReadOnlyList<UnsArea> unsAreas,
IReadOnlyList<UnsLine> unsLines, IReadOnlyList<UnsLine> unsLines,
IReadOnlyList<Equipment> equipment, IReadOnlyList<Equipment> equipment,
@@ -300,7 +300,7 @@ public static class Phase7Composer
/// <param name="virtualTags">The Equipment-namespace virtual (calculated) tags. <c>null</c> = none.</param> /// <param name="virtualTags">The Equipment-namespace virtual (calculated) tags. <c>null</c> = none.</param>
/// <param name="scripts">The scripts joined to <paramref name="virtualTags"/> by ScriptId for the expression. <c>null</c> = none.</param> /// <param name="scripts">The scripts joined to <paramref name="virtualTags"/> by ScriptId for the expression. <c>null</c> = none.</param>
/// <returns>The composition result.</returns> /// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose( public static AddressSpaceComposition Compose(
IReadOnlyList<UnsArea> unsAreas, IReadOnlyList<UnsArea> unsAreas,
IReadOnlyList<UnsLine> unsLines, IReadOnlyList<UnsLine> unsLines,
IReadOnlyList<Equipment> equipment, IReadOnlyList<Equipment> equipment,
@@ -434,7 +434,7 @@ public static class Phase7Composer
if (!scriptsById.TryGetValue(a.PredicateScriptId, out var s)) if (!scriptsById.TryGetValue(a.PredicateScriptId, out var s))
{ {
Trace.TraceWarning( Trace.TraceWarning(
"Phase7Composer: scripted alarm '{0}' (equipment '{1}') references predicate " + "AddressSpaceComposer: scripted alarm '{0}' (equipment '{1}') references predicate " +
"script '{2}' which is not in the supplied scripts — skipping.", "script '{2}' which is not in the supplied scripts — skipping.",
a.ScriptedAlarmId, a.EquipmentId, a.PredicateScriptId); a.ScriptedAlarmId, a.EquipmentId, a.PredicateScriptId);
continue; continue;
@@ -458,7 +458,7 @@ public static class Phase7Composer
Enabled: a.Enabled)); Enabled: a.Enabled));
} }
return new Phase7CompositionResult(areas, lines, nodes, plans, alarms) return new AddressSpaceComposition(areas, lines, nodes, plans, alarms)
{ {
EquipmentTags = equipmentTags, EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags, EquipmentVirtualTags = equipmentVirtualTags,
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary> /// <summary>
/// Pure diff between two <see cref="Phase7CompositionResult"/> snapshots — the /// Pure diff between two <see cref="AddressSpaceComposition"/> snapshots — the
/// <c>previous</c> currently-applied composition and the <c>next</c> from a freshly-applied /// <c>previous</c> currently-applied composition and the <c>next</c> from a freshly-applied
/// deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm) /// deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm)
/// captured by stable identity: added items are new, removed items have to be torn down, /// captured by stable identity: added items are new, removed items have to be torn down,
@@ -12,22 +12,22 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or /// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or
/// drastic schema flips. /// drastic schema flips.
/// </summary> /// </summary>
public sealed record Phase7Plan( public sealed record AddressSpacePlan(
IReadOnlyList<EquipmentNode> AddedEquipment, IReadOnlyList<EquipmentNode> AddedEquipment,
IReadOnlyList<EquipmentNode> RemovedEquipment, IReadOnlyList<EquipmentNode> RemovedEquipment,
IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment, IReadOnlyList<AddressSpacePlan.EquipmentDelta> ChangedEquipment,
IReadOnlyList<DriverInstancePlan> AddedDrivers, IReadOnlyList<DriverInstancePlan> AddedDrivers,
IReadOnlyList<DriverInstancePlan> RemovedDrivers, IReadOnlyList<DriverInstancePlan> RemovedDrivers,
IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers, IReadOnlyList<AddressSpacePlan.DriverDelta> ChangedDrivers,
IReadOnlyList<ScriptedAlarmPlan> AddedAlarms, IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms, IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms) IReadOnlyList<AddressSpacePlan.AlarmDelta> ChangedAlarms)
{ {
/// <summary> /// <summary>
/// Equipment-namespace tag diff sets, keyed by <see cref="EquipmentTagPlan.TagId"/>. Added as /// Equipment-namespace tag diff sets, keyed by <see cref="EquipmentTagPlan.TagId"/>. Added as
/// init-only members (defaulting empty) rather than positional parameters so existing /// init-only members (defaulting empty) rather than positional parameters so existing
/// <c>Phase7Plan</c> construction sites compile unchanged — consistent with how /// <c>AddressSpacePlan</c> construction sites compile unchanged — consistent with how
/// <see cref="Phase7CompositionResult.EquipmentTags"/> was added. Without these, an /// <see cref="AddressSpaceComposition.EquipmentTags"/> was added. Without these, an
/// incremental deploy that changes ONLY equipment tags produced an empty plan and /// incremental deploy that changes ONLY equipment tags produced an empty plan and
/// <c>OpcUaPublishActor.HandleRebuild</c> short-circuited before materialising them. /// <c>OpcUaPublishActor.HandleRebuild</c> short-circuited before materialising them.
/// </summary> /// </summary>
@@ -67,7 +67,7 @@ public sealed record Phase7Plan(
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current); public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
} }
public static class Phase7Planner public static class AddressSpacePlanner
{ {
/// <summary> /// <summary>
/// Diff two compositions, emitting Added/Removed/Changed sets per entity class. /// Diff two compositions, emitting Added/Removed/Changed sets per entity class.
@@ -77,7 +77,7 @@ public static class Phase7Planner
/// </summary> /// </summary>
/// <param name="previous">The previous composition result.</param> /// <param name="previous">The previous composition result.</param>
/// <param name="next">The new composition result.</param> /// <param name="next">The new composition result.</param>
public static Phase7Plan Compute(Phase7CompositionResult previous, Phase7CompositionResult next) public static AddressSpacePlan Compute(AddressSpaceComposition previous, AddressSpaceComposition next)
{ {
ArgumentNullException.ThrowIfNull(previous); ArgumentNullException.ThrowIfNull(previous);
ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(next);
@@ -85,22 +85,22 @@ public static class Phase7Planner
var (addedEq, removedEq, changedEq) = DiffById( var (addedEq, removedEq, changedEq) = DiffById(
previous.EquipmentNodes, next.EquipmentNodes, previous.EquipmentNodes, next.EquipmentNodes,
n => n.EquipmentId, n => n.EquipmentId,
(a, b) => new Phase7Plan.EquipmentDelta(a, b)); (a, b) => new AddressSpacePlan.EquipmentDelta(a, b));
var (addedDrv, removedDrv, changedDrv) = DiffById( var (addedDrv, removedDrv, changedDrv) = DiffById(
previous.DriverInstancePlans, next.DriverInstancePlans, previous.DriverInstancePlans, next.DriverInstancePlans,
d => d.DriverInstanceId, d => d.DriverInstanceId,
(a, b) => new Phase7Plan.DriverDelta(a, b)); (a, b) => new AddressSpacePlan.DriverDelta(a, b));
var (addedAlarm, removedAlarm, changedAlarm) = DiffById( var (addedAlarm, removedAlarm, changedAlarm) = DiffById(
previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans, previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans,
a => a.ScriptedAlarmId, a => a.ScriptedAlarmId,
(a, b) => new Phase7Plan.AlarmDelta(a, b)); (a, b) => new AddressSpacePlan.AlarmDelta(a, b));
var (addedEqTags, removedEqTags, changedEqTags) = DiffById( var (addedEqTags, removedEqTags, changedEqTags) = DiffById(
previous.EquipmentTags, next.EquipmentTags, previous.EquipmentTags, next.EquipmentTags,
t => t.TagId, t => t.TagId,
(a, b) => new Phase7Plan.EquipmentTagDelta(a, b)); (a, b) => new AddressSpacePlan.EquipmentTagDelta(a, b));
// VirtualTags diff by VirtualTagId, mirroring the EquipmentTags pass. EquipmentVirtualTagPlan // VirtualTags diff by VirtualTagId, mirroring the EquipmentTags pass. EquipmentVirtualTagPlan
// overrides record equality to compare ALL fields by value — scalars (Expression/DataType/ // overrides record equality to compare ALL fields by value — scalars (Expression/DataType/
@@ -110,9 +110,9 @@ public static class Phase7Planner
var (addedVTags, removedVTags, changedVTags) = DiffById( var (addedVTags, removedVTags, changedVTags) = DiffById(
previous.EquipmentVirtualTags, next.EquipmentVirtualTags, previous.EquipmentVirtualTags, next.EquipmentVirtualTags,
t => t.VirtualTagId, t => t.VirtualTagId,
(a, b) => new Phase7Plan.EquipmentVirtualTagDelta(a, b)); (a, b) => new AddressSpacePlan.EquipmentVirtualTagDelta(a, b));
return new Phase7Plan( return new AddressSpacePlan(
addedEq, removedEq, changedEq, addedEq, removedEq, changedEq,
addedDrv, removedDrv, changedDrv, addedDrv, removedDrv, changedDrv,
addedAlarm, removedAlarm, changedAlarm) addedAlarm, removedAlarm, changedAlarm)
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// ///
/// Node-id encoding uses the manager's default namespace + the caller-supplied string id /// Node-id encoding uses the manager's default namespace + the caller-supplied string id
/// as the identifier portion (e.g. <c>"ns=2;s=eq-1/temp"</c>). Equipment-folder hierarchy /// as the identifier portion (e.g. <c>"ns=2;s=eq-1/temp"</c>). Equipment-folder hierarchy
/// and OPC UA type metadata still come from the Phase7Applier / EquipmentNodeWalker /// and OPC UA type metadata still come from the AddressSpaceApplier / EquipmentNodeWalker
/// integration (F14b, tracked under #85) — this manager treats every id as a flat /// integration (F14b, tracked under #85) — this manager treats every id as a flat
/// <see cref="BaseDataVariableState"/> under the namespace root. /// <see cref="BaseDataVariableState"/> under the namespace root.
/// </summary> /// </summary>
@@ -1252,7 +1252,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// <summary> /// <summary>
/// Ensure a folder node exists at <paramref name="folderNodeId"/> with the given display /// Ensure a folder node exists at <paramref name="folderNodeId"/> with the given display
/// name, parented under <paramref name="parentNodeId"/> (or the namespace root when null). /// name, parented under <paramref name="parentNodeId"/> (or the namespace root when null).
/// #85 — used by <see cref="Phase7Applier"/> to materialise the UNS Area/Line/Equipment /// #85 — used by <see cref="AddressSpaceApplier"/> to materialise the UNS Area/Line/Equipment
/// folder hierarchy. Idempotent: the second call with the same id returns the cached /// folder hierarchy. Idempotent: the second call with the same id returns the cached
/// folder so adding child variables under it still works. /// folder so adding child variables under it still works.
/// </summary> /// </summary>
@@ -1424,7 +1424,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
_ => DataTypeIds.BaseDataType, _ => DataTypeIds.BaseDataType,
}; };
/// <summary>Clear every registered variable + folder from the address space. Phase7Applier /// <summary>Clear every registered variable + folder from the address space. AddressSpaceApplier
/// calls this when Equipment/Alarm topology changes; the populator then re-adds via /// calls this when Equipment/Alarm topology changes; the populator then re-adds via
/// EnsureFolder + WriteValue on the next pass.</summary> /// EnsureFolder + WriteValue on the next pass.</summary>
public void RebuildAddressSpace() public void RebuildAddressSpace()
@@ -169,8 +169,8 @@ public static class DeploymentArtifact
} }
/// <summary> /// <summary>
/// Parse the artifact into the projected <see cref="Phase7CompositionResult"/> used by /// Parse the artifact into the projected <see cref="AddressSpaceComposition"/> used by
/// <c>Phase7Planner</c> + <c>Phase7Applier</c>. Returns an empty composition for empty/ /// <c>AddressSpacePlanner</c> + <c>AddressSpaceApplier</c>. Returns an empty composition for empty/
/// malformed blobs so callers can treat parse failure as a no-op deploy. /// malformed blobs so callers can treat parse failure as a no-op deploy.
/// ///
/// The artifact JSON is produced by <c>ConfigComposer.SnapshotAndFlattenAsync</c> in the /// The artifact JSON is produced by <c>ConfigComposer.SnapshotAndFlattenAsync</c> in the
@@ -179,7 +179,7 @@ public static class DeploymentArtifact
/// nodes. /// nodes.
/// </summary> /// </summary>
/// <param name="blob">The deployment artifact blob to parse.</param> /// <param name="blob">The deployment artifact blob to parse.</param>
public static Phase7CompositionResult ParseComposition(ReadOnlySpan<byte> blob) public static AddressSpaceComposition ParseComposition(ReadOnlySpan<byte> blob)
{ {
if (blob.IsEmpty) return Empty(); if (blob.IsEmpty) return Empty();
@@ -197,7 +197,7 @@ public static class DeploymentArtifact
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags); var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags);
var equipmentScriptedAlarms = BuildEquipmentScriptedAlarmPlans(root); var equipmentScriptedAlarms = BuildEquipmentScriptedAlarmPlans(root);
return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms) return new AddressSpaceComposition(areas, lines, equipment, drivers, alarms)
{ {
EquipmentTags = equipmentTags, EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags, EquipmentVirtualTags = equipmentVirtualTags,
@@ -224,7 +224,7 @@ public static class DeploymentArtifact
/// <param name="nodeId">This node's identity in "host:port" form.</param> /// <param name="nodeId">This node's identity in "host:port" form.</param>
/// <param name="onInconsistency">Optional diagnostic callback for cross-cluster orphan bindings; null disables the check.</param> /// <param name="onInconsistency">Optional diagnostic callback for cross-cluster orphan bindings; null disables the check.</param>
/// <returns>The filtered composition per the node's scoping decision.</returns> /// <returns>The filtered composition per the node's scoping decision.</returns>
public static Phase7CompositionResult ParseComposition( public static AddressSpaceComposition ParseComposition(
ReadOnlySpan<byte> blob, string nodeId, Action<string>? onInconsistency = null) ReadOnlySpan<byte> blob, string nodeId, Action<string>? onInconsistency = null)
{ {
var scope = ResolveClusterScope(blob, nodeId); var scope = ResolveClusterScope(blob, nodeId);
@@ -253,7 +253,7 @@ public static class DeploymentArtifact
} }
} }
return new Phase7CompositionResult( return new AddressSpaceComposition(
full.UnsAreas.Where(a => sets.AreaIds.Contains(a.UnsAreaId)).ToArray(), full.UnsAreas.Where(a => sets.AreaIds.Contains(a.UnsAreaId)).ToArray(),
keptLines, keptLines,
keptEquipment, keptEquipment,
@@ -348,7 +348,7 @@ public static class DeploymentArtifact
} }
} }
private static Phase7CompositionResult Empty() => new( private static AddressSpaceComposition Empty() => new(
Array.Empty<UnsAreaProjection>(), Array.Empty<UnsAreaProjection>(),
Array.Empty<UnsLineProjection>(), Array.Empty<UnsLineProjection>(),
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(),
@@ -359,7 +359,7 @@ public static class DeploymentArtifact
/// Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find /// Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find
/// Equipment-namespace tags (non-null EquipmentId, owning namespace Kind == Equipment), then /// Equipment-namespace tags (non-null EquipmentId, owning namespace Kind == Equipment), then
/// emit one <see cref="EquipmentTagPlan"/> per qualifying tag. The artifact-decode mirror of /// emit one <see cref="EquipmentTagPlan"/> per qualifying tag. The artifact-decode mirror of
/// <c>Phase7Composer.Compose</c>'s equipment filter — so the compose-side + artifact-decode /// <c>AddressSpaceComposer.Compose</c>'s equipment filter — so the compose-side + artifact-decode
/// plans agree on the same set of tags. FullName is read from each tag's TagConfig blob /// plans agree on the same set of tags. FullName is read from each tag's TagConfig blob
/// (top-level "FullName" field). /// (top-level "FullName" field).
/// </summary> /// </summary>
@@ -421,7 +421,7 @@ public static class DeploymentArtifact
// AccessLevel → Writable. ConfigComposer serialises the TagAccessLevel enum WITHOUT a // AccessLevel → Writable. ConfigComposer serialises the TagAccessLevel enum WITHOUT a
// string converter, so it lands as a number (Read = 0, ReadWrite = 1); tolerate the string // string converter, so it lands as a number (Read = 0, ReadWrite = 1); tolerate the string
// form ("ReadWrite") too — same defensive both-forms parse as the Kind gate above. MUST match // form ("ReadWrite") too — same defensive both-forms parse as the Kind gate above. MUST match
// Phase7Composer's `AccessLevel == TagAccessLevel.ReadWrite` exactly (byte-parity). A missing // AddressSpaceComposer's `AccessLevel == TagAccessLevel.ReadWrite` exactly (byte-parity). A missing
// field defaults to non-writable (read-only). // field defaults to non-writable (read-only).
var writable = el.TryGetProperty("AccessLevel", out var alEl) && alEl.ValueKind switch var writable = el.TryGetProperty("AccessLevel", out var alEl) && alEl.ValueKind switch
{ {
@@ -469,7 +469,7 @@ public static class DeploymentArtifact
/// <summary> /// <summary>
/// Join the artifact's VirtualTags array to its Scripts array (by ScriptId) to emit one /// Join the artifact's VirtualTags array to its Scripts array (by ScriptId) to emit one
/// <see cref="EquipmentVirtualTagPlan"/> per VirtualTag. The artifact-decode mirror of /// <see cref="EquipmentVirtualTagPlan"/> per VirtualTag. The artifact-decode mirror of
/// <c>Phase7Composer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode /// <c>AddressSpaceComposer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode
/// plans agree. The reserved <c>{{equip}}</c> token in the joined Script's <c>SourceCode</c> is /// plans agree. The reserved <c>{{equip}}</c> token in the joined Script's <c>SourceCode</c> is
/// substituted with the owning equipment's tag base (derived from <paramref name="equipmentTags"/>' /// substituted with the owning equipment's tag base (derived from <paramref name="equipmentTags"/>'
/// FullNames) BEFORE refs are extracted, byte-parity with the composer. <c>Expression</c> = the /// FullNames) BEFORE refs are extracted, byte-parity with the composer. <c>Expression</c> = the
@@ -529,14 +529,14 @@ public static class DeploymentArtifact
// Historize: the artifact carries a Pascal-case "Historize" bool (ConfigComposer serialises // Historize: the artifact carries a Pascal-case "Historize" bool (ConfigComposer serialises
// the whole VirtualTag entity with DefaultIgnoreCondition.Never). Robust parse — default // the whole VirtualTag entity with DefaultIgnoreCondition.Never). Robust parse — default
// false; only honoured when the JSON value is an actual boolean — so absent/non-bool ⇒ false, // false; only honoured when the JSON value is an actual boolean — so absent/non-bool ⇒ false,
// byte-parity with Phase7Composer's entity-default-false behaviour. // byte-parity with AddressSpaceComposer's entity-default-false behaviour.
var historize = el.TryGetProperty("Historize", out var hEl) var historize = el.TryGetProperty("Historize", out var hEl)
&& (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False) && (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False)
&& hEl.GetBoolean(); && hEl.GetBoolean();
// Substitute the {{equip}} token with the owning equipment's tag base BEFORE extracting // Substitute the {{equip}} token with the owning equipment's tag base BEFORE extracting
// refs, so both Expression and DependencyRefs are machine-specific — byte-parity with // refs, so both Expression and DependencyRefs are machine-specific — byte-parity with
// Phase7Composer.Compose. // AddressSpaceComposer.Compose.
var expanded = EquipmentScriptPaths.SubstituteEquipmentToken( var expanded = EquipmentScriptPaths.SubstituteEquipmentToken(
source, baseByEquip.GetValueOrDefault(equipmentId!)); source, baseByEquip.GetValueOrDefault(equipmentId!));
@@ -562,7 +562,7 @@ public static class DeploymentArtifact
/// <summary> /// <summary>
/// Join the artifact's ScriptedAlarms array to its Scripts array (by PredicateScriptId) to emit /// Join the artifact's ScriptedAlarms array to its Scripts array (by PredicateScriptId) to emit
/// one <see cref="EquipmentScriptedAlarmPlan"/> per alarm. The artifact-decode mirror of /// one <see cref="EquipmentScriptedAlarmPlan"/> per alarm. The artifact-decode mirror of
/// <c>Phase7Composer.Compose</c>'s scripted-alarm producer — so the compose-side + artifact-decode /// <c>AddressSpaceComposer.Compose</c>'s scripted-alarm producer — so the compose-side + artifact-decode
/// plans agree byte-for-byte. An alarm whose <c>PredicateScriptId</c> has no matching Script row is /// plans agree byte-for-byte. An alarm whose <c>PredicateScriptId</c> has no matching Script row is
/// SKIPPED (matching the composer's skip behaviour) to preserve parity. <c>PredicateSource</c> = the /// SKIPPED (matching the composer's skip behaviour) to preserve parity. <c>PredicateSource</c> = the
/// joined script source ("" when missing — but such alarms are skipped above); <c>DependencyRefs</c> /// joined script source ("" when missing — but such alarms are skipped above); <c>DependencyRefs</c>
@@ -616,7 +616,7 @@ public static class DeploymentArtifact
if (string.IsNullOrWhiteSpace(scriptedAlarmId)) continue; if (string.IsNullOrWhiteSpace(scriptedAlarmId)) continue;
// Skip alarms whose predicate script is missing — matching Phase7Composer's skip behaviour // Skip alarms whose predicate script is missing — matching AddressSpaceComposer's skip behaviour
// so both sides emit the same set (byte-parity). // so both sides emit the same set (byte-parity).
if (predicateScriptId is null || !scriptSourceById.TryGetValue(predicateScriptId, out var source)) if (predicateScriptId is null || !scriptSourceById.TryGetValue(predicateScriptId, out var source))
continue; continue;
@@ -646,7 +646,7 @@ public static class DeploymentArtifact
/// <summary> /// <summary>
/// Extract the driver-side full reference from a tag's TagConfig JSON (top-level "FullName" /// Extract the driver-side full reference from a tag's TagConfig JSON (top-level "FullName"
/// field). The artifact-decode mirror of <c>Phase7Composer.ExtractTagFullName</c> / /// field). The artifact-decode mirror of <c>AddressSpaceComposer.ExtractTagFullName</c> /
/// <c>EquipmentNodeWalker.ExtractFullName</c> — replicated because Runtime does not reference /// <c>EquipmentNodeWalker.ExtractFullName</c> — replicated because Runtime does not reference
/// the Core driver assembly. Falls back to the raw blob when absent or non-JSON. /// the Core driver assembly. Falls back to the raw blob when absent or non-JSON.
/// </summary> /// </summary>
@@ -669,7 +669,7 @@ public static class DeploymentArtifact
/// <summary>Parses the optional <c>alarm</c> object from a tag's <c>TagConfig</c> JSON. Returns null /// <summary>Parses the optional <c>alarm</c> object from a tag's <c>TagConfig</c> JSON. Returns null
/// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The /// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The
/// live-edit side (<c>Phase7Composer.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary> /// live-edit side (<c>AddressSpaceComposer.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>
private static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig) private static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig)
{ {
if (string.IsNullOrWhiteSpace(tagConfig)) return null; if (string.IsNullOrWhiteSpace(tagConfig)) return null;
@@ -683,7 +683,7 @@ public static class DeploymentArtifact
var sev = a.TryGetProperty("severity", out var sEl) && sEl.ValueKind == JsonValueKind.Number var sev = a.TryGetProperty("severity", out var sEl) && sEl.ValueKind == JsonValueKind.Number
&& sEl.TryGetInt32(out var sv) ? sv : 500; && sEl.TryGetInt32(out var sv) ? sv : 500;
// historizeToAveva (bool?, absent ⇒ null ⇒ historize): byte-parity with // historizeToAveva (bool?, absent ⇒ null ⇒ historize): byte-parity with
// Phase7Composer.ExtractTagAlarm — only an explicit false suppresses the durable AVEVA write. // AddressSpaceComposer.ExtractTagAlarm — only an explicit false suppresses the durable AVEVA write.
bool? historize = a.TryGetProperty("historizeToAveva", out var hEl) bool? historize = a.TryGetProperty("historizeToAveva", out var hEl)
&& hEl.ValueKind is JsonValueKind.True or JsonValueKind.False && hEl.ValueKind is JsonValueKind.True or JsonValueKind.False
? hEl.GetBoolean() ? hEl.GetBoolean()
@@ -699,7 +699,7 @@ public static class DeploymentArtifact
/// whitespace-or-empty ⇒ <c>null</c>, meaning the historian tagname defaults to the tag's FullName, /// whitespace-or-empty ⇒ <c>null</c>, meaning the historian tagname defaults to the tag's FullName,
/// resolved later). The raw string value is used — not trimmed — matching <c>ExtractTagFullName</c> / /// resolved later). The raw string value is used — not trimmed — matching <c>ExtractTagFullName</c> /
/// <c>ExtractTagAlarm</c>. Never throws. The live-edit composer side /// <c>ExtractTagAlarm</c>. Never throws. The live-edit composer side
/// (<c>Phase7Composer.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary> /// (<c>AddressSpaceComposer.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig) private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig)
{ {
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null); if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
@@ -727,7 +727,7 @@ public static class DeploymentArtifact
/// <c>arrayLength</c> uint (honoured ONLY when <c>isArray</c> is true AND the prop is a JSON number /// <c>arrayLength</c> uint (honoured ONLY when <c>isArray</c> is true AND the prop is a JSON number
/// that fits <c>uint</c>; else <c>null</c>). Mirrors <see cref="ExtractTagHistorize"/> in structure + /// that fits <c>uint</c>; else <c>null</c>). Mirrors <see cref="ExtractTagHistorize"/> in structure +
/// null/blank/non-object/malformed-JSON tolerance. Never throws. The live-edit composer side /// null/blank/non-object/malformed-JSON tolerance. Never throws. The live-edit composer side
/// (<c>Phase7Composer.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary> /// (<c>AddressSpaceComposer.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary>
private static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig) private static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig)
{ {
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null); if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
@@ -764,7 +764,7 @@ public static class DeploymentArtifact
var item = reader(el); var item = reader(el);
if (item is not null) result.Add(item); if (item is not null) result.Add(item);
} }
// Match Phase7Composer's natural-key sort so plan diffs are deterministic across // Match AddressSpaceComposer's natural-key sort so plan diffs are deterministic across
// artifact-decode + composer-compose passes. // artifact-decode + composer-compose passes.
return result.OrderBy(IdentityOf, StringComparer.Ordinal).ToList(); return result.OrderBy(IdentityOf, StringComparer.Ordinal).ToList();
} }
@@ -939,7 +939,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
return; return;
} }
Phase7CompositionResult composition; AddressSpaceComposition composition;
try try
{ {
composition = DeploymentArtifact.ParseComposition(blob, _localNode.Value); composition = DeploymentArtifact.ParseComposition(blob, _localNode.Value);
@@ -63,7 +63,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
private readonly bool _subscribeRedundancyTopic; private readonly bool _subscribeRedundancyTopic;
private readonly NodeId? _localNode; private readonly NodeId? _localNode;
private readonly IDbContextFactory<OtOpcUaConfigDbContext>? _dbFactory; private readonly IDbContextFactory<OtOpcUaConfigDbContext>? _dbFactory;
private readonly Phase7Applier? _applier; private readonly AddressSpaceApplier? _applier;
private readonly IActorRef? _dbHealthProbe; private readonly IActorRef? _dbHealthProbe;
private readonly TimeSpan _staleWindow; private readonly TimeSpan _staleWindow;
private readonly TimeSpan _probeFreshnessWindow; private readonly TimeSpan _probeFreshnessWindow;
@@ -77,7 +77,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
private DbHealthProbeActor.DbHealthStatus? _lastDbHealth; private DbHealthProbeActor.DbHealthStatus? _lastDbHealth;
private RedundancyStateChanged? _lastSnapshot; private RedundancyStateChanged? _lastSnapshot;
private (bool Ok, DateTime At)? _probeAboutMe; private (bool Ok, DateTime At)? _probeAboutMe;
private Phase7CompositionResult _lastApplied = new( private AddressSpaceComposition _lastApplied = new(
Array.Empty<UnsAreaProjection>(), Array.Empty<UnsAreaProjection>(),
Array.Empty<UnsLineProjection>(), Array.Empty<UnsLineProjection>(),
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(),
@@ -116,7 +116,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
IServiceLevelPublisher? serviceLevel = null, IServiceLevelPublisher? serviceLevel = null,
NodeId? localNode = null, NodeId? localNode = null,
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null, IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
Phase7Applier? applier = null, AddressSpaceApplier? applier = null,
IActorRef? dbHealthProbe = null, IActorRef? dbHealthProbe = null,
TimeSpan? staleWindow = null, TimeSpan? staleWindow = null,
TimeSpan? probeFreshnessWindow = null, TimeSpan? probeFreshnessWindow = null,
@@ -157,7 +157,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
bool subscribeRedundancyTopic = false, bool subscribeRedundancyTopic = false,
NodeId? localNode = null, NodeId? localNode = null,
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null, IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
Phase7Applier? applier = null, AddressSpaceApplier? applier = null,
IActorRef? dbHealthProbe = null, IActorRef? dbHealthProbe = null,
TimeSpan? staleWindow = null, TimeSpan? staleWindow = null,
TimeSpan? probeFreshnessWindow = null, TimeSpan? probeFreshnessWindow = null,
@@ -197,7 +197,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
bool subscribeRedundancyTopic, bool subscribeRedundancyTopic,
NodeId? localNode, NodeId? localNode,
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null, IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
Phase7Applier? applier = null, AddressSpaceApplier? applier = null,
IActorRef? dbHealthProbe = null, IActorRef? dbHealthProbe = null,
TimeSpan? staleWindow = null, TimeSpan? staleWindow = null,
TimeSpan? probeFreshnessWindow = null, TimeSpan? probeFreshnessWindow = null,
@@ -308,7 +308,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
? DeploymentArtifact.ParseComposition(artifact, ln.Value, ? DeploymentArtifact.ParseComposition(artifact, ln.Value,
inconsistency => _log.Warning("OpcUaPublish {Node}: cross-cluster binding — {Message}", ln, inconsistency)) inconsistency => _log.Warning("OpcUaPublish {Node}: cross-cluster binding — {Message}", ln, inconsistency))
: DeploymentArtifact.ParseComposition(artifact); : DeploymentArtifact.ParseComposition(artifact);
var plan = Phase7Planner.Compute(_lastApplied, composition); var plan = AddressSpacePlanner.Compute(_lastApplied, composition);
if (plan.IsEmpty) if (plan.IsEmpty)
{ {
@@ -321,7 +321,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
_lastApplied = composition; _lastApplied = composition;
// #85 — after the plan diff lands, rebuild the UNS folder hierarchy so OPC UA // #85 — after the plan diff lands, rebuild the UNS folder hierarchy so OPC UA
// clients see Area/Line/Equipment as proper folders. Idempotent; Phase7Applier // clients see Area/Line/Equipment as proper folders. Idempotent; AddressSpaceApplier
// skips folders that already exist with the same node id. // skips folders that already exist with the same node id.
_applier.MaterialiseHierarchy(composition); _applier.MaterialiseHierarchy(composition);
// T14 — scripted alarms get their own pass right after the hierarchy so the equipment // T14 — scripted alarms get their own pass right after the hierarchy so the equipment
@@ -206,9 +206,9 @@ public static class ServiceCollectionExtensions
registry.Register<DependencyMuxActorKey>(mux); registry.Register<DependencyMuxActorKey>(mux);
// OPC UA publish actor — pinned dispatcher, owns the address-space side of the // OPC UA publish actor — pinned dispatcher, owns the address-space side of the
// pipeline. Phase7Applier is constructed here so the actor + applier share the // pipeline. AddressSpaceApplier is constructed here so the actor + applier share the
// same sink reference (when DeferredAddressSpaceSink swaps later, both see it). // same sink reference (when DeferredAddressSpaceSink swaps later, both see it).
var applier = new Phase7Applier(addressSpaceSink, loggerFactory.CreateLogger<Phase7Applier>()); var applier = new AddressSpaceApplier(addressSpaceSink, loggerFactory.CreateLogger<AddressSpaceApplier>());
var publishActor = system.ActorOf( var publishActor = system.ActorOf(
OpcUaPublishActor.Props( OpcUaPublishActor.Props(
sink: addressSpaceSink, sink: addressSpaceSink,
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
/// ///
/// <para> /// <para>
/// The published NodeId is computed by the shared <see cref="EquipmentNodeIds.Variable"/> — /// The published NodeId is computed by the shared <see cref="EquipmentNodeIds.Variable"/> —
/// the single source of truth <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c> also /// the single source of truth <c>AddressSpaceApplier.MaterialiseEquipmentVirtualTags</c> also
/// materialises against — so the value always lands on a NodeId that exists. /// materialises against — so the value always lands on a NodeId that exists.
/// </para> /// </para>
/// </summary> /// </summary>
@@ -198,7 +198,7 @@ public sealed class VirtualTagHostActor : ReceiveActor
} }
/// <summary>Folder-scoped NodeId for a VirtualTag plan. The formula now lives in the shared /// <summary>Folder-scoped NodeId for a VirtualTag plan. The formula now lives in the shared
/// <see cref="EquipmentNodeIds"/> (the single source of truth that <c>Phase7Applier</c> also /// <see cref="EquipmentNodeIds"/> (the single source of truth that <c>AddressSpaceApplier</c> also
/// materialises against), so the published value always lands on the NodeId that was materialised.</summary> /// materialises against), so the published value always lands on the NodeId that was materialised.</summary>
private static string NodeIdFor(EquipmentVirtualTagPlan p) => private static string NodeIdFor(EquipmentVirtualTagPlan p) =>
EquipmentNodeIds.Variable(p.EquipmentId, p.FolderPath, p.Name); EquipmentNodeIds.Variable(p.EquipmentId, p.FolderPath, p.Name);
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
/// the plan decisions call for. Introspection only — no SQL Server required. /// the plan decisions call for. Introspection only — no SQL Server required.
/// </summary> /// </summary>
[Trait("Category", "Unit")] [Trait("Category", "Unit")]
public sealed class Phase7ScriptingEntitiesTests public sealed class ScriptingEntitiesTests
{ {
private static OtOpcUaConfigDbContext BuildCtx() private static OtOpcUaConfigDbContext BuildCtx()
{ {
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
/// seam that the OpcUaServer unit tests only approximate with hand-built JSON. /// seam that the OpcUaServer unit tests only approximate with hand-built JSON.
/// <para> /// <para>
/// The OPC UA address-space browse is exercised separately against a real SDK node manager in /// The OPC UA address-space browse is exercised separately against a real SDK node manager in
/// <c>Phase7ApplierHierarchyTests.Equipment_namespace_structure_materialises_end_to_end_against_real_SDK</c>, /// <c>AddressSpaceApplierHierarchyTests.Equipment_namespace_structure_materialises_end_to_end_against_real_SDK</c>,
/// because the in-process <see cref="TwoNodeClusterHarness"/> binds the no-op address-space sink. /// because the in-process <see cref="TwoNodeClusterHarness"/> binds the no-op address-space sink.
/// </para> /// </para>
/// </summary> /// </summary>
@@ -10,13 +10,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary> /// <summary>
/// #85 — verifies <see cref="Phase7Applier.MaterialiseHierarchy"/> builds the UNS /// #85 — verifies <see cref="AddressSpaceApplier.MaterialiseHierarchy"/> builds the UNS
/// Area/Line/Equipment folder tree through <see cref="IOpcUaAddressSpaceSink.EnsureFolder"/>. /// Area/Line/Equipment folder tree through <see cref="IOpcUaAddressSpaceSink.EnsureFolder"/>.
/// One pure unit test (recording sink) confirms ordering + parenting; one boot-verify test /// One pure unit test (recording sink) confirms ordering + parenting; one boot-verify test
/// drives a real <see cref="OtOpcUaNodeManager"/> and inspects the resulting predefined-node /// drives a real <see cref="OtOpcUaNodeManager"/> and inspects the resulting predefined-node
/// count to prove the folders land in the SDK address space. /// count to prove the folders land in the SDK address space.
/// </summary> /// </summary>
public sealed class Phase7ApplierHierarchyTests : IDisposable public sealed class AddressSpaceApplierHierarchyTests : IDisposable
{ {
private static CancellationToken Ct => TestContext.Current.CancellationToken; private static CancellationToken Ct => TestContext.Current.CancellationToken;
@@ -29,9 +29,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
public void MaterialiseHierarchy_creates_areas_then_lines_then_equipment_with_correct_parents() public void MaterialiseHierarchy_creates_areas_then_lines_then_equipment_with_correct_parents()
{ {
var sink = new RecordingFolderSink(); var sink = new RecordingFolderSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
UnsAreas: new[] { new UnsAreaProjection("area-1", "Plant North") }, UnsAreas: new[] { new UnsAreaProjection("area-1", "Plant North") },
UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell A") }, UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell A") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") }, EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") },
@@ -52,9 +52,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
public void MaterialiseHierarchy_orphan_equipment_hangs_under_root() public void MaterialiseHierarchy_orphan_equipment_hangs_under_root()
{ {
var sink = new RecordingFolderSink(); var sink = new RecordingFolderSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
UnsAreas: Array.Empty<UnsAreaProjection>(), UnsAreas: Array.Empty<UnsAreaProjection>(),
UnsLines: Array.Empty<UnsLineProjection>(), UnsLines: Array.Empty<UnsLineProjection>(),
EquipmentNodes: new[] { new EquipmentNode("eq-orphan", "Orphan", UnsLineId: "") }, EquipmentNodes: new[] { new EquipmentNode("eq-orphan", "Orphan", UnsLineId: "") },
@@ -86,9 +86,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
sdkServer.NodeManager.ShouldNotBeNull(); sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!); var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
applier.MaterialiseHierarchy(new Phase7CompositionResult( applier.MaterialiseHierarchy(new AddressSpaceComposition(
UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") }, UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") },
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") }, UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") }, EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
@@ -98,7 +98,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
sdkServer.NodeManager!.FolderCount.ShouldBe(5); // 2 areas + 1 line + 2 equipment sdkServer.NodeManager!.FolderCount.ShouldBe(5); // 2 areas + 1 line + 2 equipment
// Idempotent: re-applying with the same composition doesn't create duplicates. // Idempotent: re-applying with the same composition doesn't create duplicates.
applier.MaterialiseHierarchy(new Phase7CompositionResult( applier.MaterialiseHierarchy(new AddressSpaceComposition(
UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") }, UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") },
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") }, UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") }, EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
@@ -132,9 +132,9 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
sdkServer.NodeManager.ShouldNotBeNull(); sdkServer.NodeManager.ShouldNotBeNull();
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!); var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
UnsAreas: Array.Empty<UnsAreaProjection>(), UnsAreas: Array.Empty<UnsAreaProjection>(),
UnsLines: Array.Empty<UnsLineProjection>(), UnsLines: Array.Empty<UnsLineProjection>(),
EquipmentNodes: new[] { new EquipmentNode("eq-1", "filling-eq", UnsLineId: "") }, EquipmentNodes: new[] { new EquipmentNode("eq-1", "filling-eq", UnsLineId: "") },
@@ -157,7 +157,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
/// <summary> /// <summary>
/// Full structure-materialisation pipeline against a real SDK node manager: real Config /// Full structure-materialisation pipeline against a real SDK node manager: real Config
/// entities (Area / Line / Equipment + an Equipment-namespace Tag) → <see cref="Phase7Composer.Compose"/> /// entities (Area / Line / Equipment + an Equipment-namespace Tag) → <see cref="AddressSpaceComposer.Compose"/>
/// → MaterialiseHierarchy + MaterialiseEquipmentTags → <see cref="OtOpcUaNodeManager"/>. Proves /// → MaterialiseHierarchy + MaterialiseEquipmentTags → <see cref="OtOpcUaNodeManager"/>. Proves
/// an Equipment namespace lands its Area/Line/Equipment folder tree + the equipment-signal /// an Equipment namespace lands its Area/Line/Equipment folder tree + the equipment-signal
/// Variable in a live OPC UA address space (structure-only; live values are a later milestone). /// Variable in a live OPC UA address space (structure-only; live values are a later milestone).
@@ -192,7 +192,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
var equipment = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-modbus", UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001" }; var equipment = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-modbus", UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001" };
var tag = new Tag { TagId = "tag-speed", DriverInstanceId = "drv-modbus", EquipmentId = "eq-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"40001\"}" }; var tag = new Tag { TagId = "tag-speed", DriverInstanceId = "drv-modbus", EquipmentId = "eq-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"40001\"}" };
var composition = Phase7Composer.Compose( var composition = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equipment }, new[] { driver }, new[] { area }, new[] { line }, new[] { equipment }, new[] { driver },
Array.Empty<ScriptedAlarm>(), new[] { tag }, new[] { ns }); Array.Empty<ScriptedAlarm>(), new[] { tag }, new[] { ns });
@@ -202,7 +202,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
planned.FullName.ShouldBe("40001"); planned.FullName.ShouldBe("40001");
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!); var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
applier.MaterialiseHierarchy(composition); applier.MaterialiseHierarchy(composition);
applier.MaterialiseEquipmentTags(composition); applier.MaterialiseEquipmentTags(composition);
@@ -6,14 +6,14 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7ApplierTests public sealed class AddressSpaceApplierTests
{ {
/// <summary>Verifies that an empty plan does not call the sink or trigger a rebuild.</summary> /// <summary>Verifies that an empty plan does not call the sink or trigger a rebuild.</summary>
[Fact] [Fact]
public void Empty_plan_does_not_call_sink_and_does_not_trigger_rebuild() public void Empty_plan_does_not_call_sink_and_does_not_trigger_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var outcome = applier.Apply(EmptyPlan); var outcome = applier.Apply(EmptyPlan);
@@ -30,7 +30,7 @@ public sealed class Phase7ApplierTests
public void Removed_equipment_writes_inactive_alarm_state_per_id_and_triggers_rebuild() public void Removed_equipment_writes_inactive_alarm_state_per_id_and_triggers_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = WithEquipmentRemoval("eq-1", "eq-2"); var plan = WithEquipmentRemoval("eq-1", "eq-2");
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -48,18 +48,18 @@ public sealed class Phase7ApplierTests
public void Added_equipment_triggers_rebuild_without_alarm_writes() public void Added_equipment_triggers_rebuild_without_alarm_writes()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = new Phase7Plan( var plan = new AddressSpacePlan(
AddedEquipment: new[] { new EquipmentNode("new", "New", "line-1") }, AddedEquipment: new[] { new EquipmentNode("new", "New", "line-1") },
RemovedEquipment: Array.Empty<EquipmentNode>(), RemovedEquipment: Array.Empty<EquipmentNode>(),
ChangedEquipment: Array.Empty<Phase7Plan.EquipmentDelta>(), ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
AddedDrivers: Array.Empty<DriverInstancePlan>(), AddedDrivers: Array.Empty<DriverInstancePlan>(),
RemovedDrivers: Array.Empty<DriverInstancePlan>(), RemovedDrivers: Array.Empty<DriverInstancePlan>(),
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(), ChangedDrivers: Array.Empty<AddressSpacePlan.DriverDelta>(),
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(), AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(), RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>()); ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -74,23 +74,23 @@ public sealed class Phase7ApplierTests
public void Driver_only_changes_do_not_trigger_address_space_rebuild() public void Driver_only_changes_do_not_trigger_address_space_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = new Phase7Plan( var plan = new AddressSpacePlan(
AddedEquipment: Array.Empty<EquipmentNode>(), AddedEquipment: Array.Empty<EquipmentNode>(),
RemovedEquipment: Array.Empty<EquipmentNode>(), RemovedEquipment: Array.Empty<EquipmentNode>(),
ChangedEquipment: Array.Empty<Phase7Plan.EquipmentDelta>(), ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
AddedDrivers: new[] { new DriverInstancePlan("d-new", "Modbus", "{}") }, AddedDrivers: new[] { new DriverInstancePlan("d-new", "Modbus", "{}") },
RemovedDrivers: Array.Empty<DriverInstancePlan>(), RemovedDrivers: Array.Empty<DriverInstancePlan>(),
ChangedDrivers: new[] ChangedDrivers: new[]
{ {
new Phase7Plan.DriverDelta( new AddressSpacePlan.DriverDelta(
new DriverInstancePlan("d-1", "Modbus", "{\"v\":1}"), new DriverInstancePlan("d-1", "Modbus", "{\"v\":1}"),
new DriverInstancePlan("d-1", "Modbus", "{\"v\":2}")), new DriverInstancePlan("d-1", "Modbus", "{\"v\":2}")),
}, },
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(), AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(), RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>()); ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -103,7 +103,7 @@ public sealed class Phase7ApplierTests
public void Sink_exception_in_WriteAlarmCondition_does_not_propagate_and_rebuild_still_fires() public void Sink_exception_in_WriteAlarmCondition_does_not_propagate_and_rebuild_still_fires()
{ {
var sink = new ThrowingSink(throwOnAlarmWrite: true); var sink = new ThrowingSink(throwOnAlarmWrite: true);
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = WithEquipmentRemoval("eq-1"); var plan = WithEquipmentRemoval("eq-1");
@@ -120,9 +120,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_creates_variable_under_equipment_folder() public void MaterialiseEquipmentTags_creates_variable_under_equipment_folder()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
UnsAreas: Array.Empty<UnsAreaProjection>(), UnsAreas: Array.Empty<UnsAreaProjection>(),
UnsLines: Array.Empty<UnsLineProjection>(), UnsLines: Array.Empty<UnsLineProjection>(),
EquipmentNodes: Array.Empty<EquipmentNode>(), EquipmentNodes: Array.Empty<EquipmentNode>(),
@@ -151,9 +151,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_nests_FolderPath_subfolder_under_equipment() public void MaterialiseEquipmentTags_nests_FolderPath_subfolder_under_equipment()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -178,9 +178,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_identical_FullName_across_two_equipments_does_not_collide() public void MaterialiseEquipmentTags_identical_FullName_across_two_equipments_does_not_collide()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -206,9 +206,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_alarm_bearing_tag_becomes_condition_plain_tag_stays_variable() public void MaterialiseEquipmentTags_alarm_bearing_tag_becomes_condition_plain_tag_stays_variable()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -241,9 +241,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_alarm_bearing_tag_with_FolderPath_conditions_under_subfolder() public void MaterialiseEquipmentTags_alarm_bearing_tag_with_FolderPath_conditions_under_subfolder()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -271,9 +271,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_resolves_historian_tagname_default_override_and_null() public void MaterialiseEquipmentTags_resolves_historian_tagname_default_override_and_null()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -304,9 +304,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_blank_override_falls_back_to_full_name() public void MaterialiseEquipmentTags_blank_override_falls_back_to_full_name()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -324,16 +324,16 @@ public sealed class Phase7ApplierTests
} }
/// <summary>Array-support Task 2 — an <see cref="EquipmentTagPlan"/> with <c>IsArray: true, /// <summary>Array-support Task 2 — an <see cref="EquipmentTagPlan"/> with <c>IsArray: true,
/// ArrayLength: 16</c> flowing through <see cref="Phase7Applier.MaterialiseEquipmentTags"/> must /// ArrayLength: 16</c> flowing through <see cref="AddressSpaceApplier.MaterialiseEquipmentTags"/> must
/// forward BOTH flags verbatim to the sink's <c>EnsureVariable</c>. Guards against arg-order swaps or /// forward BOTH flags verbatim to the sink's <c>EnsureVariable</c>. Guards against arg-order swaps or
/// accidental drops in the wire-through.</summary> /// accidental drops in the wire-through.</summary>
[Fact] [Fact]
public void MaterialiseEquipmentTags_array_plan_forwards_isArray_and_arrayLength_to_sink() public void MaterialiseEquipmentTags_array_plan_forwards_isArray_and_arrayLength_to_sink()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -358,9 +358,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_scalar_plan_forwards_isArray_false_to_sink() public void MaterialiseEquipmentTags_scalar_plan_forwards_isArray_false_to_sink()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -387,9 +387,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_array_writable_true_is_forced_read_only() public void MaterialiseEquipmentTags_array_writable_true_is_forced_read_only()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -417,9 +417,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentTags_scalar_writable_true_stays_writable() public void MaterialiseEquipmentTags_scalar_writable_true_stays_writable()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -446,9 +446,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentVirtualTags_creates_variable_under_equipment_folder() public void MaterialiseEquipmentVirtualTags_creates_variable_under_equipment_folder()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -469,7 +469,7 @@ public sealed class Phase7ApplierTests
/// <summary>Golden/parity guard: the materialiser's Variable NodeId for BOTH the equipment-tag and /// <summary>Golden/parity guard: the materialiser's Variable NodeId for BOTH the equipment-tag and
/// the equipment-VirtualTag pass is byte-identical to <see cref="EquipmentNodeIds.Variable"/> — the /// the equipment-VirtualTag pass is byte-identical to <see cref="EquipmentNodeIds.Variable"/> — the
/// single source of truth Phase7Applier + VirtualTagHostActor both point at. Covers null/empty /// single source of truth AddressSpaceApplier + VirtualTagHostActor both point at. Covers null/empty
/// FolderPath (directly under equipment) and a non-empty FolderPath (sub-folder scoped). This test /// FolderPath (directly under equipment) and a non-empty FolderPath (sub-folder scoped). This test
/// LOCKS the formula against drift: any change to the materialiser NodeId that diverges from the /// LOCKS the formula against drift: any change to the materialiser NodeId that diverges from the
/// shared helper fails here.</summary> /// shared helper fails here.</summary>
@@ -477,9 +477,9 @@ public sealed class Phase7ApplierTests
public void Materialised_variable_node_ids_match_shared_EquipmentNodeIds_formula() public void Materialised_variable_node_ids_match_shared_EquipmentNodeIds_formula()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -512,9 +512,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseEquipmentVirtualTags_two_under_same_equipment_do_not_collide() public void MaterialiseEquipmentVirtualTags_two_under_same_equipment_do_not_collide()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -541,9 +541,9 @@ public sealed class Phase7ApplierTests
public void MaterialiseScriptedAlarms_materialises_enabled_and_skips_disabled() public void MaterialiseScriptedAlarms_materialises_enabled_and_skips_disabled()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var composition = new Phase7CompositionResult( var composition = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentScriptedAlarms = new[] EquipmentScriptedAlarms = new[]
@@ -574,7 +574,7 @@ public sealed class Phase7ApplierTests
public void Added_equipment_tags_trigger_rebuild() public void Added_equipment_tags_trigger_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = EmptyPlan with var plan = EmptyPlan with
{ {
@@ -598,7 +598,7 @@ public sealed class Phase7ApplierTests
public void Added_equipment_virtual_tags_trigger_rebuild() public void Added_equipment_virtual_tags_trigger_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var plan = EmptyPlan with var plan = EmptyPlan with
{ {
@@ -624,7 +624,7 @@ public sealed class Phase7ApplierTests
public void Changed_equipment_tags_only_trigger_rebuild() public void Changed_equipment_tags_only_trigger_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
@@ -632,7 +632,7 @@ public sealed class Phase7ApplierTests
var next = CompositionWithTags( var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Int32", FullName: "40001", Writable: true, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Int32", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedEquipmentTags is populated. // Guard the arrange: ONLY ChangedEquipmentTags is populated.
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
@@ -659,7 +659,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_expression_only_skips_rebuild() public void Changed_virtual_tag_expression_only_skips_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags( var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -669,7 +669,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\") * 120", DependencyRefs: new[] { "a" })); Expression: "ctx.GetTag(\"a\") * 120", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedEquipmentVirtualTags is populated. // Guard the arrange: ONLY ChangedEquipmentVirtualTags is populated.
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
@@ -690,7 +690,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_dependency_refs_only_skips_rebuild() public void Changed_virtual_tag_dependency_refs_only_skips_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags( var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -703,7 +703,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a", "b" })); Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a", "b" }));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -719,7 +719,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_historize_only_skips_rebuild() public void Changed_virtual_tag_historize_only_skips_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags( var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -729,7 +729,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }, Historize: true)); Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }, Historize: true));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -747,7 +747,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_data_type_change_still_rebuilds() public void Changed_virtual_tag_data_type_change_still_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags( var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -757,7 +757,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Int32", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Int32",
Expression: "ctx.GetTag(\"a\") * 60", DependencyRefs: new[] { "a" })); Expression: "ctx.GetTag(\"a\") * 60", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -772,7 +772,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_name_change_still_rebuilds() public void Changed_virtual_tag_name_change_still_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags( var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -781,7 +781,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "EfficiencyPct", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "EfficiencyPct", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" })); Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -796,7 +796,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_folder_path_change_still_rebuilds() public void Changed_virtual_tag_folder_path_change_still_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags( var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -805,7 +805,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "Calc", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "Calc", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" })); Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -822,9 +822,9 @@ public sealed class Phase7ApplierTests
public void Node_irrelevant_vtag_edit_mixed_with_another_change_still_rebuilds() public void Node_irrelevant_vtag_edit_mixed_with_another_change_still_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult( var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -838,7 +838,7 @@ public sealed class Phase7ApplierTests
}, },
}; };
// Expression-only vtag edit (node-irrelevant) AND a node-affecting tag DataType flip. // Expression-only vtag edit (node-irrelevant) AND a node-affecting tag DataType flip.
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -852,7 +852,7 @@ public sealed class Phase7ApplierTests
}, },
}; };
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Both a node-irrelevant vtag change AND a node-affecting tag change are present. // Both a node-irrelevant vtag change AND a node-affecting tag change are present.
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
@@ -873,7 +873,7 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tag_equipment_id_triggers_rebuild() public void Changed_virtual_tag_equipment_id_triggers_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithVirtualTags( var previous = CompositionWithVirtualTags(
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
@@ -884,7 +884,7 @@ public sealed class Phase7ApplierTests
new EquipmentVirtualTagPlan("vt-1", "eq-2", FolderPath: "", Name: "Efficiency", DataType: "Float64", new EquipmentVirtualTagPlan("vt-1", "eq-2", FolderPath: "", Name: "Efficiency", DataType: "Float64",
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" })); Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: the planner sees this as a changed vtag (same id, different EquipmentId). // Guard the arrange: the planner sees this as a changed vtag (same id, different EquipmentId).
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
@@ -907,9 +907,9 @@ public sealed class Phase7ApplierTests
public void Changed_virtual_tags_one_irrelevant_one_structural_triggers_rebuild() public void Changed_virtual_tags_one_irrelevant_one_structural_triggers_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult( var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -922,7 +922,7 @@ public sealed class Phase7ApplierTests
Expression: "ctx.GetTag(\"load\")", DependencyRefs: new[] { "load" }), Expression: "ctx.GetTag(\"load\")", DependencyRefs: new[] { "load" }),
}, },
}; };
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -936,7 +936,7 @@ public sealed class Phase7ApplierTests
}, },
}; };
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: both vtags are diffed as changed, nothing else. // Guard the arrange: both vtags are diffed as changed, nothing else.
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(2); plan.ChangedEquipmentVirtualTags.Count.ShouldBe(2);
@@ -961,13 +961,13 @@ public sealed class Phase7ApplierTests
public void Changed_alarms_only_trigger_rebuild() public void Changed_alarms_only_trigger_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithAlarms(new ScriptedAlarmPlan("alm-1", "eq-1", "scr-1", "Temp high")); var previous = CompositionWithAlarms(new ScriptedAlarmPlan("alm-1", "eq-1", "scr-1", "Temp high"));
// Same alarm id, edited message template — the planner classifies this as a change. // Same alarm id, edited message template — the planner classifies this as a change.
var next = CompositionWithAlarms(new ScriptedAlarmPlan("alm-1", "eq-1", "scr-1", "Temp critically high")); var next = CompositionWithAlarms(new ScriptedAlarmPlan("alm-1", "eq-1", "scr-1", "Temp critically high"));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedAlarms is populated. // Guard the arrange: ONLY ChangedAlarms is populated.
plan.ChangedAlarms.Count.ShouldBe(1); plan.ChangedAlarms.Count.ShouldBe(1);
@@ -991,12 +991,12 @@ public sealed class Phase7ApplierTests
public void Changed_drivers_only_do_not_trigger_rebuild() public void Changed_drivers_only_do_not_trigger_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithDrivers(new DriverInstancePlan("d-1", "Modbus", "{\"v\":1}")); var previous = CompositionWithDrivers(new DriverInstancePlan("d-1", "Modbus", "{\"v\":1}"));
var next = CompositionWithDrivers(new DriverInstancePlan("d-1", "Modbus", "{\"v\":2}")); var next = CompositionWithDrivers(new DriverInstancePlan("d-1", "Modbus", "{\"v\":2}"));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY ChangedDrivers is populated. // Guard the arrange: ONLY ChangedDrivers is populated.
plan.ChangedDrivers.Count.ShouldBe(1); plan.ChangedDrivers.Count.ShouldBe(1);
@@ -1017,15 +1017,15 @@ public sealed class Phase7ApplierTests
/// <summary>H1a (review follow-up) — a deploy that ONLY removes existing equipment tag / VirtualTag /// <summary>H1a (review follow-up) — a deploy that ONLY removes existing equipment tag / VirtualTag
/// nodes must rebuild AND tally the removals. Removed tags/VirtualTags are plain variable nodes (no /// nodes must rebuild AND tally the removals. Removed tags/VirtualTags are plain variable nodes (no
/// Part 9 condition to write), so before the fix they reached the rebuild path but were never added /// Part 9 condition to write), so before the fix they reached the rebuild path but were never added
/// to <c>removedCount</c> — <c>Phase7ApplyOutcome.RemovedNodes</c> reported 0, a misleading audit /// to <c>removedCount</c> — <c>AddressSpaceApplyOutcome.RemovedNodes</c> reported 0, a misleading audit
/// entry. This pins both the rebuild and the accurate count.</summary> /// entry. This pins both the rebuild and the accurate count.</summary>
[Fact] [Fact]
public void Removed_equipment_tags_and_virtual_tags_only_rebuild_and_are_counted() public void Removed_equipment_tags_and_virtual_tags_only_rebuild_and_are_counted()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult( var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -1038,10 +1038,10 @@ public sealed class Phase7ApplierTests
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }), Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }),
}, },
}; };
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: ONLY the two Removed sets are populated. // Guard the arrange: ONLY the two Removed sets are populated.
plan.RemovedEquipmentTags.Count.ShouldBe(1); plan.RemovedEquipmentTags.Count.ShouldBe(1);
@@ -1069,7 +1069,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_writable_only_skips_rebuild_and_updates_in_place() public void Changed_tag_writable_only_skips_rebuild_and_updates_in_place()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
@@ -1077,7 +1077,7 @@ public sealed class Phase7ApplierTests
var next = CompositionWithTags( var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
plan.AddedEquipmentTags.ShouldBeEmpty(); plan.AddedEquipmentTags.ShouldBeEmpty();
plan.RemovedEquipmentTags.ShouldBeEmpty(); plan.RemovedEquipmentTags.ShouldBeEmpty();
@@ -1101,8 +1101,8 @@ public sealed class Phase7ApplierTests
{ {
// false → true (no override) ⇒ historian defaults to FullName. // false → true (no override) ⇒ historian defaults to FullName.
var sinkOn = new RecordingSink(); var sinkOn = new RecordingSink();
var applierOn = new Phase7Applier(sinkOn, NullLogger<Phase7Applier>.Instance); var applierOn = new AddressSpaceApplier(sinkOn, NullLogger<AddressSpaceApplier>.Instance);
var planOn = Phase7Planner.Compute( var planOn = AddressSpacePlanner.Compute(
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
FullName: "T.A", Writable: false, Alarm: null, IsHistorized: false, HistorianTagname: null)), FullName: "T.A", Writable: false, Alarm: null, IsHistorized: false, HistorianTagname: null)),
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
@@ -1117,8 +1117,8 @@ public sealed class Phase7ApplierTests
// true → false ⇒ historian null. // true → false ⇒ historian null.
var sinkOff = new RecordingSink(); var sinkOff = new RecordingSink();
var applierOff = new Phase7Applier(sinkOff, NullLogger<Phase7Applier>.Instance); var applierOff = new AddressSpaceApplier(sinkOff, NullLogger<AddressSpaceApplier>.Instance);
var planOff = Phase7Planner.Compute( var planOff = AddressSpacePlanner.Compute(
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
FullName: "T.A", Writable: false, Alarm: null, IsHistorized: true, HistorianTagname: null)), FullName: "T.A", Writable: false, Alarm: null, IsHistorized: true, HistorianTagname: null)),
CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", CompositionWithTags(new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
@@ -1138,7 +1138,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_historian_tagname_only_skips_rebuild() public void Changed_tag_historian_tagname_only_skips_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
@@ -1147,7 +1147,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float",
FullName: "T.A", Writable: false, Alarm: null, IsHistorized: true, HistorianTagname: "WW.New")); FullName: "T.A", Writable: false, Alarm: null, IsHistorized: true, HistorianTagname: "WW.New"));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1163,7 +1163,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_data_type_change_rebuilds_and_no_surgical_call() public void Changed_tag_data_type_change_rebuilds_and_no_surgical_call()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
@@ -1171,7 +1171,7 @@ public sealed class Phase7ApplierTests
var next = CompositionWithTags( var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Int32", FullName: "40001", Writable: true, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Int32", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1187,7 +1187,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_is_array_change_rebuilds() public void Changed_tag_is_array_change_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16", new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16",
@@ -1196,7 +1196,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16", new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16",
FullName: "40001", Writable: false, Alarm: null, IsArray: true, ArrayLength: 16u)); FullName: "40001", Writable: false, Alarm: null, IsArray: true, ArrayLength: 16u));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1212,14 +1212,14 @@ public sealed class Phase7ApplierTests
public void Changed_tag_full_name_change_rebuilds() public void Changed_tag_full_name_change_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags( var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40002", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40002", Writable: false, Alarm: null));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1235,14 +1235,14 @@ public sealed class Phase7ApplierTests
public void Changed_tag_name_change_rebuilds() public void Changed_tag_name_change_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags( var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "SpeedRpm", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "SpeedRpm", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1259,7 +1259,7 @@ public sealed class Phase7ApplierTests
public void Changed_tag_alarm_presence_change_rebuilds() public void Changed_tag_alarm_presence_change_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false, Alarm: null));
@@ -1267,7 +1267,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false, new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false,
Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700))); Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700)));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1288,7 +1288,7 @@ public sealed class Phase7ApplierTests
public void Changed_alarm_bearing_tag_writable_only_still_rebuilds() public void Changed_alarm_bearing_tag_writable_only_still_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
// Both previous and current carry an identical, non-null alarm — the tag is an alarm-bearing node. // Both previous and current carry an identical, non-null alarm — the tag is an alarm-bearing node.
var alarm = new EquipmentTagAlarmInfo("OffNormalAlarm", 700); var alarm = new EquipmentTagAlarmInfo("OffNormalAlarm", 700);
@@ -1300,7 +1300,7 @@ public sealed class Phase7ApplierTests
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean",
FullName: "00001", Writable: true, Alarm: alarm)); FullName: "00001", Writable: true, Alarm: alarm));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: the planner sees a changed tag (Writable differs) and nothing else. // Guard the arrange: the planner sees a changed tag (Writable differs) and nothing else.
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
@@ -1323,10 +1323,10 @@ public sealed class Phase7ApplierTests
public void Two_surgical_eligible_tag_deltas_both_apply_in_place_no_rebuild() public void Two_surgical_eligible_tag_deltas_both_apply_in_place_no_rebuild()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
// Two distinct plain (no-alarm) tags, both will flip Writable — both are surgical-eligible. // Two distinct plain (no-alarm) tags, both will flip Writable — both are surgical-eligible.
var previous = new Phase7CompositionResult( var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -1336,7 +1336,7 @@ public sealed class Phase7ApplierTests
}, },
}; };
// Only Writable flips on both tags; everything else is identical. // Only Writable flips on both tags; everything else is identical.
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -1346,7 +1346,7 @@ public sealed class Phase7ApplierTests
}, },
}; };
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
// Guard the arrange: exactly two changed tags, nothing else. // Guard the arrange: exactly two changed tags, nothing else.
plan.ChangedEquipmentTags.Count.ShouldBe(2); plan.ChangedEquipmentTags.Count.ShouldBe(2);
@@ -1380,9 +1380,9 @@ public sealed class Phase7ApplierTests
public void Surgical_eligible_tag_delta_mixed_with_added_equipment_rebuilds() public void Surgical_eligible_tag_delta_mixed_with_added_equipment_rebuilds()
{ {
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = new Phase7CompositionResult( var previous = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -1391,7 +1391,7 @@ public sealed class Phase7ApplierTests
}, },
}; };
// Surgical-eligible Writable flip on the tag AND a brand-new equipment node. // Surgical-eligible Writable flip on the tag AND a brand-new equipment node.
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-new", "New", "line-1") }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) new[] { new EquipmentNode("eq-new", "New", "line-1") }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -1400,7 +1400,7 @@ public sealed class Phase7ApplierTests
}, },
}; };
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
plan.AddedEquipment.Count.ShouldBe(1); plan.AddedEquipment.Count.ShouldBe(1);
@@ -1418,14 +1418,14 @@ public sealed class Phase7ApplierTests
public void Surgical_eligible_delta_on_non_surgical_sink_rebuilds() public void Surgical_eligible_delta_on_non_surgical_sink_rebuilds()
{ {
var sink = new PlainRecordingSink(); var sink = new PlainRecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags( var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1441,14 +1441,14 @@ public sealed class Phase7ApplierTests
public void Surgical_sink_returning_false_node_missing_falls_back_to_rebuild() public void Surgical_sink_returning_false_node_missing_falls_back_to_rebuild()
{ {
var sink = new RecordingSink { SurgicalReturns = false }; var sink = new RecordingSink { SurgicalReturns = false };
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
var previous = CompositionWithTags( var previous = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
var next = CompositionWithTags( var next = CompositionWithTags(
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null)); new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null));
var plan = Phase7Planner.Compute(previous, next); var plan = AddressSpacePlanner.Compute(previous, next);
plan.ChangedEquipmentTags.Count.ShouldBe(1); plan.ChangedEquipmentTags.Count.ShouldBe(1);
var outcome = applier.Apply(plan); var outcome = applier.Apply(plan);
@@ -1458,44 +1458,44 @@ public sealed class Phase7ApplierTests
sink.SurgicalCalls.ShouldHaveSingleItem(); // the surgical update was attempted first sink.SurgicalCalls.ShouldHaveSingleItem(); // the surgical update was attempted first
} }
private static Phase7CompositionResult CompositionWithTags(params EquipmentTagPlan[] tags) => private static AddressSpaceComposition CompositionWithTags(params EquipmentTagPlan[] tags) =>
new( new(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = tags, EquipmentTags = tags,
}; };
private static Phase7CompositionResult CompositionWithVirtualTags(params EquipmentVirtualTagPlan[] vtags) => private static AddressSpaceComposition CompositionWithVirtualTags(params EquipmentVirtualTagPlan[] vtags) =>
new( new(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = vtags, EquipmentVirtualTags = vtags,
}; };
private static Phase7CompositionResult CompositionWithAlarms(params ScriptedAlarmPlan[] alarms) => private static AddressSpaceComposition CompositionWithAlarms(params ScriptedAlarmPlan[] alarms) =>
// ScriptedAlarmPlans is the set the planner diffs into Added/Removed/ChangedAlarms. // ScriptedAlarmPlans is the set the planner diffs into Added/Removed/ChangedAlarms.
new( new(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), alarms); Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), alarms);
private static Phase7CompositionResult CompositionWithDrivers(params DriverInstancePlan[] drivers) => private static AddressSpaceComposition CompositionWithDrivers(params DriverInstancePlan[] drivers) =>
new( new(
Array.Empty<EquipmentNode>(), drivers, Array.Empty<ScriptedAlarmPlan>()); Array.Empty<EquipmentNode>(), drivers, Array.Empty<ScriptedAlarmPlan>());
private static Phase7Plan EmptyPlan => new( private static AddressSpacePlan EmptyPlan => new(
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(), Array.Empty<Phase7Plan.EquipmentDelta>(), Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(), Array.Empty<AddressSpacePlan.EquipmentDelta>(),
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(), Array.Empty<Phase7Plan.DriverDelta>(), Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(), Array.Empty<AddressSpacePlan.DriverDelta>(),
Array.Empty<ScriptedAlarmPlan>(), Array.Empty<ScriptedAlarmPlan>(), Array.Empty<Phase7Plan.AlarmDelta>()); Array.Empty<ScriptedAlarmPlan>(), Array.Empty<ScriptedAlarmPlan>(), Array.Empty<AddressSpacePlan.AlarmDelta>());
private static Phase7Plan WithEquipmentRemoval(params string[] ids) => new( private static AddressSpacePlan WithEquipmentRemoval(params string[] ids) => new(
AddedEquipment: Array.Empty<EquipmentNode>(), AddedEquipment: Array.Empty<EquipmentNode>(),
RemovedEquipment: ids.Select(id => new EquipmentNode(id, id, "line-1")).ToArray(), RemovedEquipment: ids.Select(id => new EquipmentNode(id, id, "line-1")).ToArray(),
ChangedEquipment: Array.Empty<Phase7Plan.EquipmentDelta>(), ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
AddedDrivers: Array.Empty<DriverInstancePlan>(), AddedDrivers: Array.Empty<DriverInstancePlan>(),
RemovedDrivers: Array.Empty<DriverInstancePlan>(), RemovedDrivers: Array.Empty<DriverInstancePlan>(),
ChangedDrivers: Array.Empty<Phase7Plan.DriverDelta>(), ChangedDrivers: Array.Empty<AddressSpacePlan.DriverDelta>(),
AddedAlarms: Array.Empty<ScriptedAlarmPlan>(), AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(), RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
ChangedAlarms: Array.Empty<Phase7Plan.AlarmDelta>()); ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());
private sealed class RecordingSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink private sealed class RecordingSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
{ {
@@ -6,13 +6,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary> /// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) treats a Galaxy /// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) treats a Galaxy
/// point as an ordinary equipment tag: <c>GalaxyMxGateway</c> is a standard Equipment-kind driver. /// point as an ordinary equipment tag: <c>GalaxyMxGateway</c> is a standard Equipment-kind driver.
/// An equipment-scoped <see cref="Tag"/> (non-null <see cref="Tag.EquipmentId"/>) bound to a /// An equipment-scoped <see cref="Tag"/> (non-null <see cref="Tag.EquipmentId"/>) bound to a
/// <c>GalaxyMxGateway</c> driver living in an <c>Equipment</c>-kind namespace must surface under /// <c>GalaxyMxGateway</c> driver living in an <c>Equipment</c>-kind namespace must surface under
/// <see cref="Phase7CompositionResult.EquipmentTags"/> (carrying its driver-side FullName). /// <see cref="AddressSpaceComposition.EquipmentTags"/> (carrying its driver-side FullName).
/// </summary> /// </summary>
public sealed class Phase7ComposerAliasTagTests public sealed class AddressSpaceComposerAliasTagTests
{ {
/// <summary>A <c>GalaxyMxGateway</c> driver in an Equipment-kind namespace carries an /// <summary>A <c>GalaxyMxGateway</c> driver in an Equipment-kind namespace carries an
/// equipment-scoped Galaxy tag (EquipmentId set, FolderPath null, TagConfig FullName = the Galaxy /// equipment-scoped Galaxy tag (EquipmentId set, FolderPath null, TagConfig FullName = the Galaxy
@@ -61,7 +61,7 @@ public sealed class Phase7ComposerAliasTagTests
TagConfig = "{\"FullName\":\"TestMachine_020.TestChangingInt\"}", TagConfig = "{\"FullName\":\"TestMachine_020.TestChangingInt\"}",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip }, new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(), new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { galaxyTag }, new[] { ns }); new[] { galaxyTag }, new[] { ns });
@@ -6,12 +6,12 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary> /// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) substitutes the /// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) substitutes the
/// reserved <c>{{equip}}</c> token in a shared VirtualTag script with each owning equipment's /// reserved <c>{{equip}}</c> token in a shared VirtualTag script with each owning equipment's
/// derived tag base (from its child-tag <c>FullName</c>s) — so one script reused across N /// derived tag base (from its child-tag <c>FullName</c>s) — so one script reused across N
/// identical machines resolves to N machine-specific dependency graphs. /// identical machines resolves to N machine-specific dependency graphs.
/// </summary> /// </summary>
public sealed class Phase7ComposerEquipTokenTests public sealed class AddressSpaceComposerEquipTokenTests
{ {
/// <summary>One shared <see cref="Script"/> using <c>ctx.GetTag("{{equip}}.Source")</c>, bound /// <summary>One shared <see cref="Script"/> using <c>ctx.GetTag("{{equip}}.Source")</c>, bound
/// to two equipments (TestMachine_001 / _002) each with one equipment Tag whose FullName carries /// to two equipments (TestMachine_001 / _002) each with one equipment Tag whose FullName carries
@@ -79,7 +79,7 @@ public sealed class Phase7ComposerEquipTokenTests
var vt1 = new VirtualTag { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "over50", DataType = "Boolean", ScriptId = "s-equip" }; var vt1 = new VirtualTag { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "over50", DataType = "Boolean", ScriptId = "s-equip" };
var vt2 = new VirtualTag { VirtualTagId = "vt-2", EquipmentId = "eq-2", Name = "over50", DataType = "Boolean", ScriptId = "s-equip" }; var vt2 = new VirtualTag { VirtualTagId = "vt-2", EquipmentId = "eq-2", Name = "over50", DataType = "Boolean", ScriptId = "s-equip" };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip1, equip2 }, new[] { area }, new[] { line }, new[] { equip1, equip2 },
new[] { driver1, driver2 }, Array.Empty<ScriptedAlarm>(), new[] { driver1, driver2 }, Array.Empty<ScriptedAlarm>(),
new[] { tag1, tag2 }, new[] { ns }, new[] { tag1, tag2 }, new[] { ns },
@@ -4,13 +4,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7ComposerPurityTests public sealed class AddressSpaceComposerPurityTests
{ {
/// <summary>Verifies empty inputs produce empty result.</summary> /// <summary>Verifies empty inputs produce empty result.</summary>
[Fact] [Fact]
public void Empty_inputs_produce_empty_result() public void Empty_inputs_produce_empty_result()
{ {
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
equipment: Array.Empty<Equipment>(), equipment: Array.Empty<Equipment>(),
driverInstances: Array.Empty<DriverInstance>(), driverInstances: Array.Empty<DriverInstance>(),
scriptedAlarms: Array.Empty<ScriptedAlarm>()); scriptedAlarms: Array.Empty<ScriptedAlarm>());
@@ -31,12 +31,12 @@ public sealed class Phase7ComposerPurityTests
var a1 = NewAlarm("a-1", "eq-1"); var a1 = NewAlarm("a-1", "eq-1");
var a2 = NewAlarm("a-2", "eq-2"); var a2 = NewAlarm("a-2", "eq-2");
var r1 = Phase7Composer.Compose( var r1 = AddressSpaceComposer.Compose(
equipment: new[] { e1, e2 }, equipment: new[] { e1, e2 },
driverInstances: new[] { d1, d2 }, driverInstances: new[] { d1, d2 },
scriptedAlarms: new[] { a1, a2 }); scriptedAlarms: new[] { a1, a2 });
var r2 = Phase7Composer.Compose( var r2 = AddressSpaceComposer.Compose(
equipment: new[] { e2, e1 }, equipment: new[] { e2, e1 },
driverInstances: new[] { d2, d1 }, driverInstances: new[] { d2, d1 },
scriptedAlarms: new[] { a2, a1 }); scriptedAlarms: new[] { a2, a1 });
@@ -54,8 +54,8 @@ public sealed class Phase7ComposerPurityTests
var drivers = new[] { NewDriver("drv-x") }; var drivers = new[] { NewDriver("drv-x") };
var alarms = new[] { NewAlarm("alarm-1", "eq-a") }; var alarms = new[] { NewAlarm("alarm-1", "eq-a") };
var r1 = Phase7Composer.Compose(equipment, drivers, alarms); var r1 = AddressSpaceComposer.Compose(equipment, drivers, alarms);
var r2 = Phase7Composer.Compose(equipment, drivers, alarms); var r2 = AddressSpaceComposer.Compose(equipment, drivers, alarms);
// Record equality won't help here — IReadOnlyList<T> uses reference equality. Compare // Record equality won't help here — IReadOnlyList<T> uses reference equality. Compare
// element-wise to verify the pure-function contract. // element-wise to verify the pure-function contract.
@@ -69,7 +69,7 @@ public sealed class Phase7ComposerPurityTests
public void Output_is_sorted_by_natural_key() public void Output_is_sorted_by_natural_key()
{ {
var equipment = new[] { NewEquipment("z"), NewEquipment("a"), NewEquipment("m") }; var equipment = new[] { NewEquipment("z"), NewEquipment("a"), NewEquipment("m") };
var result = Phase7Composer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>()); var result = AddressSpaceComposer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>());
result.EquipmentNodes.Select(e => e.EquipmentId) result.EquipmentNodes.Select(e => e.EquipmentId)
.ShouldBe(new[] { "a", "m", "z" }); .ShouldBe(new[] { "a", "m", "z" });
@@ -83,7 +83,7 @@ public sealed class Phase7ComposerPurityTests
{ {
var equipment = new[] { NewEquipment("filling-eq") }; // Name="filling-eq", MachineCode="FILLING-EQ" var equipment = new[] { NewEquipment("filling-eq") }; // Name="filling-eq", MachineCode="FILLING-EQ"
var node = Phase7Composer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>()) var node = AddressSpaceComposer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>())
.EquipmentNodes.ShouldHaveSingleItem(); .EquipmentNodes.ShouldHaveSingleItem();
node.EquipmentId.ShouldBe("filling-eq"); // NodeId stays the logical Id node.EquipmentId.ShouldBe("filling-eq"); // NodeId stays the logical Id
@@ -94,7 +94,7 @@ public sealed class Phase7ComposerPurityTests
[Fact] [Fact]
public void Composition_carries_empty_equipment_virtualtags_by_default() public void Composition_carries_empty_equipment_virtualtags_by_default()
{ {
var r = new Phase7CompositionResult( var r = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
r.EquipmentVirtualTags.ShouldBeEmpty(); r.EquipmentVirtualTags.ShouldBeEmpty();
} }
@@ -133,7 +133,7 @@ public sealed class Phase7ComposerPurityTests
SourceHash = "hash-1", SourceHash = "hash-1",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(), Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -171,7 +171,7 @@ public sealed class Phase7ComposerPurityTests
SourceHash = "hash-1", SourceHash = "hash-1",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(), Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -197,7 +197,7 @@ public sealed class Phase7ComposerPurityTests
ScriptId = "s-does-not-exist", ScriptId = "s-does-not-exist",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(), Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary> /// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) builds the richer /// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) builds the richer
/// per-equipment scripted-alarm projection (<see cref="EquipmentScriptedAlarmPlan"/>): each alarm /// per-equipment scripted-alarm projection (<see cref="EquipmentScriptedAlarmPlan"/>): each alarm
/// is joined to its predicate <see cref="Script"/> for the source, carries the parsed /// is joined to its predicate <see cref="Script"/> for the source, carries the parsed
/// <c>ctx.GetTag("…")</c> read refs UNION the <c>{TagPath}</c> tokens referenced in its /// <c>ctx.GetTag("…")</c> read refs UNION the <c>{TagPath}</c> tokens referenced in its
@@ -13,7 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// (including disabled alarms). The projection is deterministic so the upcoming /// (including disabled alarms). The projection is deterministic so the upcoming
/// artifact-byte-parity test (sibling task) is reliable. /// artifact-byte-parity test (sibling task) is reliable.
/// </summary> /// </summary>
public sealed class Phase7ComposerScriptedAlarmTests public sealed class AddressSpaceComposerScriptedAlarmTests
{ {
/// <summary>Two equipments, each with a scripted alarm whose predicate script reads one tag via /// <summary>Two equipments, each with a scripted alarm whose predicate script reads one tag via
/// <c>ctx.GetTag("X.Y")</c>. Each plan must carry the resolved PredicateSource, DependencyRefs /// <c>ctx.GetTag("X.Y")</c>. Each plan must carry the resolved PredicateSource, DependencyRefs
@@ -62,7 +62,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
Enabled = true, Enabled = true,
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm1, alarm2 }, Array.Empty<DriverInstance>(), new[] { alarm1, alarm2 },
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -118,7 +118,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
PredicateScriptId = "s-1", PredicateScriptId = "s-1",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm }, Array.Empty<DriverInstance>(), new[] { alarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -155,7 +155,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
PredicateScriptId = "s-1", PredicateScriptId = "s-1",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm }, Array.Empty<DriverInstance>(), new[] { alarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -199,7 +199,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
PredicateScriptId = "s-does-not-exist", PredicateScriptId = "s-does-not-exist",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { goodAlarm, orphanAlarm }, Array.Empty<DriverInstance>(), new[] { goodAlarm, orphanAlarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -234,7 +234,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
Enabled = false, Enabled = false,
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm }, Array.Empty<DriverInstance>(), new[] { alarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -261,12 +261,12 @@ public sealed class Phase7ComposerScriptedAlarmTests
var a3 = NewAlarm("al-3", "eq-1", "s-3"); var a3 = NewAlarm("al-3", "eq-1", "s-3");
var scripts = new[] { script1, script2, script3 }; var scripts = new[] { script1, script2, script3 };
var r1 = Phase7Composer.Compose( var r1 = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { a1, a2, a3 }, Array.Empty<DriverInstance>(), new[] { a1, a2, a3 },
Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts); Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts);
var r2 = Phase7Composer.Compose( var r2 = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { a3, a1, a2 }, Array.Empty<DriverInstance>(), new[] { a3, a1, a2 },
Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts); Array.Empty<Tag>(), Array.Empty<Namespace>(), scripts: scripts);
@@ -282,7 +282,7 @@ public sealed class Phase7ComposerScriptedAlarmTests
[Fact] [Fact]
public void Composition_carries_empty_scripted_alarms_by_default() public void Composition_carries_empty_scripted_alarms_by_default()
{ {
var r = new Phase7CompositionResult( var r = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
r.EquipmentScriptedAlarms.ShouldBeEmpty(); r.EquipmentScriptedAlarms.ShouldBeEmpty();
} }
@@ -6,13 +6,13 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary> /// <summary>
/// Verifies the live-edit compose seam (<see cref="Phase7Composer.Compose"/>) carries the /// Verifies the live-edit compose seam (<see cref="AddressSpaceComposer.Compose"/>) carries the
/// <see cref="VirtualTag.Historize"/> entity column onto the resulting /// <see cref="VirtualTag.Historize"/> entity column onto the resulting
/// <see cref="EquipmentVirtualTagPlan.Historize"/> (H5a). The flag was authored in the UI but /// <see cref="EquipmentVirtualTagPlan.Historize"/> (H5a). The flag was authored in the UI but
/// never threaded onto the plan, leaving equipment-namespace runtime historization dead. Both a /// never threaded onto the plan, leaving equipment-namespace runtime historization dead. Both a
/// true and a false case are asserted so the default (false) and the explicit set both prove out. /// true and a false case are asserted so the default (false) and the explicit set both prove out.
/// </summary> /// </summary>
public sealed class Phase7ComposerVirtualTagHistorizeTests public sealed class AddressSpaceComposerVirtualTagHistorizeTests
{ {
[Fact] [Fact]
public void Compose_carries_virtual_tag_historize_flag_onto_plan() public void Compose_carries_virtual_tag_historize_flag_onto_plan()
@@ -57,7 +57,7 @@ public sealed class Phase7ComposerVirtualTagHistorizeTests
var vtHist = new VirtualTag { VirtualTagId = "vt-hist", EquipmentId = "eq-1", Name = "Historized", DataType = "Int32", ScriptId = "s-1", Historize = true }; var vtHist = new VirtualTag { VirtualTagId = "vt-hist", EquipmentId = "eq-1", Name = "Historized", DataType = "Int32", ScriptId = "s-1", Historize = true };
var vtPlain = new VirtualTag { VirtualTagId = "vt-plain", EquipmentId = "eq-1", Name = "Plain", DataType = "Int32", ScriptId = "s-1", Historize = false }; var vtPlain = new VirtualTag { VirtualTagId = "vt-plain", EquipmentId = "eq-1", Name = "Plain", DataType = "Int32", ScriptId = "s-1", Historize = false };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip }, new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(), new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { tag }, new[] { ns }, new[] { tag }, new[] { ns },
@@ -3,16 +3,16 @@ using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
public sealed class Phase7PlannerTests public sealed class AddressSpacePlannerTests
{ {
/// <summary>Verifies that empty inputs produce an empty plan.</summary> /// <summary>Verifies that empty inputs produce an empty plan.</summary>
[Fact] [Fact]
public void Empty_inputs_produce_empty_plan() public void Empty_inputs_produce_empty_plan()
{ {
var prev = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); var prev = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = prev; var next = prev;
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue(); plan.IsEmpty.ShouldBeTrue();
} }
@@ -22,10 +22,10 @@ public sealed class Phase7PlannerTests
public void Identical_compositions_produce_empty_plan() public void Identical_compositions_produce_empty_plan()
{ {
var eq = new EquipmentNode("eq-1", "Eq 1", "line-1"); var eq = new EquipmentNode("eq-1", "Eq 1", "line-1");
var prev = new Phase7CompositionResult(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); var prev = new AddressSpaceComposition(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); var next = new AddressSpaceComposition(new[] { eq }, Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue(); plan.IsEmpty.ShouldBeTrue();
} }
@@ -36,9 +36,9 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Equipment_tag_only_change_yields_non_empty_plan_with_added_tag() public void Equipment_tag_only_change_yields_non_empty_plan_with_added_tag()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -47,7 +47,7 @@ public sealed class Phase7PlannerTests
}, },
}; };
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse(); plan.IsEmpty.ShouldBeFalse();
plan.AddedEquipmentTags.Single().TagId.ShouldBe("tag-1"); plan.AddedEquipmentTags.Single().TagId.ShouldBe("tag-1");
@@ -61,9 +61,9 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Equipment_virtual_tag_only_change_yields_non_empty_plan_with_added_tag() public void Equipment_virtual_tag_only_change_yields_non_empty_plan_with_added_tag()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -73,7 +73,7 @@ public sealed class Phase7PlannerTests
}, },
}; };
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse(); plan.IsEmpty.ShouldBeFalse();
plan.AddedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1"); plan.AddedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1");
@@ -85,7 +85,7 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Disappeared_virtual_tag_goes_to_RemovedEquipmentVirtualTags() public void Disappeared_virtual_tag_goes_to_RemovedEquipmentVirtualTags()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -94,10 +94,10 @@ public sealed class Phase7PlannerTests
Expression: "a + b", DependencyRefs: new[] { "a", "b" }), Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
}, },
}; };
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse(); plan.IsEmpty.ShouldBeFalse();
plan.RemovedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1"); plan.RemovedEquipmentVirtualTags.Single().VirtualTagId.ShouldBe("vt-1");
@@ -111,7 +111,7 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Same_id_with_different_expression_routes_to_ChangedEquipmentVirtualTags() public void Same_id_with_different_expression_routes_to_ChangedEquipmentVirtualTags()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -120,7 +120,7 @@ public sealed class Phase7PlannerTests
Expression: "a + b", DependencyRefs: new[] { "a", "b" }), Expression: "a + b", DependencyRefs: new[] { "a", "b" }),
}, },
}; };
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -130,7 +130,7 @@ public sealed class Phase7PlannerTests
}, },
}; };
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse(); plan.IsEmpty.ShouldBeFalse();
plan.ChangedEquipmentVirtualTags.Single().Previous.Expression.ShouldBe("a + b"); plan.ChangedEquipmentVirtualTags.Single().Previous.Expression.ShouldBe("a + b");
@@ -146,7 +146,7 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Same_id_with_toggled_historize_routes_to_ChangedEquipmentVirtualTags() public void Same_id_with_toggled_historize_routes_to_ChangedEquipmentVirtualTags()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -155,7 +155,7 @@ public sealed class Phase7PlannerTests
Expression: "a + b", DependencyRefs: new[] { "a", "b" }, Historize: false), Expression: "a + b", DependencyRefs: new[] { "a", "b" }, Historize: false),
}, },
}; };
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -165,7 +165,7 @@ public sealed class Phase7PlannerTests
}, },
}; };
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse(); plan.IsEmpty.ShouldBeFalse();
plan.ChangedEquipmentVirtualTags.Single().Previous.Historize.ShouldBeFalse(); plan.ChangedEquipmentVirtualTags.Single().Previous.Historize.ShouldBeFalse();
@@ -181,7 +181,7 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Same_id_with_toggled_isarray_routes_to_ChangedEquipmentTags() public void Same_id_with_toggled_isarray_routes_to_ChangedEquipmentTags()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -190,7 +190,7 @@ public sealed class Phase7PlannerTests
FullName: "40001", Writable: false, Alarm: null, IsArray: false, ArrayLength: null), FullName: "40001", Writable: false, Alarm: null, IsArray: false, ArrayLength: null),
}, },
}; };
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentTags = new[] EquipmentTags = new[]
@@ -200,7 +200,7 @@ public sealed class Phase7PlannerTests
}, },
}; };
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse(); plan.IsEmpty.ShouldBeFalse();
plan.ChangedEquipmentTags.Single().Previous.IsArray.ShouldBeFalse(); plan.ChangedEquipmentTags.Single().Previous.IsArray.ShouldBeFalse();
@@ -221,7 +221,7 @@ public sealed class Phase7PlannerTests
var refsA = new[] { "EQ1.Speed", "EQ1.Torque" }; var refsA = new[] { "EQ1.Speed", "EQ1.Torque" };
var refsB = new[] { "EQ1.Speed", "EQ1.Torque" }; var refsB = new[] { "EQ1.Speed", "EQ1.Torque" };
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -230,7 +230,7 @@ public sealed class Phase7PlannerTests
Expression: "ctx.GetTag(\"EQ1.Speed\") / ctx.GetTag(\"EQ1.Torque\")", DependencyRefs: refsA), Expression: "ctx.GetTag(\"EQ1.Speed\") / ctx.GetTag(\"EQ1.Torque\")", DependencyRefs: refsA),
}, },
}; };
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()) Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{ {
EquipmentVirtualTags = new[] EquipmentVirtualTags = new[]
@@ -240,7 +240,7 @@ public sealed class Phase7PlannerTests
}, },
}; };
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.IsEmpty.ShouldBeTrue(); plan.IsEmpty.ShouldBeTrue();
plan.ChangedEquipmentVirtualTags.ShouldBeEmpty(); plan.ChangedEquipmentVirtualTags.ShouldBeEmpty();
@@ -252,13 +252,13 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void New_equipment_goes_to_AddedEquipment() public void New_equipment_goes_to_AddedEquipment()
{ {
var prev = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); var prev = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "A", "line-1") }, new[] { new EquipmentNode("eq-1", "A", "line-1") },
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>()); Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-1"); plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-1");
plan.RemovedEquipment.ShouldBeEmpty(); plan.RemovedEquipment.ShouldBeEmpty();
@@ -269,13 +269,13 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Disappeared_equipment_goes_to_RemovedEquipment() public void Disappeared_equipment_goes_to_RemovedEquipment()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "A", "line-1") }, new[] { new EquipmentNode("eq-1", "A", "line-1") },
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>()); Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); var next = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-1"); plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-1");
plan.AddedEquipment.ShouldBeEmpty(); plan.AddedEquipment.ShouldBeEmpty();
@@ -285,16 +285,16 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Same_id_with_different_display_name_routes_to_ChangedEquipment() public void Same_id_with_different_display_name_routes_to_ChangedEquipment()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "Old", "line-1") }, new[] { new EquipmentNode("eq-1", "Old", "line-1") },
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>()); Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-1", "New", "line-1") }, new[] { new EquipmentNode("eq-1", "New", "line-1") },
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>()); Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.ChangedEquipment.Single().Previous.DisplayName.ShouldBe("Old"); plan.ChangedEquipment.Single().Previous.DisplayName.ShouldBe("Old");
plan.ChangedEquipment.Single().Current.DisplayName.ShouldBe("New"); plan.ChangedEquipment.Single().Current.DisplayName.ShouldBe("New");
@@ -306,16 +306,16 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Driver_config_change_routes_to_ChangedDrivers() public void Driver_config_change_routes_to_ChangedDrivers()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(),
new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"old\"}") }, new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"old\"}") },
Array.Empty<ScriptedAlarmPlan>()); Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(),
new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"new\"}") }, new[] { new DriverInstancePlan("drv-1", "Modbus", "{\"host\":\"new\"}") },
Array.Empty<ScriptedAlarmPlan>()); Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.ChangedDrivers.Single().Current.ConfigJson.ShouldContain("new"); plan.ChangedDrivers.Single().Current.ConfigJson.ShouldContain("new");
} }
@@ -324,16 +324,16 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Alarm_message_template_change_routes_to_ChangedAlarms() public void Alarm_message_template_change_routes_to_ChangedAlarms()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(),
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(),
new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "old") }); new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "old") });
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
Array.Empty<EquipmentNode>(), Array.Empty<EquipmentNode>(),
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(),
new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "new") }); new[] { new ScriptedAlarmPlan("a-1", "eq-1", "script-1", "new") });
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.ChangedAlarms.Single().Current.MessageTemplate.ShouldBe("new"); plan.ChangedAlarms.Single().Current.MessageTemplate.ShouldBe("new");
} }
@@ -342,13 +342,13 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Added_and_removed_lists_are_sorted_by_id_for_deterministic_ordering() public void Added_and_removed_lists_are_sorted_by_id_for_deterministic_ordering()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("z", "Z", "line-1"), new EquipmentNode("a", "A", "line-1") }, new[] { new EquipmentNode("z", "Z", "line-1"), new EquipmentNode("a", "A", "line-1") },
Array.Empty<DriverInstancePlan>(), Array.Empty<DriverInstancePlan>(),
Array.Empty<ScriptedAlarmPlan>()); Array.Empty<ScriptedAlarmPlan>());
var next = new Phase7CompositionResult(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>()); var next = new AddressSpaceComposition(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>());
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.RemovedEquipment.Select(e => e.EquipmentId).ShouldBe(new[] { "a", "z" }); plan.RemovedEquipment.Select(e => e.EquipmentId).ShouldBe(new[] { "a", "z" });
} }
@@ -357,16 +357,16 @@ public sealed class Phase7PlannerTests
[Fact] [Fact]
public void Mixed_changes_across_all_three_classes_are_captured_in_one_pass() public void Mixed_changes_across_all_three_classes_are_captured_in_one_pass()
{ {
var prev = new Phase7CompositionResult( var prev = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-drop", "Drop", "line-1") }, new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-drop", "Drop", "line-1") },
new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":1}") }, new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":1}") },
new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1") }); new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1") });
var next = new Phase7CompositionResult( var next = new AddressSpaceComposition(
new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-new", "New", "line-1") }, new[] { new EquipmentNode("eq-keep", "Keep", "line-1"), new EquipmentNode("eq-new", "New", "line-1") },
new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":2}") }, new[] { new DriverInstancePlan("drv-keep", "Modbus", "{}"), new DriverInstancePlan("drv-change", "Modbus", "{\"v\":2}") },
new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1"), new ScriptedAlarmPlan("a-new", "eq-new", "s2", "t2") }); new[] { new ScriptedAlarmPlan("a-keep", "eq-keep", "s1", "t1"), new ScriptedAlarmPlan("a-new", "eq-new", "s2", "t2") });
var plan = Phase7Planner.Compute(prev, next); var plan = AddressSpacePlanner.Compute(prev, next);
plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-new"); plan.AddedEquipment.Single().EquipmentId.ShouldBe("eq-new");
plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-drop"); plan.RemovedEquipment.Single().EquipmentId.ShouldBe("eq-drop");
@@ -84,7 +84,7 @@ public sealed class DeferredAddressSpaceSinkTests
} }
/// <summary>F10b regression: the deferred wrapper MUST forward the surgical capability to a /// <summary>F10b regression: the deferred wrapper MUST forward the surgical capability to a
/// surgical inner sink — otherwise <c>Phase7Applier</c> (which injects THIS wrapper on every /// surgical inner sink — otherwise <c>AddressSpaceApplier</c> (which injects THIS wrapper on every
/// driver-role host, not the inner <c>SdkAddressSpaceSink</c>) never sees the capability and the /// driver-role host, not the inner <c>SdkAddressSpaceSink</c>) never sees the capability and the
/// in-place tag-attribute optimization is inert in production (it silently falls back to rebuild).</summary> /// in-place tag-attribute optimization is inert in production (it silently falls back to rebuild).</summary>
[Fact] [Fact]
@@ -14,7 +14,7 @@ public class ExtractTagAlarmTests
[InlineData("{\"FullName\":\"X.Y\",\"alarm\":\"oops\"}", false, null, 0)] [InlineData("{\"FullName\":\"X.Y\",\"alarm\":\"oops\"}", false, null, 0)]
public void ExtractTagAlarm_parses_or_returns_null(string cfg, bool present, string? type, int sev) public void ExtractTagAlarm_parses_or_returns_null(string cfg, bool present, string? type, int sev)
{ {
var info = Phase7Composer.ExtractTagAlarm(cfg); var info = AddressSpaceComposer.ExtractTagAlarm(cfg);
if (!present) { info.ShouldBeNull(); return; } if (!present) { info.ShouldBeNull(); return; }
info!.AlarmType.ShouldBe(type); info!.AlarmType.ShouldBe(type);
info.Severity.ShouldBe(sev); info.Severity.ShouldBe(sev);
@@ -30,7 +30,7 @@ public class ExtractTagAlarmTests
[InlineData("{\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":500,\"historizeToAveva\":\"oops\"}}", null)] [InlineData("{\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":500,\"historizeToAveva\":\"oops\"}}", null)]
public void ExtractTagAlarm_parses_historizeToAveva(string cfg, bool? expected) public void ExtractTagAlarm_parses_historizeToAveva(string cfg, bool? expected)
{ {
var info = Phase7Composer.ExtractTagAlarm(cfg); var info = AddressSpaceComposer.ExtractTagAlarm(cfg);
info.ShouldNotBeNull(); info.ShouldNotBeNull();
info!.HistorizeToAveva.ShouldBe(expected); info!.HistorizeToAveva.ShouldBe(expected);
} }
@@ -7,12 +7,12 @@ using ZB.MOM.WW.OtOpcUa.OpcUaServer;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary> /// <summary>
/// Verifies <see cref="Phase7Composer.ExtractTagArray"/> parses the optional array intent from a /// Verifies <see cref="AddressSpaceComposer.ExtractTagArray"/> parses the optional array intent from a
/// tag's <c>TagConfig</c> JSON exactly as <see cref="Phase7Composer.ExtractTagHistorize"/> parses /// tag's <c>TagConfig</c> JSON exactly as <see cref="AddressSpaceComposer.ExtractTagHistorize"/> parses
/// the historize intent: the <c>isArray</c> bool (absent / not a bool / non-object root / blank / /// the historize intent: the <c>isArray</c> bool (absent / not a bool / non-object root / blank /
/// malformed ⇒ <c>false</c>) and the optional <c>arrayLength</c> uint (only honoured when /// malformed ⇒ <c>false</c>) and the optional <c>arrayLength</c> uint (only honoured when
/// <c>isArray</c> is true AND the prop is a JSON number that fits uint; else <c>null</c>). Never /// <c>isArray</c> is true AND the prop is a JSON number that fits uint; else <c>null</c>). Never
/// throws. Also pins the end-to-end <see cref="Phase7Composer.Compose"/> thread-through onto /// throws. Also pins the end-to-end <see cref="AddressSpaceComposer.Compose"/> thread-through onto
/// <see cref="EquipmentTagPlan.IsArray"/> / <see cref="EquipmentTagPlan.ArrayLength"/>. /// <see cref="EquipmentTagPlan.IsArray"/> / <see cref="EquipmentTagPlan.ArrayLength"/>.
/// </summary> /// </summary>
public class ExtractTagArrayTests public class ExtractTagArrayTests
@@ -45,13 +45,13 @@ public class ExtractTagArrayTests
[InlineData("{\"isArray\":true,\"arrayLength\":4294967296}", true, null)] [InlineData("{\"isArray\":true,\"arrayLength\":4294967296}", true, null)]
public void ExtractTagArray_parses_or_returns_defaults(string? cfg, bool expectedIsArray, uint? expectedLength) public void ExtractTagArray_parses_or_returns_defaults(string? cfg, bool expectedIsArray, uint? expectedLength)
{ {
var (isArray, arrayLength) = Phase7Composer.ExtractTagArray(cfg); var (isArray, arrayLength) = AddressSpaceComposer.ExtractTagArray(cfg);
isArray.ShouldBe(expectedIsArray); isArray.ShouldBe(expectedIsArray);
arrayLength.ShouldBe(expectedLength); arrayLength.ShouldBe(expectedLength);
} }
/// <summary>End-to-end: an equipment tag whose TagConfig carries <c>isArray</c>/<c>arrayLength</c> /// <summary>End-to-end: an equipment tag whose TagConfig carries <c>isArray</c>/<c>arrayLength</c>
/// surfaces those on its <see cref="EquipmentTagPlan"/> through <see cref="Phase7Composer.Compose"/>, /// surfaces those on its <see cref="EquipmentTagPlan"/> through <see cref="AddressSpaceComposer.Compose"/>,
/// exactly as the historize keys thread through.</summary> /// exactly as the historize keys thread through.</summary>
[Fact] [Fact]
public void Compose_threads_array_keys_onto_equipment_tag_plan() public void Compose_threads_array_keys_onto_equipment_tag_plan()
@@ -94,7 +94,7 @@ public class ExtractTagArrayTests
TagConfig = "{\"FullName\":\"40001\",\"isArray\":true,\"arrayLength\":16}", TagConfig = "{\"FullName\":\"40001\",\"isArray\":true,\"arrayLength\":16}",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip }, new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(), new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { arrayTag }, new[] { ns }); new[] { arrayTag }, new[] { ns });
@@ -147,7 +147,7 @@ public class ExtractTagArrayTests
TagConfig = "{\"FullName\":\"40005\"}", TagConfig = "{\"FullName\":\"40005\"}",
}; };
var result = Phase7Composer.Compose( var result = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip }, new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(), new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { scalarTag }, new[] { ns }); new[] { scalarTag }, new[] { ns });
@@ -28,7 +28,7 @@ public class ExtractTagHistorizeTests
[InlineData("{\"isHistorized\":true,\"historianTagname\":123}", true, null)] [InlineData("{\"isHistorized\":true,\"historianTagname\":123}", true, null)]
public void ExtractTagHistorize_parses_or_returns_defaults(string? cfg, bool expectedHistorized, string? expectedTagname) public void ExtractTagHistorize_parses_or_returns_defaults(string? cfg, bool expectedHistorized, string? expectedTagname)
{ {
var (isHistorized, historianTagname) = Phase7Composer.ExtractTagHistorize(cfg); var (isHistorized, historianTagname) = AddressSpaceComposer.ExtractTagHistorize(cfg);
isHistorized.ShouldBe(expectedHistorized); isHistorized.ShouldBe(expectedHistorized);
historianTagname.ShouldBe(expectedTagname); historianTagname.ShouldBe(expectedTagname);
} }
@@ -178,7 +178,7 @@ public sealed class DeploymentArtifactAliasParityTests
/// <summary> /// <summary>
/// The load-bearing direct byte-parity proof for the two equipment-tag producers: for the SAME /// The load-bearing direct byte-parity proof for the two equipment-tag producers: for the SAME
/// input draft, the live-edit composer (<see cref="Phase7Composer.Compose"/>) and the /// input draft, the live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the
/// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>) /// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>)
/// must emit IDENTICAL <c>EquipmentTags</c> — element-wise equal on every field /// must emit IDENTICAL <c>EquipmentTags</c> — element-wise equal on every field
/// (TagId, EquipmentId, DriverInstanceId, FolderPath, Name, DataType, FullName) AND in the same /// (TagId, EquipmentId, DriverInstanceId, FolderPath, Name, DataType, FullName) AND in the same
@@ -275,7 +275,7 @@ public sealed class DeploymentArtifactAliasParityTests
var namespaces = new[] { ns }; var namespaces = new[] { ns };
// ---- Side 1: the live-edit composer ---- // ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose( var composed = AddressSpaceComposer.Compose(
areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces); areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces);
// ---- Side 2: serialise the SAME draft to the artifact blob shape ConfigComposer emits // ---- Side 2: serialise the SAME draft to the artifact blob shape ConfigComposer emits
@@ -349,7 +349,7 @@ public sealed class DeploymentArtifactAliasParityTests
/// The native-alarm <c>historizeToAveva</c> opt-out (bool?, the per-condition durable-AVEVA-write /// The native-alarm <c>historizeToAveva</c> opt-out (bool?, the per-condition durable-AVEVA-write
/// gate) is parsed by BOTH equipment-tag producers' <c>ExtractTagAlarm</c> and MUST stay byte-parity: /// gate) is parsed by BOTH equipment-tag producers' <c>ExtractTagAlarm</c> and MUST stay byte-parity:
/// for the SAME tag TagConfig carrying <c>alarm.historizeToAveva: true</c> (and <c>: false</c>), the /// for the SAME tag TagConfig carrying <c>alarm.historizeToAveva: true</c> (and <c>: false</c>), the
/// live-edit composer (<see cref="Phase7Composer.Compose"/>) and the artifact decoder /// live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the artifact decoder
/// (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>) must derive the /// (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>) must derive the
/// identical <c>EquipmentTagAlarmInfo.HistorizeToAveva</c>. The galaxy-tag parity test above already /// identical <c>EquipmentTagAlarmInfo.HistorizeToAveva</c>. The galaxy-tag parity test above already
/// covers the absent ⇒ null case; this pins the explicit-bool branch on both sides. /// covers the absent ⇒ null case; this pins the explicit-bool branch on both sides.
@@ -399,7 +399,7 @@ public sealed class DeploymentArtifactAliasParityTests
}; };
// ---- Side 1: the live-edit composer ---- // ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose( var composed = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip }, new[] { driver }, new[] { area }, new[] { line }, new[] { equip }, new[] { driver },
Array.Empty<ScriptedAlarm>(), new[] { alarmTag }, new[] { ns }); Array.Empty<ScriptedAlarm>(), new[] { alarmTag }, new[] { ns });
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary> /// <summary>
/// Proves the Phase 4c array intent (<c>isArray</c> + optional <c>arrayLength</c>), which rides /// Proves the Phase 4c array intent (<c>isArray</c> + optional <c>arrayLength</c>), which rides
/// inside the raw <c>TagConfig</c> JSON blob, round-trips with byte-parity through both /// inside the raw <c>TagConfig</c> JSON blob, round-trips with byte-parity through both
/// equipment-tag producers: the live-edit composer (<see cref="Phase7Composer.Compose"/>) and the /// equipment-tag producers: the live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the
/// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>). /// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>).
/// A secondary/follower node decoding a serialized deployment artifact MUST materialise array tags /// A secondary/follower node decoding a serialized deployment artifact MUST materialise array tags
/// identically to the primary, so the artifact side must derive <c>IsArray</c> / <c>ArrayLength</c> /// identically to the primary, so the artifact side must derive <c>IsArray</c> / <c>ArrayLength</c>
@@ -153,7 +153,7 @@ public sealed class DeploymentArtifactArrayParityTests
var namespaces = new[] { ns }; var namespaces = new[] { ns };
// ---- Side 1: the live-edit composer ---- // ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose( var composed = AddressSpaceComposer.Compose(
areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces); areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces);
// ---- Side 2: serialise the SAME draft to the artifact blob shape, then decode it ---- // ---- Side 2: serialise the SAME draft to the artifact blob shape, then decode it ----
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// Verifies the artifact-decode mirror substitutes the reserved <c>{{equip}}</c> token in a /// Verifies the artifact-decode mirror substitutes the reserved <c>{{equip}}</c> token in a
/// VirtualTag script's <c>ctx.GetTag("…")</c> literals with the owning equipment's tag base /// VirtualTag script's <c>ctx.GetTag("…")</c> literals with the owning equipment's tag base
/// (derived from its child Equipment-namespace tag's FullName) — byte-parity with /// (derived from its child Equipment-namespace tag's FullName) — byte-parity with
/// <c>Phase7Composer.Compose</c>'s live-edit path, using the same shared /// <c>AddressSpaceComposer.Compose</c>'s live-edit path, using the same shared
/// <c>EquipmentScriptPaths</c> helper and the same equipmentTags-derived base. /// <c>EquipmentScriptPaths</c> helper and the same equipmentTags-derived base.
/// </summary> /// </summary>
public sealed class DeploymentArtifactEquipTokenTests public sealed class DeploymentArtifactEquipTokenTests
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary> /// <summary>
/// Proves the Phase C HistoryRead intent (<c>isHistorized</c> + optional <c>historianTagname</c>), /// Proves the Phase C HistoryRead intent (<c>isHistorized</c> + optional <c>historianTagname</c>),
/// which rides inside the raw <c>TagConfig</c> JSON blob, round-trips with byte-parity through both /// which rides inside the raw <c>TagConfig</c> JSON blob, round-trips with byte-parity through both
/// equipment-tag producers: the live-edit composer (<see cref="Phase7Composer.Compose"/>) and the /// equipment-tag producers: the live-edit composer (<see cref="AddressSpaceComposer.Compose"/>) and the
/// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>). /// artifact decoder (<see cref="DeploymentArtifact.ParseComposition(System.ReadOnlySpan{byte})"/>).
/// The artifact serializer re-parses the SAME <c>TagConfig</c> string both sides emit, so no /// The artifact serializer re-parses the SAME <c>TagConfig</c> string both sides emit, so no
/// ConfigComposer change is needed — the flags are already carried in the blob. /// ConfigComposer change is needed — the flags are already carried in the blob.
@@ -120,7 +120,7 @@ public sealed class DeploymentArtifactHistorizeParityTests
var namespaces = new[] { ns }; var namespaces = new[] { ns };
// ---- Side 1: the live-edit composer ---- // ---- Side 1: the live-edit composer ----
var composed = Phase7Composer.Compose( var composed = AddressSpaceComposer.Compose(
areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces); areas, lines, equipment, drivers, Array.Empty<ScriptedAlarm>(), tags, namespaces);
// ---- Side 2: serialise the SAME draft to the artifact blob shape, then decode it ---- // ---- Side 2: serialise the SAME draft to the artifact blob shape, then decode it ----
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary> /// <summary>
/// Byte-parity tests for the scripted-alarm deployment-artifact decode path /// Byte-parity tests for the scripted-alarm deployment-artifact decode path
/// (<c>DeploymentArtifact.BuildEquipmentScriptedAlarmPlans</c>) against the live compose seam /// (<c>DeploymentArtifact.BuildEquipmentScriptedAlarmPlans</c>) against the live compose seam
/// (<c>Phase7Composer.Compose</c>). Both sides derive <c>EquipmentScriptedAlarmPlan</c> from the /// (<c>AddressSpaceComposer.Compose</c>). Both sides derive <c>EquipmentScriptedAlarmPlan</c> from the
/// same ScriptedAlarm + Script data via the shared <c>EquipmentScriptPaths.ExtractAlarmDependencyRefs</c> /// same ScriptedAlarm + Script data via the shared <c>EquipmentScriptPaths.ExtractAlarmDependencyRefs</c>
/// helper, so the decoded plans must equal the composer's element-wise (the record has value /// helper, so the decoded plans must equal the composer's element-wise (the record has value
/// equality including DependencyRefs order). Mirrors the existing EquipmentVirtualTags parity /// equality including DependencyRefs order). Mirrors the existing EquipmentVirtualTags parity
@@ -70,7 +70,7 @@ public sealed class DeploymentArtifactScriptedAlarmParityTests
Enabled = false, Enabled = false,
}; };
var composed = Phase7Composer.Compose( var composed = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { alarm1, alarm2 }, Array.Empty<DriverInstance>(), new[] { alarm1, alarm2 },
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -189,7 +189,7 @@ public sealed class DeploymentArtifactScriptedAlarmParityTests
Enabled = true, Enabled = true,
}; };
var composed = Phase7Composer.Compose( var composed = AddressSpaceComposer.Compose(
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(), Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
Array.Empty<DriverInstance>(), new[] { goodAlarm, orphanAlarm }, Array.Empty<DriverInstance>(), new[] { goodAlarm, orphanAlarm },
Array.Empty<Tag>(), Array.Empty<Namespace>(), Array.Empty<Tag>(), Array.Empty<Namespace>(),
@@ -257,7 +257,7 @@ public sealed class DeploymentArtifactTests
/// Verifies ParseComposition surfaces Equipment-namespace VirtualTags (joined to their Script /// Verifies ParseComposition surfaces Equipment-namespace VirtualTags (joined to their Script
/// by ScriptId for the expression source) as <c>EquipmentVirtualTags</c>, with the /// by ScriptId for the expression source) as <c>EquipmentVirtualTags</c>, with the
/// <c>DependencyRefs</c> extracted from the script's <c>ctx.GetTag("…")</c> literals — the /// <c>DependencyRefs</c> extracted from the script's <c>ctx.GetTag("…")</c> literals — the
/// artifact-decode mirror of <c>Phase7Composer.Compose</c>'s VirtualTag producer. /// artifact-decode mirror of <c>AddressSpaceComposer.Compose</c>'s VirtualTag producer.
/// </summary> /// </summary>
[Fact] [Fact]
public void ParseComposition_reads_EquipmentVirtualTags_from_virtualtags_and_scripts() public void ParseComposition_reads_EquipmentVirtualTags_from_virtualtags_and_scripts()
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
/// <summary> /// <summary>
/// Byte-parity tests for the VirtualTag <c>Historize</c> flag across the artifact-decode path /// Byte-parity tests for the VirtualTag <c>Historize</c> flag across the artifact-decode path
/// (<c>DeploymentArtifact.BuildEquipmentVirtualTagPlans</c>) and the live compose seam /// (<c>DeploymentArtifact.BuildEquipmentVirtualTagPlans</c>) and the live compose seam
/// (<c>Phase7Composer.Compose</c>) — H5b. The artifact JSON already carries a Pascal-case /// (<c>AddressSpaceComposer.Compose</c>) — H5b. The artifact JSON already carries a Pascal-case
/// <c>"Historize"</c> bool (ConfigComposer serialises the whole VirtualTag entity with /// <c>"Historize"</c> bool (ConfigComposer serialises the whole VirtualTag entity with
/// <c>DefaultIgnoreCondition.Never</c>); the decode just had to read it. Both sides default to /// <c>DefaultIgnoreCondition.Never</c>); the decode just had to read it. Both sides default to
/// <c>false</c> when the flag is unset/absent/non-bool, so the decoded plans must equal the /// <c>false</c> when the flag is unset/absent/non-bool, so the decoded plans must equal the
@@ -65,7 +65,7 @@ public sealed class DeploymentArtifactVirtualTagHistorizeParityTests
var vtHist = new VirtualTag { VirtualTagId = "vt-hist", EquipmentId = "eq-1", Name = "Historized", DataType = "Int32", ScriptId = "s-1", Historize = true }; var vtHist = new VirtualTag { VirtualTagId = "vt-hist", EquipmentId = "eq-1", Name = "Historized", DataType = "Int32", ScriptId = "s-1", Historize = true };
var vtPlain = new VirtualTag { VirtualTagId = "vt-plain", EquipmentId = "eq-1", Name = "Plain", DataType = "Int32", ScriptId = "s-1", Historize = false }; var vtPlain = new VirtualTag { VirtualTagId = "vt-plain", EquipmentId = "eq-1", Name = "Plain", DataType = "Int32", ScriptId = "s-1", Historize = false };
var composed = Phase7Composer.Compose( var composed = AddressSpaceComposer.Compose(
new[] { area }, new[] { line }, new[] { equip }, new[] { area }, new[] { line }, new[] { equip },
new[] { driver }, Array.Empty<ScriptedAlarm>(), new[] { driver }, Array.Empty<ScriptedAlarm>(),
new[] { tag }, new[] { ns }, new[] { tag }, new[] { ns },
@@ -24,7 +24,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
{ {
var db = NewInMemoryDbFactory(); var db = NewInMemoryDbFactory();
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
SeedDeployment(db, equipmentIds: new[] { "eq-1", "eq-2" }, driverIds: new[] { "drv-1" }); SeedDeployment(db, equipmentIds: new[] { "eq-1", "eq-2" }, driverIds: new[] { "drv-1" });
@@ -49,7 +49,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
{ {
var db = NewInMemoryDbFactory(); var db = NewInMemoryDbFactory();
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
// No deployment seeded — LoadLatestArtifact returns empty blob. // No deployment seeded — LoadLatestArtifact returns empty blob.
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests( var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
@@ -69,7 +69,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
{ {
var db = NewInMemoryDbFactory(); var db = NewInMemoryDbFactory();
var sink = new RecordingSink(); var sink = new RecordingSink();
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance); var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
SeedDeployment(db, equipmentIds: new[] { "eq-1" }, driverIds: Array.Empty<string>()); SeedDeployment(db, equipmentIds: new[] { "eq-1" }, driverIds: Array.Empty<string>());
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests( var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
@@ -114,7 +114,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
// --- SITE-A node: only the SITE-A tag's variable, never MAIN's. --- // --- SITE-A node: only the SITE-A tag's variable, never MAIN's. ---
var dbA = NewInMemoryDbFactory(); var dbA = NewInMemoryDbFactory();
var sinkA = new RecordingSink(); var sinkA = new RecordingSink();
var applierA = new Phase7Applier(sinkA, NullLogger<Phase7Applier>.Instance); var applierA = new AddressSpaceApplier(sinkA, NullLogger<AddressSpaceApplier>.Instance);
SeedMultiClusterDeployment(dbA); SeedMultiClusterDeployment(dbA);
var siteActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests( var siteActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
@@ -134,7 +134,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
// --- MAIN node: the mirror — only MAIN's tag's variable, never SITE-A's. --- // --- MAIN node: the mirror — only MAIN's tag's variable, never SITE-A's. ---
var dbM = NewInMemoryDbFactory(); var dbM = NewInMemoryDbFactory();
var sinkM = new RecordingSink(); var sinkM = new RecordingSink();
var applierM = new Phase7Applier(sinkM, NullLogger<Phase7Applier>.Instance); var applierM = new AddressSpaceApplier(sinkM, NullLogger<AddressSpaceApplier>.Instance);
SeedMultiClusterDeployment(dbM); SeedMultiClusterDeployment(dbM);
var mainActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests( var mainActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(