refactor(drivers): delete the dead discovered-node injection path (§8.2, #507)

#507 was filed as "injection inert in v3, re-migrate onto the raw subtree".
Reading it, the retained code is not revivable: it resolves equipment from
EquipmentNode.DriverInstanceId UNION EquipmentTags, and BOTH are structurally
empty in v3 (AddressSpaceComposer always constructs EquipmentNode with a null
DriverInstanceId; DeploymentArtifact hard-codes an empty EquipmentTags set).
Removing the guard would have changed a log line and injected nothing. So it
is deleted rather than fixed, and #507 closes as superseded by /raw
browse-commit.

Removed: HandleDiscoveredNodes, PartitionDiscoveredByDeviceHost,
ShouldWarnPartition, PlansRoutingEqual, ApplyDiscoveredPlansForDriver,
_discoveredByDriver, the redeploy re-inject tail, DiscoveredNodeMapper,
DiscoveredInjection, AddressSpaceApplier.MaterialiseDiscoveredNodes,
OpcUaPublishActor.MaterialiseDiscoveredNodes, and the Runtime-local copies of
CapturingAddressSpaceBuilder/DiscoveredNode.

The connect-time discovery loop goes too (StartDiscovery, RediscoverTick,
HandleRediscoverAsync, DiscoveredNodesReady, TriggerRediscovery). With
injection gone it had no consumer, and leaving it would either dead-letter or
keep browsing real devices up to ~15x per connect to drop the result.
ITagDiscovery itself stays — the /raw browse picker drives it through
Commons/Browsing/DiscoveryDriverBrowser, which never went through the actor,
so the picker is unaffected.

deferment.md §4.4 called the 18 DiscoveryInjectionDormantV3 tests "Real —
blocked on #507". They are not: they assert an equipment-rooted graft
(EquipmentRootNodeId == "EQ-1") that v3 cannot produce, so they could never
have unskipped as written. Deleted along with DiscoveredNodeMapperTests, the
CapturingAddressSpaceBuilder tests, and the MaterialiseDiscoveredNodes tests
in AddressSpaceApplierTests/OpcUaPublishActorTests. Runtime.Tests skipped:
31 -> 13.

IHostConnectivityProbe is deliberately NOT resolved here. GetHostStatuses()
has no production call site and nothing writes a DriverHostStatus row, but the
table was re-created in the v3 initial migration, so deleting on inference
would be wrong. Split to Gitea #521 with both options costed.

CLAUDE.md, docs/drivers/{Galaxy,TwinCAT,MTConnect}.md updated — they described
the seam as dead.

Build clean; Runtime.Tests 476 passed / 13 skipped; OpcUaServer.Tests 362
passed / 4 skipped.
This commit is contained in:
Joseph Doherty
2026-07-27 18:57:09 -04:00
parent 09a401b881
commit adce27e7fa
22 changed files with 117 additions and 4116 deletions
@@ -886,49 +886,6 @@ public sealed class AddressSpaceApplier
return failed;
}
/// <summary>
/// Materialise driver-discovered nodes (FixedTree) under an equipment at runtime. Idempotent:
/// re-applies are cheap (the sink's EnsureFolder/EnsureVariable early-return on existing nodes), so
/// this is safely re-run after every address-space rebuild. Folders are ensured parent-first.
/// Emits a NodeAdded model-change so connected clients can refresh. Discovered nodes are read-only
/// value nodes; array discovered nodes (rare) are forced read-only like the equipment-tag pass.
/// </summary>
/// <param name="equipmentRootNodeId">The equipment root node the discovered nodes hang under; the
/// NodeAdded model-change is announced under this node.</param>
/// <param name="folders">The discovered folders to ensure (parent-first by depth).</param>
/// <param name="variables">The discovered variables to ensure (read-only value nodes).</param>
/// <returns>The count of swallowed sink failures in this pass (0 on a clean apply) — the publish actor
/// surfaces a non-zero count as a degraded discovered-node injection (archreview 01/S-1).</returns>
public int MaterialiseDiscoveredNodes(
string equipmentRootNodeId,
IReadOnlyList<DiscoveredFolder> folders,
IReadOnlyList<DiscoveredVariable> variables)
{
ArgumentException.ThrowIfNullOrEmpty(equipmentRootNodeId);
ArgumentNullException.ThrowIfNull(folders);
ArgumentNullException.ThrowIfNull(variables);
if (folders.Count == 0 && variables.Count == 0) return 0;
var failed = 0;
// Parent-first: a child folder's parent must exist before it. Ordering by '/' count == depth.
foreach (var f in folders.OrderBy(f => f.NodeId.Count(c => c == '/')))
if (!SafeEnsureFolder(f.NodeId, f.ParentNodeId, f.DisplayName, AddressSpaceRealm.Raw)) failed++;
foreach (var v in variables)
{
// Mirror MaterialiseEquipmentTags: arrays forced read-only (the driver write path can't handle arrays).
var writable = v.Writable && !v.IsArray;
if (!SafeEnsureVariable(v.NodeId, v.ParentNodeId, v.DisplayName, v.DataType, writable,
AddressSpaceRealm.Raw, historianTagname: null, isArray: v.IsArray, arrayLength: v.ArrayLength)) failed++;
}
_sink.RaiseNodesAddedModelChange(equipmentRootNodeId, AddressSpaceRealm.Raw);
_logger.LogInformation(
"AddressSpaceApplier: discovered nodes materialised under {Equipment} (folders={Folders}, vars={Vars}, failed={Failed})",
equipmentRootNodeId, folders.Count, variables.Count, failed);
return failed;
}
/// <summary>
/// Materialise Equipment-namespace VirtualTags from a composition snapshot — the VirtualTag
@@ -1,8 +0,0 @@
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// <summary>A folder to ensure during discovered-node injection (NodeId + parent + display).</summary>
public sealed record DiscoveredFolder(string NodeId, string? ParentNodeId, string DisplayName);
/// <summary>A read-or-write variable to ensure during discovered-node injection.</summary>
public sealed record DiscoveredVariable(
string NodeId, string ParentNodeId, string DisplayName, string DataType, bool Writable, bool IsArray, uint? ArrayLength);