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
@@ -169,8 +169,8 @@ public static class DeploymentArtifact
}
/// <summary>
/// Parse the artifact into the projected <see cref="Phase7CompositionResult"/> used by
/// <c>Phase7Planner</c> + <c>Phase7Applier</c>. Returns an empty composition for empty/
/// Parse the artifact into the projected <see cref="AddressSpaceComposition"/> used by
/// <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.
///
/// The artifact JSON is produced by <c>ConfigComposer.SnapshotAndFlattenAsync</c> in the
@@ -179,7 +179,7 @@ public static class DeploymentArtifact
/// nodes.
/// </summary>
/// <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();
@@ -197,7 +197,7 @@ public static class DeploymentArtifact
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags);
var equipmentScriptedAlarms = BuildEquipmentScriptedAlarmPlans(root);
return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms)
return new AddressSpaceComposition(areas, lines, equipment, drivers, alarms)
{
EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags,
@@ -224,7 +224,7 @@ public static class DeploymentArtifact
/// <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>
/// <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)
{
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(),
keptLines,
keptEquipment,
@@ -348,7 +348,7 @@ public static class DeploymentArtifact
}
}
private static Phase7CompositionResult Empty() => new(
private static AddressSpaceComposition Empty() => new(
Array.Empty<UnsAreaProjection>(),
Array.Empty<UnsLineProjection>(),
Array.Empty<EquipmentNode>(),
@@ -359,7 +359,7 @@ public static class DeploymentArtifact
/// Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find
/// Equipment-namespace tags (non-null EquipmentId, owning namespace Kind == Equipment), then
/// 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
/// (top-level "FullName" field).
/// </summary>
@@ -421,7 +421,7 @@ public static class DeploymentArtifact
// AccessLevel → Writable. ConfigComposer serialises the TagAccessLevel enum WITHOUT a
// 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
// 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).
var writable = el.TryGetProperty("AccessLevel", out var alEl) && alEl.ValueKind switch
{
@@ -469,7 +469,7 @@ public static class DeploymentArtifact
/// <summary>
/// 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
/// <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
/// 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
@@ -529,14 +529,14 @@ public static class DeploymentArtifact
// Historize: the artifact carries a Pascal-case "Historize" bool (ConfigComposer serialises
// 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,
// 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)
&& (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False)
&& hEl.GetBoolean();
// 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
// Phase7Composer.Compose.
// AddressSpaceComposer.Compose.
var expanded = EquipmentScriptPaths.SubstituteEquipmentToken(
source, baseByEquip.GetValueOrDefault(equipmentId!));
@@ -562,7 +562,7 @@ public static class DeploymentArtifact
/// <summary>
/// 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
/// <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
/// 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>
@@ -616,7 +616,7 @@ public static class DeploymentArtifact
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).
if (predicateScriptId is null || !scriptSourceById.TryGetValue(predicateScriptId, out var source))
continue;
@@ -646,7 +646,7 @@ public static class DeploymentArtifact
/// <summary>
/// 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
/// the Core driver assembly. Falls back to the raw blob when absent or non-JSON.
/// </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
/// 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)
{
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
&& sEl.TryGetInt32(out var sv) ? sv : 500;
// 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)
&& hEl.ValueKind is JsonValueKind.True or JsonValueKind.False
? 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,
/// 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>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)
{
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
/// 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
/// (<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)
{
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
@@ -764,7 +764,7 @@ public static class DeploymentArtifact
var item = reader(el);
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.
return result.OrderBy(IdentityOf, StringComparer.Ordinal).ToList();
}
@@ -939,7 +939,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
return;
}
Phase7CompositionResult composition;
AddressSpaceComposition composition;
try
{
composition = DeploymentArtifact.ParseComposition(blob, _localNode.Value);