Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 17 additions & 1 deletion .github/workflows/interoperability-reference-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,27 @@ jobs:
[bool]$serverAt.physicalQualificationRequired -and
[bool]$serverAt.noAutomaticMmsFailover -and
$lock.ref -eq 'main'
$closure = $lock.dataSetTypeClosureTrial
$dataSetTrial = $null -ne $closure -and
$lock.commit -eq '43f8935eb636967359028a0d9df293aac1812d99' -and
$lock.commit -eq $closure.exactCommit -and
$closure.baseEngineCommit -eq $serverAt.exactCommit -and
[int]$lock.sourcePullRequest -eq 155 -and
[int]$closure.sourcePullRequest -eq 155 -and
$closure.status -eq 'code-verified-candidate-not-physical' -and
[bool]$closure.physicalQualificationRequired -and
[bool]$closure.fullNameRescanForbidden -and
[bool]$closure.cyclicMmsProcessPollingForbidden -and
[bool]$closure.dynamicDataSetWritesForbidden -and
[int]$closure.maxNameListPages -eq 64 -and
[int]$closure.maxExtraLogicalNodes -eq 48 -and
[int]$closure.maxMemberHints -eq 2048 -and
$lock.ref -eq 'main'
if ($null -ne $trial) {
if ($trial.status -ne 'code-verified-not-physical' -or
[int]$trial.issue -ne 144 -or
[int]$trial.integrationPullRequest -ne 148 -or
-not ($originalTrial -or $serverAtTrial) -or
-not ($originalTrial -or $serverAtTrial -or $dataSetTrial) -or
-not [bool]$trial.fullDiscoveryFallbackForbidden -or
-not [bool]$trial.vendorBranchingForbidden -or
-not [bool]$trial.physicalPromotionRequired) {
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/scl-interoperability-r7.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ jobs:
}
Write-Host 'Running exact P6.2 ServerAt trial on the P6.1 code-only ancestry; physical/main promotion remains blocked.'
}
elseif ($null -ne $lock.dataSetTypeClosureTrial -and
$lock.commit -eq $lock.dataSetTypeClosureTrial.exactCommit) {
$closure = $lock.dataSetTypeClosureTrial
if ($lock.commit -ne '43f8935eb636967359028a0d9df293aac1812d99' -or
$lock.ref -ne 'main' -or
[int]$lock.sourcePullRequest -ne 155 -or
[int]$closure.sourcePullRequest -ne 155 -or
$closure.baseEngineCommit -ne $lock.sclServerAtTrial.exactCommit -or
$closure.baseEngineCommit -ne '352c81e6a798635c6addcee0683235ca87ad416d' -or
$closure.status -ne 'code-verified-candidate-not-physical' -or
-not [bool]$closure.physicalQualificationRequired -or
-not [bool]$closure.fullNameRescanForbidden -or
-not [bool]$closure.cyclicMmsProcessPollingForbidden -or
-not [bool]$closure.dynamicDataSetWritesForbidden -or
[int]$closure.maxNameListPages -ne 64 -or
[int]$closure.maxExtraLogicalNodes -ne 48 -or
[int]$closure.maxMemberHints -ne 2048 -or
$lock.purpose -notmatch 'CODE-VERIFIED INTEROPERABILITY TRIAL ONLY') {
throw 'P7.7 DataSet-first code-only trial provenance is invalid or promoted.'
}
Write-Host 'Running bounded DataSet-first P7.7 engine trial. Physical promotion remains blocked.'
}
elseif ($lock.commit -eq $convergence.activeStack.engineModelAndScl.head) {
if ([int]$lock.sourcePullRequest -notin @(134, 135) -or
$lock.purpose -notmatch 'R7 physical SCL repair pin') {
Expand Down
23 changes: 22 additions & 1 deletion .github/workflows/smart-discovery-capture-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,27 @@ jobs:
- name: Verify P0-5c smart capture sources
shell: powershell
run: |
$lock = Get-Content .\ArIED61850Tester\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json
$closure = $lock.dataSetTypeClosureTrial
$dataSetTrial = $null -ne $closure -and
$lock.commit -eq '43f8935eb636967359028a0d9df293aac1812d99' -and
$lock.commit -eq $closure.exactCommit -and
[int]$lock.sourcePullRequest -eq 155 -and
[int]$closure.sourcePullRequest -eq 155 -and
$closure.baseEngineCommit -eq '352c81e6a798635c6addcee0683235ca87ad416d' -and
$closure.status -eq 'code-verified-candidate-not-physical' -and
[bool]$closure.physicalQualificationRequired -and
[bool]$closure.fullNameRescanForbidden -and
[bool]$closure.cyclicMmsProcessPollingForbidden -and
[bool]$closure.dynamicDataSetWritesForbidden -and
[int]$closure.maxNameListPages -eq 64 -and
[int]$closure.maxExtraLogicalNodes -eq 48 -and
[int]$closure.maxMemberHints -eq 2048
$expectedSmartProbe = if ($dataSetTrial) {
'ProbeSmartAsync\(_session, discovery, smartOptions, CancellationToken\.None\)'
} else {
'ProbeSmartAsync\(_session, discovery\.IedDirectory, smartOptions, CancellationToken\.None\)'
}
$helper = Get-Content .\ArIED61850Tester\Services\NativeIec61850Client.SmartDiscoveryCapture.cs -Raw
$optimization = Get-Content .\ArIED61850Tester\Services\NativeIec61850Client.SmartDiscoveryOptimization.cs -Raw
$lifecycle = Get-Content .\ArIED61850Tester\Services\NativeIec61850Client.SmartDiscoveryLifecycle.cs -Raw
Expand All @@ -98,7 +119,7 @@ jobs:
$helper -notmatch 'GetOrCreateSmartDiscoveryAssociationFlight' -or
$helper -notmatch 'flight\.WaitAsync\(cancellationToken\)' -or
$helper -notmatch '_mmsIoGate\.WaitAsync\(CancellationToken\.None\)' -or
$helper -notmatch 'ProbeSmartAsync\(_session, discovery\.IedDirectory, smartOptions, CancellationToken\.None\)' -or
$helper -notmatch $expectedSmartProbe -or
$optimization -notmatch 'TryGetSmartDiscoveryAuthority' -or
$optimization -notmatch 'BuildSmartCaptureSignalProjection' -or
$optimization -notmatch 'AddSmartIndexedLogicalNodeFallbacks' -or
Expand Down
19 changes: 18 additions & 1 deletion .github/workflows/smart-discovery-mainline-readiness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,24 @@ jobs:
$serverAt.status -eq 'code-verified-candidate-not-physical' -and
[bool]$serverAt.physicalQualificationRequired -and
[bool]$serverAt.noAutomaticMmsFailover
if ($null -ne $trial -and ($originalTrial -or $serverAtTrial)) {
# Exact code-only engine trial, never a physical or mainline authority.
$closure = $lock.dataSetTypeClosureTrial
$dataSetTrial = $null -ne $closure -and
$lock.commit -eq '43f8935eb636967359028a0d9df293aac1812d99' -and
$lock.commit -eq $closure.exactCommit -and
$closure.baseEngineCommit -eq $serverAt.exactCommit -and
[int]$lock.sourcePullRequest -eq 155 -and
[int]$closure.sourcePullRequest -eq 155 -and
$closure.status -eq 'code-verified-candidate-not-physical' -and
[bool]$closure.physicalQualificationRequired -and
[bool]$closure.fullNameRescanForbidden -and
[bool]$closure.cyclicMmsProcessPollingForbidden -and
[bool]$closure.dynamicDataSetWritesForbidden -and
[int]$closure.maxNameListPages -eq 64 -and
[int]$closure.maxExtraLogicalNodes -eq 48 -and
[int]$closure.maxMemberHints -eq 2048 -and
$lock.purpose -match 'CODE-VERIFIED INTEROPERABILITY TRIAL ONLY'
if ($null -ne $trial -and ($originalTrial -or $serverAtTrial -or $dataSetTrial)) {
if ($trial.status -ne 'code-verified-not-physical' -or
[int]$trial.issue -ne 144 -or
[int]$trial.integrationPullRequest -ne 148 -or
Expand Down
11 changes: 8 additions & 3 deletions Services/NativeIec61850Client.SmartDiscoveryCapture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ private static ArMms.MmsSmartDiscoveryOptions CreateP0FrozenSmartDiscoveryOption
ProbeReportAttributes = true,
MaxReportAttributeProbes = 64,
ReadDataSetDirectories = true,
MaxDataSetDirectoryReads = 64
MaxDataSetDirectoryReads = 64,
MaxDataSetTypeExtraLogicalNodes = 48,
MaxDataSetTypeMemberHints = 2048
};

private async Task<IReadOnlyList<SignalDefinition>> DiscoverSignalsSmartForCaptureAsync(
Expand Down Expand Up @@ -187,12 +189,12 @@ private async Task<IReadOnlyList<SignalDefinition>> DiscoverSignalsSmartForCaptu

progress?.Report(new IedDiscoveryProgress(
IedDiscoveryStage.ProbingLogicalNodes,
"Smart MMS type discovery: coverage-aware Logical Node hierarchy probes…",
"Reading details for signals used by the IED reports…",
52d, 5, 10));

var typeWatch = Stopwatch.StartNew();
var variableTypes = await LiveIedVariableTypeProbeExecutor
.ProbeSmartAsync(_session, discovery.IedDirectory, smartOptions, CancellationToken.None)
.ProbeSmartAsync(_session, discovery, smartOptions, CancellationToken.None)
.ConfigureAwait(false);
typeWatch.Stop();

Expand Down Expand Up @@ -270,6 +272,8 @@ private async Task<IReadOnlyList<SignalDefinition>> DiscoverSignalsSmartForCaptu
.Count();
var rawVariables = snapshot.DomainVariables.Values.Sum(values => values.Count);
var successfulTypeRoots = variableTypes.Count(result => result.IsSuccess);
var dataSetTypeHints = discovery.IedDirectory.Points.Count(point =>
string.Equals(point.Source, "LiveMmsDataSetDirectoryTypeHint", StringComparison.Ordinal));
var typeBudget = _session.LastSmartTypeProbeBudget?.Summary ?? "Smart type budget unavailable.";

totalWatch.Stop();
Expand All @@ -278,6 +282,7 @@ private async Task<IReadOnlyList<SignalDefinition>> DiscoverSignalsSmartForCaptu
$"IEDName={(string.IsNullOrWhiteSpace(identity.IedName) ? "unresolved" : identity.IedName)} ({identity.Source}); " +
$"{discovery.Summary} {liveModel.Summary} LN={logicalNodes}, SCADA candidates={signals.Count}, " +
$"MMS names={rawVariables}, smart type probes={variableTypes.Count}, successful type probes={successfulTypeRoots}, " +
$"DataSet-backed typed member hints={dataSetTypeHints}, " +
$"indexed LN hints={projectionStats.LogicalNodeHints}, indexed fallback signals={projectionStats.AddedFallbackSignals}. " +
$"{typeBudget} " +
$"TimingMs directory={directoryWatch.Elapsed.TotalMilliseconds:F1}, types={typeWatch.Elapsed.TotalMilliseconds:F1}, " +
Expand Down
29 changes: 29 additions & 0 deletions docs/P77_DATASET_FIRST_DISCOVERY_FIELD_ACCEPTANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# P7.7 · DataSet-first discovery parity trial

**Status:** engine code/CI verified; ARSAS field acceptance pending. This change is not a complete full-model SCL parity claim.

## What changes for the operator

Connection and Discovery remain one-click. ARSAS first finds DataSets and report controls, then automatically reads the type descriptions needed for those exact reporting signals. The operator is not asked to choose MMS request budgets or manually browse 20,000 signal names.

If the IED contains signal families beyond the fast namespace slice, report-owned `CSWI`, `eveGGIO`, `MMXU` and other verified LN roots are still eligible for schema probes. The displayed phase measurements and control identities stay unchanged. Technical detail remains available in Diagnostics, not as an interruption to the normal workflow.

## Protocol guardrails

- ARIEC61850 is the sole authority for schema interpretation and source evidence.
- Exactly one association-scoped bounded GVA LN→DO→leaf ladder; no second bulk GetNameList.
- Only successful live DataSet directory memberships on observed MMS domains are admitted.
- Default additional LN budget 48; member hints 2048, deduplicated by exact MMS reference.
- The 64-page initial namespace cap and negotiated concurrent-request window remain unchanged.
- No cyclic MMS process polling, no dynamic DataSet writes, no automatic breaker operation.
- Original positional member identity, per-DataSet RCB and report causality remain intact.

## Field qualification (must use same IED state)

1. Open SCL and IP Discovery against the same endpoint; both use Static DataSet mode and 6/6 configured RCB traffic.
2. Compare **exact leaf references** as well as totals: `eveGGIO1.Ind*.stVal` (30), `CSWI1..16.Pos.stVal` (16), 13 `MMXU1.A/PhV/PPV` magnitude leaves and scalar `AuxV`. All present only if actually schema-proven; no fabricated values.
3. Confirm quality, actual values and timestamps, not just row presence; `BufOvfl` / SqNum findings are a distinct continuity gate.
4. Compare number and total bytes of MMS requests and elapsed time using a fresh separate connection. Avoid comparing loopback timings as a physical Ethernet benchmark.
5. Optional comprehensive engineering scan is a separate future capability; this change specifically targets **report-member semantic completeness** while preserving fast discovery.

The first F650 comparison had Open SCL 67 values, IP Discovery 24, with 124/124 DataSet memberships and six RCBs routed. Fixing the omitted DataSet-backed typed LN roots is a hypothesis backed by the user's PCAP audit; it still requires the follow-up physical capture to prove acceptance.
19 changes: 16 additions & 3 deletions engines/ARIEC61850.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"schemaVersion": 1,
"repository": "masarray/ARIEC61850",
"ref": "main",
"commit": "352c81e6a798635c6addcee0683235ca87ad416d",
"sourcePullRequest": 153,
"purpose": "CODE-VERIFIED INTEROPERABILITY TRIAL ONLY; P6.2 ServerAt authority extends P0-P6.1 at immutable commit 352c81e6a798635c6addcee0683235ca87ad416d. Neither code verification nor the SCD readout implies physical validation or production promotion. All historical physical and P6.1 baselines remain unchanged; static RCB/report semantics and no cyclic polling invariant are untouched.",
"commit": "43f8935eb636967359028a0d9df293aac1812d99",
"sourcePullRequest": 155,
"purpose": "P7.7 CODE-VERIFIED INTEROPERABILITY TRIAL ONLY: DataSet-first bounded typed LN closure, exactly one smart GVA pass over names plus successful live DataSet directory evidence. Extends immutable P6.2 pin 352c81e6a798635c6addcee0683235ca87ad416d with one engine commit. No full GetNameList rescan, no cyclic process polling or dynamic DataSet mutation. Physical F650 parity and buffered continuity remain acceptance gates; prior golden-wire and field-proven baselines unchanged.",
"previousTrialPin": {
"commit": "4467124775d8d9d76f3db194f9fbfd97144767a8",
"sourcePullRequest": 134,
Expand Down Expand Up @@ -72,5 +72,18 @@
"fixture": "sanitized J Server / F ServerAt multi-AP",
"noAutomaticMmsFailover": true,
"physicalQualificationRequired": true
},
"dataSetTypeClosureTrial": {
"exactCommit": "43f8935eb636967359028a0d9df293aac1812d99",
"baseEngineCommit": "352c81e6a798635c6addcee0683235ca87ad416d",
"sourcePullRequest": 155,
"status": "code-verified-candidate-not-physical",
"physicalQualificationRequired": true,
"fullNameRescanForbidden": true,
"cyclicMmsProcessPollingForbidden": true,
"dynamicDataSetWritesForbidden": true,
"maxNameListPages": 64,
"maxExtraLogicalNodes": 48,
"maxMemberHints": 2048
}
}
19 changes: 13 additions & 6 deletions tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void SmartDiscovery_DefersEagerFcValueReadsFromStructuralScan()
Assert.Contains("MaxDataSetDirectoryReads = 64", capture, StringComparison.Ordinal);
Assert.Contains("GetOrCreateSmartDiscoveryAssociationFlight(", capture, StringComparison.Ordinal);
Assert.Contains("DiscoverSmartSingleFlightAsync(smartOptions, CancellationToken.None)", capture, StringComparison.Ordinal);
Assert.Contains("ProbeSmartAsync(_session, discovery.IedDirectory, smartOptions, CancellationToken.None)", capture, StringComparison.Ordinal);
Assert.Contains("ProbeSmartAsync(_session, discovery, smartOptions, CancellationToken.None)", capture, StringComparison.Ordinal);
Assert.DoesNotContain("_session.DiscoverAsync(", capture, StringComparison.Ordinal);
Assert.DoesNotContain("DiscoverDomainVariableNamesAsync", capture, StringComparison.Ordinal);
Assert.DoesNotContain("TryBuildSupplementalGetNameListSnapshotAsync", capture, StringComparison.Ordinal);
Expand Down Expand Up @@ -407,12 +407,12 @@ public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCodeVerifiedInter
using var document = System.Text.Json.JsonDocument.Parse(lockFile);
var root = document.RootElement;

// The current source is the explicit P6.2 code-only trial. P6.1 remains
// immutable ancestry, not a field-proven replacement or a new main pin.
// P7.7 extends the immutable P6.2 engine head as a code-only
// DataSet semantic trial, not a new physical/production authority.
Assert.Equal(
"352c81e6a798635c6addcee0683235ca87ad416d",
"43f8935eb636967359028a0d9df293aac1812d99",
root.GetProperty("commit").GetString());
Assert.Equal(153, root.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(155, root.GetProperty("sourcePullRequest").GetInt32());

Assert.Equal(
"648124097621046f5f127ceb1cf853fea54db730",
Expand Down Expand Up @@ -448,8 +448,15 @@ public void EngineLock_PreservesPhysicalSclRepairBaselineAcrossCodeVerifiedInter
Assert.Equal(153, serverAt.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal(trial.GetProperty("exactCommit").GetString(),
serverAt.GetProperty("baseEngineCommit").GetString());
Assert.Equal(root.GetProperty("commit").GetString(),
Assert.Equal("352c81e6a798635c6addcee0683235ca87ad416d",
serverAt.GetProperty("exactCommit").GetString());
var datasetTrial = root.GetProperty("dataSetTypeClosureTrial");
Assert.Equal(root.GetProperty("commit").GetString(), datasetTrial.GetProperty("exactCommit").GetString());
Assert.Equal(serverAt.GetProperty("exactCommit").GetString(), datasetTrial.GetProperty("baseEngineCommit").GetString());
Assert.Equal(155, datasetTrial.GetProperty("sourcePullRequest").GetInt32());
Assert.Equal("code-verified-candidate-not-physical", datasetTrial.GetProperty("status").GetString());
Assert.True(datasetTrial.GetProperty("physicalQualificationRequired").GetBoolean());
Assert.True(datasetTrial.GetProperty("cyclicMmsProcessPollingForbidden").GetBoolean());
Assert.True(serverAt.GetProperty("physicalQualificationRequired").GetBoolean());
Assert.True(serverAt.GetProperty("noAutomaticMmsFailover").GetBoolean());
Assert.Equal(759, trial.GetProperty("ciRun").GetInt32());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void P05c_CompleteEnrichmentChain_IsAssociationScopedSingleFlight()
Assert.Contains("flight.WaitAsync(cancellationToken)", capture, StringComparison.Ordinal);
Assert.Contains("_mmsIoGate.WaitAsync(CancellationToken.None)", capture, StringComparison.Ordinal);
Assert.Contains("DiscoverSmartSingleFlightAsync(smartOptions, CancellationToken.None)", capture, StringComparison.Ordinal);
Assert.Contains("ProbeSmartAsync(_session, discovery.IedDirectory, smartOptions, CancellationToken.None)", capture, StringComparison.Ordinal);
Assert.Contains("ProbeSmartAsync(_session, discovery, smartOptions, CancellationToken.None)", capture, StringComparison.Ordinal);
Assert.DoesNotContain("ProbeSmartAsync(_session, discovery.IedDirectory, smartOptions, cancellationToken)", capture, StringComparison.Ordinal);

Assert.Contains("_smartDiscoveryAssociationFlight", lifecycle, StringComparison.Ordinal);
Expand Down
Loading