Skip to content
Open
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
6 changes: 3 additions & 3 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1700,12 +1700,12 @@ private void SaveTypedModelAsScl(
canonical,
result);

if (semanticPatch.Changed)
if (semanticPatch.Messages.Count > 0)
{
AddLog(
"INFO",
semanticPatch.Changed ? "INFO" : "WARN",
"SCL Export",
$"{device.Name}: export-only semantic parity applied after canonical serialization and before reload validation • " +
$"{device.Name}: export-only semantic parity {(semanticPatch.Changed ? "applied" : "skipped")} after canonical serialization and before reload validation • " +
string.Join(" ", semanticPatch.Messages));
}
}
Expand Down
40 changes: 33 additions & 7 deletions Services/SclExportSemanticParityPatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,50 @@ public static SclExportSemanticParityPatchResult ApplyForLiveModel(
if (!File.Exists(sclPath))
throw new FileNotFoundException("Generated SCL file does not exist.", sclPath);

// This compatibility correction is deliberately keyed to the exact object
// proven by the physical comparison. It is not a generic name/value guess.
var targetExistsInLiveModel = liveModel.LogicalDevices
// The object name narrows the compatibility scope; it is not sufficient
// evidence for a CDC/type correction on another relay or firmware.
var targetDataObjects = liveModel.LogicalDevices
.SelectMany(device => device.LogicalNodes)
.Where(node => string.Equals(node.Name, TargetLogicalNodeName, StringComparison.Ordinal))
.SelectMany(node => node.DataObjects)
.Any(dataObject =>
.Where(dataObject =>
string.Equals(dataObject.Name, TargetDataObject, StringComparison.Ordinal) &&
dataObject.Reference.EndsWith(
"/" + TargetLogicalNodeName + "." + TargetDataObject,
StringComparison.Ordinal));
StringComparison.Ordinal))
.ToArray();

if (targetDataObjects.Length == 0)
{
return new SclExportSemanticParityPatchResult(
0, 0, Array.Empty<string>());
}

if (targetDataObjects.Length != 1)
{
throw new InvalidDataException(
$"Ambiguous live {TargetLogicalNodeName}.{TargetDataObject}: {targetDataObjects.Length} matching objects.");
}

var exactIntegerStatus = targetDataObjects[0].Attributes
.Where(attribute =>
string.Equals(attribute.AttributePath, "stVal", StringComparison.Ordinal) &&
string.Equals(attribute.FunctionalConstraint, "ST", StringComparison.Ordinal) &&
string.Equals(attribute.TypeDiscoveryStatus, "Exact", StringComparison.Ordinal) &&
attribute.TypeConfidence == LiveIedDiscoveryConfidenceLevel.Exact &&
string.Equals(attribute.SclBType, "INT32", StringComparison.OrdinalIgnoreCase))
Comment on lines +60 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject conflicting exact stVal declarations

When the attribute list contains one qualifying INT32 entry and another exact ST stVal entry declaring BOOLEAN or INT32U, this filter discards the contradictory entry, leaving exactIntegerStatus.Length == 1; the patch then rewrites the saved SCL to INS/INT32 despite ambiguous evidence. Collect all exact ST stVal candidates and require the sole candidate to be INT32 before applying the correction.

AGENTS.md reference: AGENTS.md:L60-L66

Useful? React with 👍 / 👎.

.ToArray();

if (!targetExistsInLiveModel)
if (exactIntegerStatus.Length != 1)
{
// Preserve a legitimately Boolean or unproven status object. Never invent
// INS/INT32 from the name, old inferred CDC, or an instance Val string.
return new SclExportSemanticParityPatchResult(
0,
0,
Array.Empty<string>());
[
$"{TargetLogicalNodeName}.{TargetDataObject}: export-only correction skipped; exact ST stVal INT32 TypeSpecification evidence is unavailable or ambiguous."
]);
}

var document = XDocument.Load(sclPath, LoadOptions.PreserveWhitespace);
Expand Down
58 changes: 56 additions & 2 deletions tests/ARSAS.Tests/SclExportSemanticParityPatchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,45 @@ public void PhysicalReferenceCounter_IsCorrectedOnlyInSavedScl_AndSharedTypeIsNo
}
}

[Theory]
[InlineData("BOOLEAN", "Exact", LiveIedDiscoveryConfidenceLevel.Exact)]
[InlineData("INT32", "NotRead", LiveIedDiscoveryConfidenceLevel.Low)]
[InlineData("INT32", "Exact", LiveIedDiscoveryConfidenceLevel.Low)]
[InlineData("INT32U", "Exact", LiveIedDiscoveryConfidenceLevel.Exact)]
public void MissingOrContradictoryLiveType_DoesNotInventInsInSavedScl(
string sclBType,
string discoveryStatus,
LiveIedDiscoveryConfidenceLevel confidence)
{
var root = Path.Combine(
Path.GetTempPath(),
"arsas-scl-exact-type-gate-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var path = Path.Combine(root, "relay.iid");

try
{
const string xml = "<SCL xmlns=\"http://www.iec.ch/61850/2003/SCL\"><DataTypeTemplates /></SCL>";
File.WriteAllText(path, xml);
var model = PhysicalReferenceModel(sclBType, discoveryStatus, confidence);
var result = SclExportSemanticParityPatch.ApplyForLiveModel(model, path);

Assert.False(result.Changed);
Assert.Equal(0, result.PatchedDataObjects);
Assert.Contains(result.Messages, message =>
message.Contains("exact ST stVal INT32", StringComparison.Ordinal));
Assert.Equal(xml, File.ReadAllText(path));
Assert.Equal("SPS", Assert.Single(model.LogicalDevices
.SelectMany(device => device.LogicalNodes)
.SelectMany(node => node.DataObjects)).InferredCdc);
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}

[Fact]
public void NonTargetLiveModel_DoesNotRewriteSavedScl()
{
Expand Down Expand Up @@ -179,7 +218,10 @@ public void SaveHook_IsAfterCanonicalSerialization_AndBeforeReloadValidation()
StringComparison.Ordinal);
}

private static LiveIedModelDiscoveryDocument PhysicalReferenceModel()
private static LiveIedModelDiscoveryDocument PhysicalReferenceModel(
string sclBType = "INT32",
string discoveryStatus = "Exact",
LiveIedDiscoveryConfidenceLevel confidence = LiveIedDiscoveryConfidenceLevel.Exact)
=> new()
{
LogicalDevices =
Expand All @@ -202,7 +244,19 @@ private static LiveIedModelDiscoveryDocument PhysicalReferenceModel()
{
Reference = "AA1E1F06R4ADD/MPLS_GGIO1.CBClsCounter",
Name = "CBClsCounter",
InferredCdc = "SPS"
InferredCdc = "SPS",
Attributes =
[
new LiveIedDataAttributeModel
{
ObjectReference = "AA1E1F06R4ADD/MPLS_GGIO1.CBClsCounter.stVal",
AttributePath = "stVal",
FunctionalConstraint = "ST",
SclBType = sclBType,
TypeDiscoveryStatus = discoveryStatus,
TypeConfidence = confidence
}
]
}
]
}
Expand Down
Loading