fix(virtualtags): a disabled VirtualTag is no longer deployed or evaluated (lmxopcua#787)
v2-ci / unit-tests (push) Waiting to run

VirtualTag.Enabled was authorable but ignored by both runtime-plan seams.
AddressSpaceComposer.Compose and DeploymentArtifact.BuildEquipmentVirtualTagPlans
now drop disabled rows (byte-parity), so no node is materialised and no
VirtualTagActor is spawned - the same outcome as a disabled scripted alarm.
DraftValidator exempts a disabled VT from VirtualTagNoEffectiveTrigger.
Calculation tags have no Enabled flag, so no equivalent gap. docs/VirtualTags.md updated.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-10 03:15:22 -05:00
parent 1ead8f458a
commit 963b0d66fc
7 changed files with 86 additions and 2 deletions
@@ -615,7 +615,10 @@ public static class AddressSpaceComposer
// DependencyRefs = the distinct ctx.GetTag("…") literals the VirtualTagActor subscribes to.
// VirtualTag has no FolderPath today → "".
var scriptsById = resolvedScripts.ToDictionary(s => s.ScriptId, StringComparer.Ordinal);
// A disabled VirtualTag (lmxopcua#787) is not part of the runtime plan: no node, no evaluation —
// the same as a disabled scripted alarm (which is not materialised). Mirrored by the artifact decode.
var equipmentVirtualTags = vtags
.Where(v => v.Enabled)
.OrderBy(v => v.EquipmentId, StringComparer.Ordinal)
.ThenBy(v => v.Name, StringComparer.Ordinal)
.Select(v =>
@@ -1405,6 +1405,10 @@ internal sealed class ParsedDeploymentArtifact : IDisposable
if (string.IsNullOrWhiteSpace(virtualTagId) || string.IsNullOrWhiteSpace(equipmentId)
|| string.IsNullOrWhiteSpace(name)) continue;
// Enabled (lmxopcua#787): a disabled VirtualTag is not deployed or evaluated — byte-parity with
// AddressSpaceComposer.Compose's `.Where(v => v.Enabled)`. Absent/non-bool => the entity default (true).
if (el.TryGetProperty("Enabled", out var enEl) && enEl.ValueKind == JsonValueKind.False) continue;
var source = scriptId is not null && scriptSourceById.TryGetValue(scriptId, out var src)
? src : string.Empty;