53d222e0f7
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
62 lines
4.0 KiB
C#
62 lines
4.0 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
/// <summary>
|
|
/// Materialises a <see cref="DraftSnapshot"/> from the live config DB so
|
|
/// <see cref="DraftValidator"/> can run against the current edit state at deploy time.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This is a whole-DB ("global") snapshot — every cluster's rows in one pass — which is
|
|
/// what the deploy path needs: the admin-operations actor snapshots and flattens the
|
|
/// entire config, not one cluster. The validator's rules compare entity-level
|
|
/// <c>ClusterId</c> fields against each other (e.g. namespace binding), so the snapshot's
|
|
/// own <see cref="DraftSnapshot.ClusterId"/> is not read by any rule and is left empty.
|
|
/// </para>
|
|
/// <para>
|
|
/// <see cref="DraftSnapshot.GenerationId"/> is a placeholder (the generation model was
|
|
/// dropped); no rule reads it. <see cref="DraftSnapshot.PriorEquipment"/> is empty because
|
|
/// there is no prior-generation table to diff against. <see cref="DraftSnapshot.Enterprise"/>
|
|
/// / <see cref="DraftSnapshot.Site"/> are left null so the path-length rule uses its
|
|
/// conservative upper bound.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class DraftSnapshotFactory
|
|
{
|
|
/// <summary>Builds a <see cref="DraftSnapshot"/> from the current config DB rows.</summary>
|
|
/// <param name="db">The config DB context to read from.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>A snapshot populated from the live DB, ready for <see cref="DraftValidator.Validate"/>.</returns>
|
|
public static async Task<DraftSnapshot> FromConfigDbAsync(OtOpcUaConfigDbContext db, CancellationToken ct = default)
|
|
=> new DraftSnapshot
|
|
{
|
|
// GenerationId=0 and null Enterprise/Site are intentional conservative fallbacks at the
|
|
// global-factory level: the path-length validator uses its upper bound, and cross-gen
|
|
// EquipmentUuid checks run in separate validator passes — no rule here reads these fields.
|
|
GenerationId = 0, // generation model dropped; placeholder (no rule reads it)
|
|
ClusterId = string.Empty, // global snapshot; rules compare entity ClusterId fields, not this
|
|
// v3: Namespace entity retired — no namespace list to materialize. The Raw tree
|
|
// (RawFolders/TagGroups) + UnsTagReferences feed the v3 rules (charset, tagname length,
|
|
// effective-leaf uniqueness).
|
|
RawFolders = await db.RawFolders.AsNoTracking().ToListAsync(ct),
|
|
DriverInstances = await db.DriverInstances.AsNoTracking().ToListAsync(ct),
|
|
Devices = await db.Devices.AsNoTracking().ToListAsync(ct),
|
|
TagGroups = await db.TagGroups.AsNoTracking().ToListAsync(ct),
|
|
UnsAreas = await db.UnsAreas.AsNoTracking().ToListAsync(ct),
|
|
UnsLines = await db.UnsLines.AsNoTracking().ToListAsync(ct),
|
|
Equipment = await db.Equipment.AsNoTracking().ToListAsync(ct),
|
|
Tags = await db.Tags.AsNoTracking().ToListAsync(ct),
|
|
UnsTagReferences = await db.UnsTagReferences.AsNoTracking().ToListAsync(ct),
|
|
VirtualTags = await db.VirtualTags.AsNoTracking().ToListAsync(ct),
|
|
ScriptedAlarms = await db.ScriptedAlarms.AsNoTracking().ToListAsync(ct),
|
|
Scripts = await db.Scripts.AsNoTracking().ToListAsync(ct),
|
|
PollGroups = await db.PollGroups.AsNoTracking().ToListAsync(ct),
|
|
PriorEquipment = [], // intentional: no prior-generation table to diff against at this level
|
|
ActiveReservations = await db.ExternalIdReservations
|
|
.AsNoTracking()
|
|
.Where(r => r.ReleasedAt == null) // active only — matches DraftSnapshot.ActiveReservations semantics
|
|
.ToListAsync(ct),
|
|
};
|
|
}
|