Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9c3a8eb
Add hybrid report acquisition planning contracts
masarray Aug 14, 2026
7466fee
Add hybrid report physical validation tracker
masarray Aug 14, 2026
ea9e702
Consume ARIEC hybrid report acquisition planner
masarray Aug 14, 2026
d9a851a
Mark engine-authoritative report plans
masarray Aug 14, 2026
cfe5831
Pin ARIEC hybrid acquisition planner revision
masarray Aug 14, 2026
4ad02ab
Add one-shot guarded hybrid runtime patch
masarray Aug 14, 2026
91a63f3
Run guarded hybrid runtime patch
masarray Aug 14, 2026
1f434b5
Narrow guarded hybrid runtime patch scope
masarray Aug 14, 2026
b92f823
Retry guarded hybrid runtime patch with method scope
masarray Aug 14, 2026
555c262
Integrate ARIEC hybrid acquisition into monitor runtime [hybrid-runti…
github-actions[bot] Aug 14, 2026
02e6a2b
Remove one-shot hybrid runtime patch workflow
masarray Aug 14, 2026
86aefa4
Remove one-shot hybrid runtime patch script
masarray Aug 14, 2026
ad41199
Remove guarded hybrid runtime patch helper
masarray Aug 14, 2026
fe63fcc
Add guarded hybrid authority cleanup helper
masarray Aug 14, 2026
f4dd0a5
Run guarded hybrid authority cleanup
masarray Aug 14, 2026
5459d42
Expose internal hybrid evidence tracker to tests
masarray Aug 14, 2026
a43cf44
Make ARIEC hybrid authority exclusive in monitor planning [hybrid-aut…
github-actions[bot] Aug 14, 2026
8c99499
Remove one-shot hybrid authority cleanup workflow
masarray Aug 14, 2026
14d1dc6
Remove guarded hybrid authority cleanup helper
masarray Aug 14, 2026
162e2af
Characterize ARIEC hybrid physical evidence semantics
masarray Aug 14, 2026
35ba374
Strengthen hybrid physical activation evidence
masarray Aug 14, 2026
2869aa7
Record static and dynamic activation details
masarray Aug 14, 2026
7cf61a3
Add guarded hybrid execution revalidation helper
masarray Aug 14, 2026
a013a88
Run guarded hybrid execution revalidation
masarray Aug 14, 2026
d51bf7f
Revalidate ARIEC hybrid plan before execution [hybrid-execution-reval…
github-actions[bot] Aug 14, 2026
37e4e47
Remove one-shot hybrid execution revalidation workflow
masarray Aug 14, 2026
20e8a30
Remove guarded hybrid execution revalidation helper
masarray Aug 14, 2026
0057fd1
Characterize dynamic setup evidence separately from report traffic
masarray Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions Models/NativeHybridReportAcquisitionModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace ArIED61850Tester.Models;

/// <summary>
/// ARSAS projection of an ARIEC-owned hybrid report acquisition plan.
/// The application never infers report capability from these counters; they only expose
/// the typed decision already returned by the engine planner.
/// </summary>
public sealed class NativeHybridReportPlanningResult
{
public bool IsAuthoritative { get; init; }
public string Authority { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public string Summary { get; init; } = string.Empty;
public IReadOnlyList<ReportControlPlan> ReportPlans { get; init; } = Array.Empty<ReportControlPlan>();
public IReadOnlyList<string> PollingPointKeys { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> UncoveredPointKeys { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> UnmappedPointKeys { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
public int RequestedPointCount { get; init; }
public int CatalogMappedPointCount { get; init; }
public int StaticBrcbSignalCount { get; init; }
public int StaticUrcbSignalCount { get; init; }
public int DynamicBrcbSignalCount { get; init; }
public int DynamicUrcbSignalCount { get; init; }
public int PollingFallbackSignalCount { get; init; }
public int UncoveredSignalCount { get; init; }

public bool HasReportPlans => ReportPlans.Count > 0;
}

/// <summary>
/// Physical-validation evidence collected by ARSAS after executing an engine-authoritative
/// report plan against a real IED. Silence is never interpreted as signal absence.
/// </summary>
public sealed class HybridReportPhysicalValidationSnapshot
{
public DateTimeOffset CapturedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public string DeviceId { get; init; } = string.Empty;
public string DeviceName { get; init; } = string.Empty;
public string Endpoint { get; init; } = string.Empty;
public int PlannedStaticBrcbCount { get; init; }
public int PlannedStaticUrcbCount { get; init; }
public int PlannedDynamicBrcbCount { get; init; }
public int PlannedDynamicUrcbCount { get; init; }
public int ActivatedReportPlanCount { get; init; }
public int FailedActivationCount { get; init; }
public int ReportFrameCount { get; init; }
public int ReportUpdateCount { get; init; }
public int ChangeVerifiedPointCount { get; init; }
public int PollingFallbackPointCount { get; init; }
public int UncoveredPointCount { get; init; }
public IReadOnlyList<HybridReportPhysicalValidationPlan> Plans { get; init; } = Array.Empty<HybridReportPhysicalValidationPlan>();
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();

public bool HasPhysicalReportEvidence => ReportFrameCount > 0 || ReportUpdateCount > 0;
}

public sealed class HybridReportPhysicalValidationPlan
{
public string PlanId { get; init; } = string.Empty;
public string AcquisitionKind { get; init; } = string.Empty;
public string ReportControlReference { get; init; } = string.Empty;
public string DataSetReference { get; init; } = string.Empty;
public int PlannedSignalCount { get; init; }
public bool ActivationSucceeded { get; init; }
public string ActivationMessage { get; init; } = string.Empty;
public string SubscriptionSummary { get; init; } = string.Empty;
public int MemberCount { get; init; }
public int SetupWriteStepCount { get; init; }
public bool UsedDynamicDataSet { get; init; }
public int ReportFrameCount { get; init; }
public int ReportUpdateCount { get; init; }
public int ChangeVerifiedPointCount { get; init; }
public DateTimeOffset? FirstReportAtUtc { get; init; }
public DateTimeOffset? LastReportAtUtc { get; init; }
}
6 changes: 6 additions & 0 deletions Models/ReportControlPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public sealed class ReportControlPlan
public string TriggerOptions { get; set; } = string.Empty;
public string OptionalFields { get; set; } = string.Empty;
public string Status { get; set; } = "Planned";
/// <summary>
/// True when the RCB/DataSet choice and exact subscription plan were emitted by
/// ARIEC61850 MmsHybridReportAcquisitionPlanner. ARSAS executes this plan as-is.
/// </summary>
public bool IsEngineAuthoritative { get; set; }
public string EngineAcquisitionKind { get; set; } = string.Empty;
public List<Iec61850MonitorPoint> Bindings { get; set; } = new();

public int BindingCount => Bindings.Count;
Expand Down
3 changes: 3 additions & 0 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ARSAS.Tests")]
166 changes: 166 additions & 0 deletions Services/HybridReportPhysicalValidationTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using ArIED61850Tester.Models;

namespace ArIED61850Tester.Services;

/// <summary>
/// Collects application-side physical evidence after ARIEC has produced an authoritative
/// hybrid report plan. The tracker records only observations from actual start/slice
/// results. A lack of report traffic is intentionally not converted into signal absence.
/// </summary>
internal sealed class HybridReportPhysicalValidationTracker
{
private sealed class PlanState
{
public required ReportControlPlan Plan { get; init; }
public bool ActivationAttempted { get; set; }
public bool ActivationSucceeded { get; set; }
public string ActivationMessage { get; set; } = "Not attempted";
public string SubscriptionSummary { get; set; } = string.Empty;
public int MemberCount { get; set; }
public int SetupWriteStepCount { get; set; }
public bool UsedDynamicDataSet { get; set; }
public int ReportFrameCount { get; set; }
public int ReportUpdateCount { get; set; }
public HashSet<string> ChangeVerifiedPointKeys { get; } = new(StringComparer.OrdinalIgnoreCase);
public DateTimeOffset? FirstReportAtUtc { get; set; }
public DateTimeOffset? LastReportAtUtc { get; set; }
}

private readonly Dictionary<string, PlanState> _plans = new(StringComparer.OrdinalIgnoreCase);
private readonly List<string> _warnings = new();
private NativeHybridReportPlanningResult? _planning;

public void Reset(NativeHybridReportPlanningResult? planning)
{
_planning = planning;
_plans.Clear();
_warnings.Clear();

if (planning is null)
return;

foreach (var warning in planning.Warnings)
AddWarning(warning);

foreach (var plan in planning.ReportPlans)
_plans[plan.PlanId] = new PlanState { Plan = plan };
}

public void RecordActivation(ReportControlPlan plan, NativeReportMonitorStartResult result)
{
if (!plan.IsEngineAuthoritative)
return;

if (!_plans.TryGetValue(plan.PlanId, out var state))
{
state = new PlanState { Plan = plan };
_plans[plan.PlanId] = state;
}

state.ActivationAttempted = true;
state.ActivationSucceeded = result.IsSuccess;
state.ActivationMessage = result.Message;
state.SubscriptionSummary = result.SubscriptionSummary;
state.MemberCount = result.MemberCount;
state.SetupWriteStepCount = result.WriteStepCount;
state.UsedDynamicDataSet = result.UsedDynamicDataSet;
foreach (var warning in result.Warnings)
AddWarning(warning);
}

public void RecordSlice(
ReportControlPlan plan,
NativeReportMonitorSliceResult slice,
IEnumerable<string>? changeVerifiedPointKeys = null)
{
if (!plan.IsEngineAuthoritative || !_plans.TryGetValue(plan.PlanId, out var state))
return;

state.ReportFrameCount += slice.ReportFrames.Count;
state.ReportUpdateCount += slice.Updates.Count;

var observedTimes = slice.ReportFrames
.Select(frame => frame.ReceivedAt)
.Where(value => value != default)
.OrderBy(value => value)
.ToArray();
if (observedTimes.Length > 0)
{
state.FirstReportAtUtc ??= observedTimes[0];
state.LastReportAtUtc = observedTimes[^1];
}
else if (slice.ReportFrames.Count > 0 || slice.Updates.Count > 0)
{
var now = DateTimeOffset.UtcNow;
state.FirstReportAtUtc ??= now;
state.LastReportAtUtc = now;
}

if (changeVerifiedPointKeys is not null)
{
foreach (var key in changeVerifiedPointKeys.Where(key => !string.IsNullOrWhiteSpace(key)))
state.ChangeVerifiedPointKeys.Add(key);
}

foreach (var warning in slice.Warnings)
AddWarning(warning);
}

public HybridReportPhysicalValidationSnapshot Capture(Iec61850MonitorDevice device)
{
ArgumentNullException.ThrowIfNull(device);
var planning = _planning;
var plans = _plans.Values
.OrderBy(state => state.Plan.EngineAcquisitionKind, StringComparer.OrdinalIgnoreCase)
.ThenBy(state => state.Plan.ReportControlReference, StringComparer.OrdinalIgnoreCase)
.Select(state => new HybridReportPhysicalValidationPlan
{
PlanId = state.Plan.PlanId,
AcquisitionKind = state.Plan.EngineAcquisitionKind,
ReportControlReference = state.Plan.ReportControlReference,
DataSetReference = state.Plan.DataSetReference,
PlannedSignalCount = state.Plan.Bindings.Count,
ActivationSucceeded = state.ActivationSucceeded,
ActivationMessage = state.ActivationMessage,
SubscriptionSummary = state.SubscriptionSummary,
MemberCount = state.MemberCount,
SetupWriteStepCount = state.SetupWriteStepCount,
UsedDynamicDataSet = state.UsedDynamicDataSet,
ReportFrameCount = state.ReportFrameCount,
ReportUpdateCount = state.ReportUpdateCount,
ChangeVerifiedPointCount = state.ChangeVerifiedPointKeys.Count,
FirstReportAtUtc = state.FirstReportAtUtc,
LastReportAtUtc = state.LastReportAtUtc
})
.ToArray();

return new HybridReportPhysicalValidationSnapshot
{
CapturedAtUtc = DateTimeOffset.UtcNow,
DeviceId = device.DeviceId,
DeviceName = device.Name,
Endpoint = device.EndpointText,
PlannedStaticBrcbCount = planning?.StaticBrcbSignalCount ?? 0,
PlannedStaticUrcbCount = planning?.StaticUrcbSignalCount ?? 0,
PlannedDynamicBrcbCount = planning?.DynamicBrcbSignalCount ?? 0,
PlannedDynamicUrcbCount = planning?.DynamicUrcbSignalCount ?? 0,
ActivatedReportPlanCount = _plans.Values.Count(state => state.ActivationAttempted && state.ActivationSucceeded),
FailedActivationCount = _plans.Values.Count(state => state.ActivationAttempted && !state.ActivationSucceeded),
ReportFrameCount = plans.Sum(plan => plan.ReportFrameCount),
ReportUpdateCount = plans.Sum(plan => plan.ReportUpdateCount),
ChangeVerifiedPointCount = plans.Sum(plan => plan.ChangeVerifiedPointCount),
PollingFallbackPointCount = planning?.PollingFallbackSignalCount ?? 0,
UncoveredPointCount = planning?.UncoveredSignalCount ?? 0,
Plans = plans,
Warnings = _warnings.ToArray()
};
}

private void AddWarning(string? warning)
{
var text = (warning ?? string.Empty).Trim();
if (text.Length == 0 || _warnings.Contains(text, StringComparer.OrdinalIgnoreCase))
return;
_warnings.Add(text);
}
}
Loading
Loading