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
@@ -4,9 +4,9 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <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="Phase7CompositionResult"/> snapshots:
/// <see cref="AddressSpaceComposition"/> snapshots:
///
/// <list type="bullet">
/// <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"/>,
/// and tests can capture every call.
/// </summary>
public sealed class Phase7Applier
public sealed class AddressSpaceApplier
{
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="logger">The logger instance.</param>
public Phase7Applier(IOpcUaAddressSpaceSink sink, ILogger<Phase7Applier> logger)
public AddressSpaceApplier(IOpcUaAddressSpaceSink sink, ILogger<AddressSpaceApplier> logger)
{
ArgumentNullException.ThrowIfNull(sink);
ArgumentNullException.ThrowIfNull(logger);
@@ -44,15 +44,15 @@ public sealed class Phase7Applier
/// callers (OpcUaPublishActor) can correlate the work back to the originating deployment.
/// </summary>
/// <param name="plan">The plan to apply.</param>
/// <returns>A Phase7ApplyOutcome summarizing the applied changes.</returns>
public Phase7ApplyOutcome Apply(Phase7Plan plan)
/// <returns>A AddressSpaceApplyOutcome summarizing the applied changes.</returns>
public AddressSpaceApplyOutcome Apply(AddressSpacePlan plan)
{
ArgumentNullException.ThrowIfNull(plan);
if (plan.IsEmpty)
{
_logger.LogDebug("Phase7Applier: plan is empty; skipping sink writes");
return new Phase7ApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
_logger.LogDebug("AddressSpaceApplier: plan is empty; skipping sink writes");
return new AddressSpaceApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
}
var ts = DateTime.UtcNow;
@@ -68,7 +68,7 @@ public sealed class Phase7Applier
removedCount++;
}
// 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.
removedCount += plan.RemovedEquipmentTags.Count + plan.RemovedEquipmentVirtualTags.Count;
@@ -136,7 +136,7 @@ public sealed class Phase7Applier
try { ok = surgical.UpdateTagAttributes(nodeId, writable, historian); }
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;
}
if (!ok) { allApplied = false; break; }
@@ -152,10 +152,10 @@ public sealed class Phase7Applier
}
_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);
return new Phase7ApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
return new AddressSpaceApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
}
private void SafeRebuild()
@@ -166,7 +166,7 @@ public sealed class Phase7Applier
}
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.
/// </summary>
/// <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);
@@ -198,7 +198,7 @@ public sealed class Phase7Applier
}
_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);
}
@@ -223,7 +223,7 @@ public sealed class Phase7Applier
/// </para>
/// </summary>
/// <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);
if (composition.EquipmentTags.Count == 0) return;
@@ -276,7 +276,7 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: equipment tags materialised (tags={Tags}, equipment={Equipment})",
"AddressSpaceApplier: equipment tags materialised (tags={Tags}, equipment={Equipment})",
composition.EquipmentTags.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>).
/// </summary>
/// <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);
if (composition.EquipmentVirtualTags.Count == 0) return;
@@ -328,7 +328,7 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})",
"AddressSpaceApplier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})",
composition.EquipmentVirtualTags.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).
/// </summary>
/// <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);
if (composition.EquipmentScriptedAlarms.Count == 0) return;
@@ -357,7 +357,7 @@ public sealed class Phase7Applier
}
_logger.LogInformation(
"Phase7Applier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
"AddressSpaceApplier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
materialised,
composition.EquipmentScriptedAlarms.Where(a => a.Enabled)
.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)
{
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)
{
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
@@ -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.
// 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).
private static bool VtagDeltaIsNodeIrrelevant(Phase7Plan.EquipmentVirtualTagDelta d) =>
private static bool VtagDeltaIsNodeIrrelevant(AddressSpacePlan.EquipmentVirtualTagDelta d) =>
(d.Previous with
{
Expression = d.Current.Expression,
@@ -394,7 +394,7 @@ public sealed class Phase7Applier
// ISurgicalAddressSpaceSink.UpdateTagAttributes, avoiding a full rebuild (preserving subscriptions).
// DataType / IsArray / ArrayLength / FullName / DriverInstanceId / identity / alarm differences fall
// 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 with
{
@@ -418,18 +418,18 @@ public sealed class Phase7Applier
private void SafeWriteAlarmCondition(string nodeId, AlarmConditionSnapshot state, DateTime 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)
{
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>
public sealed record Phase7ApplyOutcome(
public sealed record AddressSpaceApplyOutcome(
int RemovedNodes,
int AddedNodes,
int ChangedNodes,
@@ -6,11 +6,11 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
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
/// 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>
public sealed record Phase7CompositionResult(
public sealed record AddressSpaceComposition(
IReadOnlyList<UnsAreaProjection> UnsAreas,
IReadOnlyList<UnsLineProjection> UnsLines,
IReadOnlyList<EquipmentNode> EquipmentNodes,
@@ -21,7 +21,7 @@ public sealed record Phase7CompositionResult(
/// <param name="equipmentNodes">The equipment nodes.</param>
/// <param name="driverInstancePlans">The driver instance plans.</param>
/// <param name="scriptedAlarmPlans">The scripted alarm plans.</param>
public Phase7CompositionResult(
public AddressSpaceComposition(
IReadOnlyList<EquipmentNode> equipmentNodes,
IReadOnlyList<DriverInstancePlan> driverInstancePlans,
IReadOnlyList<ScriptedAlarmPlan> scriptedAlarmPlans)
@@ -32,7 +32,7 @@ public sealed record Phase7CompositionResult(
/// <summary>
/// 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
/// 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
@@ -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"/>
/// 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
/// 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
/// <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
@@ -117,7 +117,7 @@ public sealed record EquipmentTagAlarmInfo(string AlarmType, int Severity, bool?
/// <summary>
/// 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="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
/// (<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
@@ -252,19 +252,19 @@ public sealed record EquipmentScriptedAlarmPlan(
/// startup (Task 53) consumes the result and hands it to the node-manager factory.
///
/// #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>EquipmentNodeWalker</c> integration that did this server-side is fully replaced by the
/// (composer → applier → sink → node manager) chain.
/// </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>
/// <param name="equipment">The equipment.</param>
/// <param name="driverInstances">The driver instances.</param>
/// <param name="scriptedAlarms">The scripted alarms.</param>
/// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose(
public static AddressSpaceComposition Compose(
IReadOnlyList<Equipment> equipment,
IReadOnlyList<DriverInstance> driverInstances,
IReadOnlyList<ScriptedAlarm> scriptedAlarms) =>
@@ -278,7 +278,7 @@ public static class Phase7Composer
/// <param name="driverInstances">The driver instances.</param>
/// <param name="scriptedAlarms">The scripted alarms.</param>
/// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose(
public static AddressSpaceComposition Compose(
IReadOnlyList<UnsArea> unsAreas,
IReadOnlyList<UnsLine> unsLines,
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="scripts">The scripts joined to <paramref name="virtualTags"/> by ScriptId for the expression. <c>null</c> = none.</param>
/// <returns>The composition result.</returns>
public static Phase7CompositionResult Compose(
public static AddressSpaceComposition Compose(
IReadOnlyList<UnsArea> unsAreas,
IReadOnlyList<UnsLine> unsLines,
IReadOnlyList<Equipment> equipment,
@@ -434,7 +434,7 @@ public static class Phase7Composer
if (!scriptsById.TryGetValue(a.PredicateScriptId, out var s))
{
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.",
a.ScriptedAlarmId, a.EquipmentId, a.PredicateScriptId);
continue;
@@ -458,7 +458,7 @@ public static class Phase7Composer
Enabled: a.Enabled));
}
return new Phase7CompositionResult(areas, lines, nodes, plans, alarms)
return new AddressSpaceComposition(areas, lines, nodes, plans, alarms)
{
EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags,
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <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
/// deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm)
/// 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
/// drastic schema flips.
/// </summary>
public sealed record Phase7Plan(
public sealed record AddressSpacePlan(
IReadOnlyList<EquipmentNode> AddedEquipment,
IReadOnlyList<EquipmentNode> RemovedEquipment,
IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment,
IReadOnlyList<AddressSpacePlan.EquipmentDelta> ChangedEquipment,
IReadOnlyList<DriverInstancePlan> AddedDrivers,
IReadOnlyList<DriverInstancePlan> RemovedDrivers,
IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers,
IReadOnlyList<AddressSpacePlan.DriverDelta> ChangedDrivers,
IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms)
IReadOnlyList<AddressSpacePlan.AlarmDelta> ChangedAlarms)
{
/// <summary>
/// Equipment-namespace tag diff sets, keyed by <see cref="EquipmentTagPlan.TagId"/>. Added as
/// init-only members (defaulting empty) rather than positional parameters so existing
/// <c>Phase7Plan</c> construction sites compile unchanged — consistent with how
/// <see cref="Phase7CompositionResult.EquipmentTags"/> was added. Without these, an
/// <c>AddressSpacePlan</c> construction sites compile unchanged — consistent with how
/// <see cref="AddressSpaceComposition.EquipmentTags"/> was added. Without these, an
/// incremental deploy that changes ONLY equipment tags produced an empty plan and
/// <c>OpcUaPublishActor.HandleRebuild</c> short-circuited before materialising them.
/// </summary>
@@ -67,7 +67,7 @@ public sealed record Phase7Plan(
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
}
public static class Phase7Planner
public static class AddressSpacePlanner
{
/// <summary>
/// Diff two compositions, emitting Added/Removed/Changed sets per entity class.
@@ -77,7 +77,7 @@ public static class Phase7Planner
/// </summary>
/// <param name="previous">The previous 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(next);
@@ -85,22 +85,22 @@ public static class Phase7Planner
var (addedEq, removedEq, changedEq) = DiffById(
previous.EquipmentNodes, next.EquipmentNodes,
n => n.EquipmentId,
(a, b) => new Phase7Plan.EquipmentDelta(a, b));
(a, b) => new AddressSpacePlan.EquipmentDelta(a, b));
var (addedDrv, removedDrv, changedDrv) = DiffById(
previous.DriverInstancePlans, next.DriverInstancePlans,
d => d.DriverInstanceId,
(a, b) => new Phase7Plan.DriverDelta(a, b));
(a, b) => new AddressSpacePlan.DriverDelta(a, b));
var (addedAlarm, removedAlarm, changedAlarm) = DiffById(
previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans,
a => a.ScriptedAlarmId,
(a, b) => new Phase7Plan.AlarmDelta(a, b));
(a, b) => new AddressSpacePlan.AlarmDelta(a, b));
var (addedEqTags, removedEqTags, changedEqTags) = DiffById(
previous.EquipmentTags, next.EquipmentTags,
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
// 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(
previous.EquipmentVirtualTags, next.EquipmentVirtualTags,
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,
addedDrv, removedDrv, changedDrv,
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
/// 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
/// <see cref="BaseDataVariableState"/> under the namespace root.
/// </summary>
@@ -1252,7 +1252,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// <summary>
/// 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).
/// #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 so adding child variables under it still works.
/// </summary>
@@ -1424,7 +1424,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
_ => 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
/// EnsureFolder + WriteValue on the next pass.</summary>
public void RebuildAddressSpace()