feat(driver-calc): B2-WP7 Calculation driver — IDependencyConsumer + triggers + deploy cycle gate
Adds the Calculation pseudo-driver (tags computed by C# scripts over other tags' live values) plus the host seam that feeds it: - IDependencyConsumer capability interface (Core.Abstractions) + DriverTypeNames.Calculation. - CalculationDriver : IDriver, ISubscribable, IReadable, IDependencyConsumer — change-gate + dedupe (VirtualTagActor parity), timer-group scheduler, Bad-transition + script-log emission, Good recovery; reuses the T0-4 CalculationEvaluator. - DriverHostActor spawns a DependencyConsumerMuxAdapter per dependency-consuming driver, registering its refs on the per-node DependencyMuxActor and forwarding DependencyValueChanged → OnDependencyValue; re-registers on every apply, torn down with the driver. - Factory + probe registered in DriverFactoryBootstrap (keeps the T0-3 guard green); guard test csproj references the driver so the bin-scan discovers the factory. - DeploymentArtifact injects the resolved scriptSource (scriptId → SourceCode) into a calc tag's delivered TagConfig so the host-blind driver can compile it. - DraftValidator deploy gates: CalculationScriptMissing / CalculationScriptNotFound (scriptId existence) + CalculationDependencyCycle (DependencyGraph.DetectCycles over calc→calc edges). - Tests: driver units (dep-ref extraction, change-gate, dedupe, Bad+script-log+recovery, timer, read), tag-config parsing, DraftValidator gates (self/2-cycle/cross-driver-terminal), and a Runtime integration test proving values FLOW mux → adapter → driver → calc-of-calc end-to-end. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -141,6 +142,13 @@ public static class DeploymentArtifact
|
||||
|
||||
var resolver = new RawPathResolver(folders, drivers, devices, groups);
|
||||
|
||||
// scriptId → SourceCode, from the artifact's Scripts array — the same join the VirtualTag plan
|
||||
// builder uses. A calc tag's TagConfig carries a scriptId (not the source); the calc driver is
|
||||
// host-blind and can't resolve it, so we inject the resolved `scriptSource` into the delivered
|
||||
// TagConfig here (the deploy-side counterpart of the Calculation driver). Tags with no scriptId
|
||||
// are untouched.
|
||||
var scriptSourceById = BuildScriptSourceMap(root);
|
||||
|
||||
foreach (var el in EnumerateArray(root, "Tags"))
|
||||
{
|
||||
var deviceId = ReadString(el, "DeviceId");
|
||||
@@ -152,6 +160,7 @@ public static class DeploymentArtifact
|
||||
if (rawPath is null) continue; // broken chain — dropped (deploy gate rejects invalid names)
|
||||
var tagConfig = el.TryGetProperty("TagConfig", out var tcEl) && tcEl.ValueKind == JsonValueKind.String
|
||||
? tcEl.GetString() ?? "{}" : "{}";
|
||||
tagConfig = InjectScriptSource(tagConfig, scriptSourceById);
|
||||
// WriteIdempotent (bool; non-bool/absent ⇒ false — the safe R2 default: writes are non-idempotent).
|
||||
var writeIdempotent = el.TryGetProperty("WriteIdempotent", out var wiEl)
|
||||
&& wiEl.ValueKind is JsonValueKind.True or JsonValueKind.False && wiEl.GetBoolean();
|
||||
@@ -168,6 +177,47 @@ public static class DeploymentArtifact
|
||||
return byDriver;
|
||||
}
|
||||
|
||||
/// <summary>Build the <c>scriptId → SourceCode</c> map from the artifact's <c>Scripts</c> array (mirrors
|
||||
/// the VirtualTag plan builder's join). Empty when the artifact carries no Scripts.</summary>
|
||||
private static Dictionary<string, string> BuildScriptSourceMap(JsonElement root)
|
||||
{
|
||||
var byId = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var el in EnumerateArray(root, "Scripts"))
|
||||
{
|
||||
var sid = ReadString(el, "ScriptId");
|
||||
if (string.IsNullOrWhiteSpace(sid)) continue;
|
||||
byId[sid!] = el.TryGetProperty("SourceCode", out var srcEl) && srcEl.ValueKind == JsonValueKind.String
|
||||
? srcEl.GetString() ?? string.Empty : string.Empty;
|
||||
}
|
||||
return byId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When <paramref name="tagConfig"/> carries a <c>scriptId</c> (a calc tag), inject the resolved
|
||||
/// <c>scriptSource</c> so the host-blind Calculation driver can compile + evaluate it. Identity for
|
||||
/// tags with no <c>scriptId</c> (every other driver's tags), an unresolvable id, or malformed JSON —
|
||||
/// so no existing tag's blob changes. The pre-existing <c>scriptId</c>/trigger fields are preserved.
|
||||
/// </summary>
|
||||
/// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON.</param>
|
||||
/// <param name="scriptSourceById">The <c>scriptId → SourceCode</c> map from the artifact.</param>
|
||||
/// <returns>The TagConfig with <c>scriptSource</c> injected, or the original when nothing to inject.</returns>
|
||||
private static string InjectScriptSource(string tagConfig, IReadOnlyDictionary<string, string> scriptSourceById)
|
||||
{
|
||||
if (scriptSourceById.Count == 0 || string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
|
||||
JsonNode? node;
|
||||
try { node = JsonNode.Parse(tagConfig); }
|
||||
catch (JsonException) { return tagConfig; }
|
||||
if (node is not JsonObject obj) return tagConfig;
|
||||
|
||||
var scriptId = obj.TryGetPropertyValue("scriptId", out var sidNode) && sidNode is JsonValue sv
|
||||
&& sv.TryGetValue<string>(out var sid) ? sid : null;
|
||||
if (string.IsNullOrWhiteSpace(scriptId) || !scriptSourceById.TryGetValue(scriptId!, out var source))
|
||||
return tagConfig;
|
||||
|
||||
obj["scriptSource"] = source;
|
||||
return obj.ToJsonString();
|
||||
}
|
||||
|
||||
/// <summary>Group the artifact's Devices into per-driver <see cref="DriverDeviceConfigMerger.DeviceRow"/>
|
||||
/// lists (entity Name + schemaless DeviceConfig), ordered by DeviceId for a stable merge.</summary>
|
||||
/// <param name="root">The artifact root element.</param>
|
||||
|
||||
Reference in New Issue
Block a user