From 1fd30e55a4bfee76dffaad008834dcbee0342d9d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:43:13 +0700 Subject: [PATCH 01/19] refactor(evidence): use neutral reference schema while preserving numerical field authority --- .../interoperability-reference-target.json | 500 ++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 evidence/interoperability-reference-target.json diff --git a/evidence/interoperability-reference-target.json b/evidence/interoperability-reference-target.json new file mode 100644 index 000000000..5c4f00426 --- /dev/null +++ b/evidence/interoperability-reference-target.json @@ -0,0 +1,500 @@ +{ + "schemaVersion": 1, + "name": "independent black-box reference tool discovery and SCL convergence", + "status": "physical-retest-passed-merge-ready", + "target": { + "discovery": "Reach reference-grade MMS discovery efficiency without sacrificing exact IEC 61850 model evidence.", + "model": "Build the canonical IEC 61850 model from structure-first MMS evidence with correct LN identity, DO/SDO hierarchy, FC ownership, DataSet/RCB semantics, and schema-aware CDC handling.", + "scl": "Save Edition 2 IID / Edition 1 ICD that reopens in ARSAS, reconnects to the relay, and is usable as trusted SCL." + }, + "activeStack": { + "enginePerformance": { + "repository": "masarray/ARIEC61850", + "pullRequest": 134, + "head": "a31e396f0bbc9507215a8c9d125e910555ed6f0a", + "status": "merged", + "testedHead": "a31e396f0bbc9507215a8c9d125e910555ed6f0a", + "mainMerge": "e6779ff74e5716af4fcfc3dc926dae0567b3cdb0" + }, + "engineModelAndScl": { + "repository": "masarray/ARIEC61850", + "pullRequest": 135, + "basePullRequest": 134, + "head": "648124097621046f5f127ceb1cf853fea54db730", + "status": "merged", + "testedHead": "9935d6902d786cc69b299260fe36b835944d5e81", + "mainMerge": "648124097621046f5f127ceb1cf853fea54db730" + }, + "consumerIntegration": { + "repository": "masarray/arsas", + "pullRequest": 324, + "branch": "test/smart-ied-discovery-pr134", + "status": "physical-proven-merge-ready" + } + }, + "physicalReference": { + "relay": "AA1E1F06R4", + "logicalDevices": 32, + "logicalNodes": 119, + "dataSets": 2, + "runtimeReportControls": 34, + "logicalSclReportControls": 32, + "externalReferenceCapture": { + "associations": 1, + "confirmedMmsRequests": 417, + "getVariableAccessAttributes": 119, + "reads": 156, + "note": "Reference capture supplied by the physical comparison; values are acceptance reference, not CI-simulated proof." + }, + "rejectedLegacyArsasCapture": { + "associations": 2, + "confirmedMmsRequests": 30552, + "getVariableAccessAttributes": 23724, + "reads": 6548, + "note": "Regression signature. A future field build showing this pattern is rejected." + }, + "externalSclReference": { + "lDevices": 32, + "logicalNodes": 119, + "dataSets": 2, + "fcda": 58, + "logicalReportControls": 32, + "expandedTopLevelDataObjects": 860, + "expandedDataObjectsIncludingSdo": 906, + "expandedScalarLeaves": 4925 + } + }, + "discoveryAcceptance": { + "exactlyOneAssociation": true, + "supplementalLegacyAssociationForbidden": true, + "recursivePerLeafGvaStormForbidden": true, + "secondFullGetNameListSweepForbidden": true, + "eagerInitialFcValueReadDuringStructuralDiscovery": false, + "typeStrategy": "LN/root structure first, bounded leaf fallback only when exact structure evidence is unavailable.", + "physicalPerformanceGate": "Compare request count, GVA count, Read count, peak outstanding calls, TTFI and total discovery time against independent black-box reference tool on the same relay." + }, + "modelAcceptance": { + "prefixedLogicalNodeIdentityExamples": [ + "RPRE_MMXU1 => prefix=RPRE_, lnClass=MMXU, lnInst=1", + "FPRE_MMXN1 => prefix=FPRE_, lnClass=MMXN, lnInst=1", + "I01ATCTR1 => prefix=I01A, lnClass=TCTR, lnInst=1" + ], + "standardSdoProjection": [ + "WYE", + "DEL", + "SEQ" + ], + "descendantFunctionalConstraintOwnership": true, + "standardRegistryAuthority": [ + "TCTR", + "TVTR", + "LTIM", + "EEName", + "MltLev" + ], + "edition2ServiceTrackingCdc": [ + "CST", + "BTS", + "UTS", + "STS", + "CTS" + ], + "edition1TrackingDowngrade": "omit with explicit warning; never emit invalid Edition 1 SCL", + "mmsTypeDeclarationOrder": "LN-root TypeSpecification declaration order is authoritative through model, SCL and FC-root projection.", + "settingFunctionalConstraints": "SG/SE are setting data; only actual SGCB produces SettingControl.", + "mhaiThdProjection": "MHAI ThdA/ThdPhV are WYE with CMV phase SDOs." + }, + "sclAcceptance": { + "profile": "full-model", + "reopenInArsasRequired": true, + "exactAssociationRebuildRequired": true, + "nativeCallingIdentityRequired": true, + "initialTrustedSclReads": "safe bounded SCL-guided reads after trusted-SCL reconnect; multi-DO CF groups use DO-scoped structured Reads because SCL LNodeType order is not authoritative for MMS FC-root DO order.", + "rcbProjection": "34 runtime RCB -> 32 logical ReportControl; ConfReportControl max=34", + "zeroSilentModelDeletion": true, + "physicalReconnectRequired": true, + "reuseTransportAndReportingLock": "Both Edition 2 and Edition 1 must match 32/32 MMS domains, complete all planned safe FC-root Reads without transport failures, preserve both static DataSets, arm exactly the configured Analog/Digital report plans, resolve all 58 runtime points, and observe actual InformationReport traffic.", + "semanticProjectionTarget": "projectionErrors=0 and projected leaf cache has no case-collapse loss." + }, + "promotion": { + "productionPromoted": false, + "mergeAllowedBeforePhysicalRetest": false, + "physicalRetestRequired": false, + "requiredEvidence": [ + "new ARSAS PCAP", + "new generated Ed2 IID", + "new generated Ed1 ICD", + "diagnostic report", + "Ed2 SCL-assisted reuse diagnostic with projectionErrorSamples", + "Ed1 SCL-assisted reuse diagnostic with projectionErrorSamples", + "same-relay comparison against independent black-box reference tool" + ], + "mergeAllowedAfterPhysicalRetest": true, + "physicalRetestPassed": true, + "physicalRetestDate": "2026-09-19", + "note": "R10 physical discovery/save/reopen evidence passed for the exact ARSAS/engine source tree. Merge is allowed; production promotion remains a separate release decision." + }, + "legacyPolicy": { + "rule": "Historical PRs may remain as provenance only; they must not be used as new branch bases after an active convergence authority supersedes them.", + "knownInvalidAuthority": [ + "PR #125 calling identity profile" + ], + "activeAuthorityOnly": [ + "ARIEC61850 #134", + "ARIEC61850 #135", + "ARSAS #324" + ] + }, + "physicalEvidence": { + "arsasR9": { + "relay": "AA1E1F06R4", + "testedArtifact": { + "appHead": "a89d6ef230951fde98f2b35e38dbc2dc84b6382f", + "engineHead": "3e12fb9d84ae27ddbebb2389ed2eb58d0fde6d90" + }, + "discovery": { + "associations": 1, + "confirmedMmsRequests": 323, + "getNameList": 138, + "getVariableAccessAttributes": 119, + "getNamedVariableListAttributes": 2, + "reads": 64, + "topLevelDataObjects": 860, + "dataObjectsIncludingSdo": 906, + "scalarLeaves": 4925, + "dataSets": 2, + "fcda": 58, + "logicalReportControls": 32, + "settingControls": 1 + }, + "reuseEdition2": { + "expectedDomains": 32, + "matchedDomains": 32, + "fcRoots": 563, + "successfulReads": 563, + "failedReads": 0, + "projectedLeaves": 4250, + "initialValueCache": 4239, + "projectionErrors": 46, + "staticDataSets": 2, + "staticReportControls": 32, + "reportBackedRuntimePoints": 58, + "unresolvedRuntimePoints": 0, + "actualInformationReportObserved": true + }, + "reuseEdition1": { + "expectedDomains": 32, + "matchedDomains": 32, + "fcRoots": 562, + "successfulReads": 562, + "failedReads": 0, + "projectedLeaves": 4055, + "initialValueCache": 4055, + "projectionErrors": 46, + "staticDataSets": 2, + "staticReportControls": 32, + "reportBackedRuntimePoints": 58, + "unresolvedRuntimePoints": 0, + "actualInformationReportObserved": true + } + }, + "arsasR10": { + "relay": "AA1E1F06R4", + "testedArtifact": { + "appHead": "eb8eb13d491f9aa265205852b8a4bab07af440ff", + "engineTestedHead": "9935d6902d786cc69b299260fe36b835944d5e81", + "engineMergedMain": "648124097621046f5f127ceb1cf853fea54db730", + "engineTree": "1cf7e08f333f24994625e8fe8416dbd0a16195b1" + }, + "discovery": { + "associations": 1, + "confirmedMmsRequests": 323, + "getNameList": 138, + "getVariableAccessAttributes": 119, + "getNamedVariableListAttributes": 2, + "reads": 64, + "topLevelDataObjects": 860, + "dataObjectsIncludingSdo": 906, + "scalarLeaves": 4925, + "dataSets": 2, + "fcda": 58, + "logicalReportControls": 32, + "settingControls": 1 + }, + "saveScl": { + "instanceEvidence": 3854, + "edition2ValCount": 3202, + "edition1ValCount": 3106, + "edition2RoundTrip": { + "logicalDevices": 32, + "logicalNodes": 119, + "dataSets": 2, + "logicalReportControls": 32 + }, + "edition1RoundTrip": { + "logicalDevices": 32, + "logicalNodes": 119, + "dataSets": 2, + "logicalReportControls": 32 + } + }, + "reuseEdition2": { + "expectedDomains": 32, + "observedDomains": 32, + "matchedDomains": 32, + "missingDomains": 0, + "extraDomains": 0, + "logicalDevices": 32, + "logicalNodes": 119, + "topLevelDataObjects": 860, + "dataAttributes": 5677, + "initialTargets": 709, + "fcRootTargets": 525, + "doScopedTargets": 184, + "successfulReads": 709, + "failedReads": 0, + "projectedLeaves": 4441, + "projectedUniqueValues": 4441, + "initialValueCache": 4441, + "cacheLoss": 0, + "projectionErrors": 0, + "staticDataSets": 2, + "staticMembers": 58, + "staticReportControls": 32, + "reportBackedRuntimePoints": 58, + "unresolvedRuntimePoints": 0, + "primaryUnresolvedAtSelection": 2, + "actualInformationReportObserved": true, + "cyclicMmsProcessPolling": 0 + }, + "reuseEdition1": { + "expectedDomains": 32, + "observedDomains": 32, + "matchedDomains": 32, + "missingDomains": 0, + "extraDomains": 0, + "logicalDevices": 32, + "logicalNodes": 119, + "topLevelDataObjects": 845, + "dataAttributes": 5470, + "initialTargets": 708, + "fcRootTargets": 524, + "doScopedTargets": 184, + "successfulReads": 708, + "failedReads": 0, + "projectedLeaves": 4246, + "projectedUniqueValues": 4246, + "initialValueCache": 4246, + "cacheLoss": 0, + "projectionErrors": 0, + "staticDataSets": 2, + "staticMembers": 58, + "staticReportControls": 32, + "reportBackedRuntimePoints": 58, + "unresolvedRuntimePoints": 0, + "primaryUnresolvedAtSelection": 2, + "actualInformationReportObserved": true, + "cyclicMmsProcessPolling": 0 + } + } + }, + "openGaps": { + "trustedSclProjectionParity": { + "targetProjectionErrors": 0, + "observedEdition2": 46, + "observedEdition1": 46, + "status": "resolved-r10-physical", + "rule": "Do not classify SCL reuse as semantically converged until projectionErrors reaches zero on both editions. Successful MMS Reads alone are insufficient.", + "rootCause": "R9 PCAP + Ed2 IID reproduction matched all 46 physical errors: every mismatch is in CF, across 18 FC roots / 191 affected SCL leaves, caused by cross-DO positional ordering differences between SCL LNodeType order and the MMS CF structure. No SCL count= arrays are present.", + "sourceRepair": "For multi-DO CF groups, InitialFcReadPlanner now emits exact LN$CF$DO targets. InitialFcValueProjector projects each DO value directly, eliminating cross-DO positional mapping while retaining batching.", + "r10ObservedEdition2": 0, + "r10ObservedEdition1": 0, + "note": "R9 historical 46/46 evidence is retained for provenance; R10 physically proves the repair on both editions." + }, + "edition2CaseDistinctInitialValueCache": { + "targetLoss": 0, + "observedProjectedMinusCached": 11, + "status": "resolved-r10-physical", + "rule": "LTRK t and T and any other case-distinct IEC 61850 paths must remain separate through TypeSpecification mapping, initial projection, ARSAS cache, canonical instance evidence, DAI/Val export and reopen. cacheLoss must be zero.", + "sourceRepair": "ARSAS trusted-SCL initial-value cache now uses StringComparer.Ordinal. Engine canonical value dedup, TypeSpecification member resolution, and canonical SCL instance-value targeting are exact-case. Diagnostics expose projectedUniqueValues and cacheLoss.", + "r10ObservedProjectedMinusCached": 0, + "r10Edition2ProjectedUniqueValues": 4441, + "r10Edition2InitialValueCache": 4441, + "note": "R9 historical loss=11 is retained for provenance; R10 physically proves exact-case cache preservation." + }, + "preallocatedAddReportControls": { + "observedWithoutDataSet": 30, + "status": "resolved-r10-export-and-reuse", + "rule": "Do not invent datSet. rptID-backed singleton runtime names ending 01 must project to indexed logical ReportControl max=1; an unassigned indexed slot is a warning, not a fatal missing-DataSet error.", + "r10FatalMissingDatasetErrors": 0, + "r10LogicalReportControls": 32, + "note": "Unassigned indexed ADD slots remain without invented datSet and no longer block reuse/reporting." + }, + "saveTimeInstanceValues": { + "observedR9ExportValCount": 0, + "referenceSclValCount": 1529, + "status": "mechanism-physically-proven-semantic-diff-open", + "rule": "Keep structural discovery read-free; acquire bounded safe FC-root values only during explicit Save SCL and verify resulting instance evidence physically.", + "r10InstanceEvidence": 3854, + "r10Edition2ValCount": 3202, + "r10Edition1ValCount": 3106, + "note": "Save-time value enrichment is physically proven. More Val elements than independent black-box reference tool is not treated as automatically better; exact semantic path/value comparison remains open." + }, + "templateReuse": { + "status": "next-improvement", + "rule": "Reduce duplicate LNodeType/DOType/DAType templates only after wire and semantic reuse parity is stable.", + "r10Edition2": { + "lNodeType": 119, + "doType": 906, + "daType": 752, + "fileBytes": 731266 + }, + "externalReference": { + "lNodeType": 38, + "doType": 60, + "daType": 17, + "fileBytesApprox": 247000 + }, + "acceptance": "Intern only structurally and semantically identical templates. Expanded 32/119/860/906/4925 model, FC ownership, values and round-trip behavior must remain unchanged." + }, + "saveEnrichmentReuse": { + "status": "next-optimization", + "observation": "Ed2 and Ed1 saves in one unchanged live session both produced instanceEvidence=3854; the bounded enrichment snapshot can be reused across edition serializers when association/model generation is unchanged.", + "rule": "Do not cache across reconnect, model-generation change, or explicit refresh. Optimization must not alter P0 discovery." + } + }, + "p0StructuralDiscoveryFreeze": { + "contractId": "P0-R9-STRUCTURAL", + "status": "implemented-and-r9-physically-proven", + "scope": "AA1E1F06R4 physical acceptance plus source/CI freeze. Counts are evidence and gates for this relay, not generic hardcoded runtime behavior for other IEDs.", + "sourcePolicy": { + "smartRouteRequired": true, + "associationScopedSingleFlightRequired": true, + "applicationMmsGateExclusive": true, + "logicalNodeRootTypeStrategyRequired": true, + "eagerInitialFcReadsForbidden": true, + "supplementalLegacyBrowseForbidden": true, + "secondFullGetNameListSweepForbidden": true, + "recursivePerLeafGvaStormForbidden": true, + "saveTimeAndTrustedSclValueReadsRemainSeparate": true + }, + "frozenOptions": { + "maxConcurrentChains": 8, + "unknownPeerMaxConcurrentChains": 4, + "maxDomains": 256, + "maxVariableNamesPerDomain": 20000, + "maxVariableListNamesPerDomain": 4096, + "maxNameListPages": 64, + "probeReportAttributes": true, + "maxReportAttributeProbes": 64, + "readDataSetDirectories": true, + "maxDataSetDirectoryReads": 64 + }, + "aa1e1f06r4Acceptance": { + "associations": 1, + "referenceConfirmedMmsRequests": 323, + "maximumConfirmedMmsRequests": 417, + "referenceGetNameList": 138, + "referenceGetVariableAccessAttributes": 119, + "maximumGetVariableAccessAttributes": 119, + "referenceGetNamedVariableListAttributes": 2, + "referenceReads": 64, + "maximumReads": 156, + "logicalDevices": 32, + "logicalNodes": 119, + "topLevelDataObjects": 860, + "dataObjectsIncludingSdo": 906, + "scalarLeaves": 4925, + "dataSets": 2, + "fcda": 58, + "logicalReportControls": 32, + "settingControls": 1 + }, + "rule": "Do not optimize or enrich structural discovery further while SCL semantic convergence is still open. Any required instance-value acquisition belongs to explicit Save SCL or trusted-SCL reconnect phases." + }, + "p1ProjectionOrderRepair": { + "contractId": "P1-CF-DO-SCOPED", + "status": "physically-proven-r10", + "preservesP0StructuralDiscovery": true, + "physicalR9Reproduction": { + "projectionErrors": 46, + "affectedFcRoots": 18, + "affectedSclLeaves": 191, + "functionalConstraint": "CF", + "sclArrayCountAttributes": 0 + }, + "rule": "Never use SCL LNodeType cross-DO order as authority for an MMS multi-DO CF structure. Read each CF DataObject by exact MMS object name and batch those references with the existing bounded Read executor.", + "expectedPhysicalOutcome": { + "projectionErrors": 0, + "noSilentCrossDoValueSwap": true, + "sameAssociation": true, + "noAdditionalDiscoveryGva": true + }, + "physicalR10Outcome": { + "edition2": { + "initialTargets": 709, + "doScopedTargets": 184, + "successfulReads": 709, + "failedReads": 0, + "projectionErrors": 0 + }, + "edition1": { + "initialTargets": 708, + "doScopedTargets": 184, + "successfulReads": 708, + "failedReads": 0, + "projectionErrors": 0 + }, + "reportBackedRuntimePoints": 58, + "unresolvedRuntimePoints": 0, + "actualInformationReportObserved": true + } + }, + "p2CaseSensitiveValuePipeline": { + "contractId": "P2-CASE-EXACT-VALUES", + "status": "physically-proven-r10", + "preservesP0StructuralDiscovery": true, + "preservesP1CfScopedHydration": true, + "physicalR9Evidence": { + "edition2ProjectedLeaves": 4250, + "edition2InitialValueCache": 4239, + "observedCacheLoss": 11, + "edition1ProjectedLeaves": 4055, + "edition1InitialValueCache": 4055, + "edition1ObservedCacheLoss": 0 + }, + "sourceInvariants": { + "trustedSclValueCacheComparer": "StringComparer.Ordinal", + "canonicalInstanceValueDedupComparer": "StringComparer.Ordinal", + "typeSpecificationMemberComparison": "StringComparison.Ordinal", + "sclDataObjectInstanceTargetComparison": "StringComparison.Ordinal", + "sclDaBdaSdoComponentComparison": "StringComparison.Ordinal", + "normalizationMayNotFoldCase": true + }, + "diagnosticContract": [ + "projectedUniqueValues", + "initialValueCache", + "cacheLoss" + ], + "expectedPhysicalOutcome": { + "edition2CacheLoss": 0, + "edition1CacheLoss": 0, + "ltrkLowercaseTAndUppercaseTRemainDistinct": true, + "noAdditionalDiscoveryGva": true, + "sameAssociation": true + }, + "physicalR10Outcome": { + "edition2": { + "projectedUniqueValues": 4441, + "initialValueCache": 4441, + "cacheLoss": 0 + }, + "edition1": { + "projectedUniqueValues": 4246, + "initialValueCache": 4246, + "cacheLoss": 0 + }, + "actualInformationReportObserved": true + } + } +} From 30da491c83984f2df1451800ddf14140958d9f2d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:43:16 +0700 Subject: [PATCH 02/19] refactor(evidence): retire vendor-named active reference path --- evidence/iedscout-convergence-target.json | 500 ---------------------- 1 file changed, 500 deletions(-) delete mode 100644 evidence/iedscout-convergence-target.json diff --git a/evidence/iedscout-convergence-target.json b/evidence/iedscout-convergence-target.json deleted file mode 100644 index a853fd0ea..000000000 --- a/evidence/iedscout-convergence-target.json +++ /dev/null @@ -1,500 +0,0 @@ -{ - "schemaVersion": 1, - "name": "IEDScout discovery and SCL convergence", - "status": "physical-retest-passed-merge-ready", - "target": { - "discovery": "Reach IEDScout-like MMS discovery efficiency without sacrificing exact IEC 61850 model evidence.", - "model": "Build the canonical IEC 61850 model from structure-first MMS evidence with correct LN identity, DO/SDO hierarchy, FC ownership, DataSet/RCB semantics, and schema-aware CDC handling.", - "scl": "Save Edition 2 IID / Edition 1 ICD that reopens in ARSAS, reconnects to the relay, and is usable as trusted SCL." - }, - "activeStack": { - "enginePerformance": { - "repository": "masarray/ARIEC61850", - "pullRequest": 134, - "head": "a31e396f0bbc9507215a8c9d125e910555ed6f0a", - "status": "merged", - "testedHead": "a31e396f0bbc9507215a8c9d125e910555ed6f0a", - "mainMerge": "e6779ff74e5716af4fcfc3dc926dae0567b3cdb0" - }, - "engineModelAndScl": { - "repository": "masarray/ARIEC61850", - "pullRequest": 135, - "basePullRequest": 134, - "head": "648124097621046f5f127ceb1cf853fea54db730", - "status": "merged", - "testedHead": "9935d6902d786cc69b299260fe36b835944d5e81", - "mainMerge": "648124097621046f5f127ceb1cf853fea54db730" - }, - "consumerIntegration": { - "repository": "masarray/arsas", - "pullRequest": 324, - "branch": "test/smart-ied-discovery-pr134", - "status": "physical-proven-merge-ready" - } - }, - "physicalReference": { - "relay": "AA1E1F06R4", - "logicalDevices": 32, - "logicalNodes": 119, - "dataSets": 2, - "runtimeReportControls": 34, - "logicalSclReportControls": 32, - "iedScoutCapture": { - "associations": 1, - "confirmedMmsRequests": 417, - "getVariableAccessAttributes": 119, - "reads": 156, - "note": "Reference capture supplied by the physical comparison; values are acceptance reference, not CI-simulated proof." - }, - "rejectedLegacyArsasCapture": { - "associations": 2, - "confirmedMmsRequests": 30552, - "getVariableAccessAttributes": 23724, - "reads": 6548, - "note": "Regression signature. A future field build showing this pattern is rejected." - }, - "iedScoutSclReference": { - "lDevices": 32, - "logicalNodes": 119, - "dataSets": 2, - "fcda": 58, - "logicalReportControls": 32, - "expandedTopLevelDataObjects": 860, - "expandedDataObjectsIncludingSdo": 906, - "expandedScalarLeaves": 4925 - } - }, - "discoveryAcceptance": { - "exactlyOneAssociation": true, - "supplementalLegacyAssociationForbidden": true, - "recursivePerLeafGvaStormForbidden": true, - "secondFullGetNameListSweepForbidden": true, - "eagerInitialFcValueReadDuringStructuralDiscovery": false, - "typeStrategy": "LN/root structure first, bounded leaf fallback only when exact structure evidence is unavailable.", - "physicalPerformanceGate": "Compare request count, GVA count, Read count, peak outstanding calls, TTFI and total discovery time against IEDScout on the same relay." - }, - "modelAcceptance": { - "prefixedLogicalNodeIdentityExamples": [ - "RPRE_MMXU1 => prefix=RPRE_, lnClass=MMXU, lnInst=1", - "FPRE_MMXN1 => prefix=FPRE_, lnClass=MMXN, lnInst=1", - "I01ATCTR1 => prefix=I01A, lnClass=TCTR, lnInst=1" - ], - "standardSdoProjection": [ - "WYE", - "DEL", - "SEQ" - ], - "descendantFunctionalConstraintOwnership": true, - "standardRegistryAuthority": [ - "TCTR", - "TVTR", - "LTIM", - "EEName", - "MltLev" - ], - "edition2ServiceTrackingCdc": [ - "CST", - "BTS", - "UTS", - "STS", - "CTS" - ], - "edition1TrackingDowngrade": "omit with explicit warning; never emit invalid Edition 1 SCL", - "mmsTypeDeclarationOrder": "LN-root TypeSpecification declaration order is authoritative through model, SCL and FC-root projection.", - "settingFunctionalConstraints": "SG/SE are setting data; only actual SGCB produces SettingControl.", - "mhaiThdProjection": "MHAI ThdA/ThdPhV are WYE with CMV phase SDOs." - }, - "sclAcceptance": { - "profile": "full-model", - "reopenInArsasRequired": true, - "exactAssociationRebuildRequired": true, - "nativeCallingIdentityRequired": true, - "initialTrustedSclReads": "safe bounded SCL-guided reads after trusted-SCL reconnect; multi-DO CF groups use DO-scoped structured Reads because SCL LNodeType order is not authoritative for MMS FC-root DO order.", - "rcbProjection": "34 runtime RCB -> 32 logical ReportControl; ConfReportControl max=34", - "zeroSilentModelDeletion": true, - "physicalReconnectRequired": true, - "reuseTransportAndReportingLock": "Both Edition 2 and Edition 1 must match 32/32 MMS domains, complete all planned safe FC-root Reads without transport failures, preserve both static DataSets, arm exactly the configured Analog/Digital report plans, resolve all 58 runtime points, and observe actual InformationReport traffic.", - "semanticProjectionTarget": "projectionErrors=0 and projected leaf cache has no case-collapse loss." - }, - "promotion": { - "productionPromoted": false, - "mergeAllowedBeforePhysicalRetest": false, - "physicalRetestRequired": false, - "requiredEvidence": [ - "new ARSAS PCAP", - "new generated Ed2 IID", - "new generated Ed1 ICD", - "diagnostic report", - "Ed2 SCL-assisted reuse diagnostic with projectionErrorSamples", - "Ed1 SCL-assisted reuse diagnostic with projectionErrorSamples", - "same-relay comparison against IEDScout" - ], - "mergeAllowedAfterPhysicalRetest": true, - "physicalRetestPassed": true, - "physicalRetestDate": "2026-09-19", - "note": "R10 physical discovery/save/reopen evidence passed for the exact ARSAS/engine source tree. Merge is allowed; production promotion remains a separate release decision." - }, - "legacyPolicy": { - "rule": "Historical PRs may remain as provenance only; they must not be used as new branch bases after an active convergence authority supersedes them.", - "knownInvalidAuthority": [ - "PR #125 calling identity profile" - ], - "activeAuthorityOnly": [ - "ARIEC61850 #134", - "ARIEC61850 #135", - "ARSAS #324" - ] - }, - "physicalEvidence": { - "arsasR9": { - "relay": "AA1E1F06R4", - "testedArtifact": { - "appHead": "a89d6ef230951fde98f2b35e38dbc2dc84b6382f", - "engineHead": "3e12fb9d84ae27ddbebb2389ed2eb58d0fde6d90" - }, - "discovery": { - "associations": 1, - "confirmedMmsRequests": 323, - "getNameList": 138, - "getVariableAccessAttributes": 119, - "getNamedVariableListAttributes": 2, - "reads": 64, - "topLevelDataObjects": 860, - "dataObjectsIncludingSdo": 906, - "scalarLeaves": 4925, - "dataSets": 2, - "fcda": 58, - "logicalReportControls": 32, - "settingControls": 1 - }, - "reuseEdition2": { - "expectedDomains": 32, - "matchedDomains": 32, - "fcRoots": 563, - "successfulReads": 563, - "failedReads": 0, - "projectedLeaves": 4250, - "initialValueCache": 4239, - "projectionErrors": 46, - "staticDataSets": 2, - "staticReportControls": 32, - "reportBackedRuntimePoints": 58, - "unresolvedRuntimePoints": 0, - "actualInformationReportObserved": true - }, - "reuseEdition1": { - "expectedDomains": 32, - "matchedDomains": 32, - "fcRoots": 562, - "successfulReads": 562, - "failedReads": 0, - "projectedLeaves": 4055, - "initialValueCache": 4055, - "projectionErrors": 46, - "staticDataSets": 2, - "staticReportControls": 32, - "reportBackedRuntimePoints": 58, - "unresolvedRuntimePoints": 0, - "actualInformationReportObserved": true - } - }, - "arsasR10": { - "relay": "AA1E1F06R4", - "testedArtifact": { - "appHead": "eb8eb13d491f9aa265205852b8a4bab07af440ff", - "engineTestedHead": "9935d6902d786cc69b299260fe36b835944d5e81", - "engineMergedMain": "648124097621046f5f127ceb1cf853fea54db730", - "engineTree": "1cf7e08f333f24994625e8fe8416dbd0a16195b1" - }, - "discovery": { - "associations": 1, - "confirmedMmsRequests": 323, - "getNameList": 138, - "getVariableAccessAttributes": 119, - "getNamedVariableListAttributes": 2, - "reads": 64, - "topLevelDataObjects": 860, - "dataObjectsIncludingSdo": 906, - "scalarLeaves": 4925, - "dataSets": 2, - "fcda": 58, - "logicalReportControls": 32, - "settingControls": 1 - }, - "saveScl": { - "instanceEvidence": 3854, - "edition2ValCount": 3202, - "edition1ValCount": 3106, - "edition2RoundTrip": { - "logicalDevices": 32, - "logicalNodes": 119, - "dataSets": 2, - "logicalReportControls": 32 - }, - "edition1RoundTrip": { - "logicalDevices": 32, - "logicalNodes": 119, - "dataSets": 2, - "logicalReportControls": 32 - } - }, - "reuseEdition2": { - "expectedDomains": 32, - "observedDomains": 32, - "matchedDomains": 32, - "missingDomains": 0, - "extraDomains": 0, - "logicalDevices": 32, - "logicalNodes": 119, - "topLevelDataObjects": 860, - "dataAttributes": 5677, - "initialTargets": 709, - "fcRootTargets": 525, - "doScopedTargets": 184, - "successfulReads": 709, - "failedReads": 0, - "projectedLeaves": 4441, - "projectedUniqueValues": 4441, - "initialValueCache": 4441, - "cacheLoss": 0, - "projectionErrors": 0, - "staticDataSets": 2, - "staticMembers": 58, - "staticReportControls": 32, - "reportBackedRuntimePoints": 58, - "unresolvedRuntimePoints": 0, - "primaryUnresolvedAtSelection": 2, - "actualInformationReportObserved": true, - "cyclicMmsProcessPolling": 0 - }, - "reuseEdition1": { - "expectedDomains": 32, - "observedDomains": 32, - "matchedDomains": 32, - "missingDomains": 0, - "extraDomains": 0, - "logicalDevices": 32, - "logicalNodes": 119, - "topLevelDataObjects": 845, - "dataAttributes": 5470, - "initialTargets": 708, - "fcRootTargets": 524, - "doScopedTargets": 184, - "successfulReads": 708, - "failedReads": 0, - "projectedLeaves": 4246, - "projectedUniqueValues": 4246, - "initialValueCache": 4246, - "cacheLoss": 0, - "projectionErrors": 0, - "staticDataSets": 2, - "staticMembers": 58, - "staticReportControls": 32, - "reportBackedRuntimePoints": 58, - "unresolvedRuntimePoints": 0, - "primaryUnresolvedAtSelection": 2, - "actualInformationReportObserved": true, - "cyclicMmsProcessPolling": 0 - } - } - }, - "openGaps": { - "trustedSclProjectionParity": { - "targetProjectionErrors": 0, - "observedEdition2": 46, - "observedEdition1": 46, - "status": "resolved-r10-physical", - "rule": "Do not classify SCL reuse as semantically converged until projectionErrors reaches zero on both editions. Successful MMS Reads alone are insufficient.", - "rootCause": "R9 PCAP + Ed2 IID reproduction matched all 46 physical errors: every mismatch is in CF, across 18 FC roots / 191 affected SCL leaves, caused by cross-DO positional ordering differences between SCL LNodeType order and the MMS CF structure. No SCL count= arrays are present.", - "sourceRepair": "For multi-DO CF groups, InitialFcReadPlanner now emits exact LN$CF$DO targets. InitialFcValueProjector projects each DO value directly, eliminating cross-DO positional mapping while retaining batching.", - "r10ObservedEdition2": 0, - "r10ObservedEdition1": 0, - "note": "R9 historical 46/46 evidence is retained for provenance; R10 physically proves the repair on both editions." - }, - "edition2CaseDistinctInitialValueCache": { - "targetLoss": 0, - "observedProjectedMinusCached": 11, - "status": "resolved-r10-physical", - "rule": "LTRK t and T and any other case-distinct IEC 61850 paths must remain separate through TypeSpecification mapping, initial projection, ARSAS cache, canonical instance evidence, DAI/Val export and reopen. cacheLoss must be zero.", - "sourceRepair": "ARSAS trusted-SCL initial-value cache now uses StringComparer.Ordinal. Engine canonical value dedup, TypeSpecification member resolution, and canonical SCL instance-value targeting are exact-case. Diagnostics expose projectedUniqueValues and cacheLoss.", - "r10ObservedProjectedMinusCached": 0, - "r10Edition2ProjectedUniqueValues": 4441, - "r10Edition2InitialValueCache": 4441, - "note": "R9 historical loss=11 is retained for provenance; R10 physically proves exact-case cache preservation." - }, - "preallocatedAddReportControls": { - "observedWithoutDataSet": 30, - "status": "resolved-r10-export-and-reuse", - "rule": "Do not invent datSet. rptID-backed singleton runtime names ending 01 must project to indexed logical ReportControl max=1; an unassigned indexed slot is a warning, not a fatal missing-DataSet error.", - "r10FatalMissingDatasetErrors": 0, - "r10LogicalReportControls": 32, - "note": "Unassigned indexed ADD slots remain without invented datSet and no longer block reuse/reporting." - }, - "saveTimeInstanceValues": { - "observedR9ExportValCount": 0, - "referenceIedScoutValCount": 1529, - "status": "mechanism-physically-proven-semantic-diff-open", - "rule": "Keep structural discovery read-free; acquire bounded safe FC-root values only during explicit Save SCL and verify resulting instance evidence physically.", - "r10InstanceEvidence": 3854, - "r10Edition2ValCount": 3202, - "r10Edition1ValCount": 3106, - "note": "Save-time value enrichment is physically proven. More Val elements than IEDScout is not treated as automatically better; exact semantic path/value comparison remains open." - }, - "templateReuse": { - "status": "next-improvement", - "rule": "Reduce duplicate LNodeType/DOType/DAType templates only after wire and semantic reuse parity is stable.", - "r10Edition2": { - "lNodeType": 119, - "doType": 906, - "daType": 752, - "fileBytes": 731266 - }, - "iedScoutReference": { - "lNodeType": 38, - "doType": 60, - "daType": 17, - "fileBytesApprox": 247000 - }, - "acceptance": "Intern only structurally and semantically identical templates. Expanded 32/119/860/906/4925 model, FC ownership, values and round-trip behavior must remain unchanged." - }, - "saveEnrichmentReuse": { - "status": "next-optimization", - "observation": "Ed2 and Ed1 saves in one unchanged live session both produced instanceEvidence=3854; the bounded enrichment snapshot can be reused across edition serializers when association/model generation is unchanged.", - "rule": "Do not cache across reconnect, model-generation change, or explicit refresh. Optimization must not alter P0 discovery." - } - }, - "p0StructuralDiscoveryFreeze": { - "contractId": "P0-R9-STRUCTURAL", - "status": "implemented-and-r9-physically-proven", - "scope": "AA1E1F06R4 physical acceptance plus source/CI freeze. Counts are evidence and gates for this relay, not generic hardcoded runtime behavior for other IEDs.", - "sourcePolicy": { - "smartRouteRequired": true, - "associationScopedSingleFlightRequired": true, - "applicationMmsGateExclusive": true, - "logicalNodeRootTypeStrategyRequired": true, - "eagerInitialFcReadsForbidden": true, - "supplementalLegacyBrowseForbidden": true, - "secondFullGetNameListSweepForbidden": true, - "recursivePerLeafGvaStormForbidden": true, - "saveTimeAndTrustedSclValueReadsRemainSeparate": true - }, - "frozenOptions": { - "maxConcurrentChains": 8, - "unknownPeerMaxConcurrentChains": 4, - "maxDomains": 256, - "maxVariableNamesPerDomain": 20000, - "maxVariableListNamesPerDomain": 4096, - "maxNameListPages": 64, - "probeReportAttributes": true, - "maxReportAttributeProbes": 64, - "readDataSetDirectories": true, - "maxDataSetDirectoryReads": 64 - }, - "aa1e1f06r4Acceptance": { - "associations": 1, - "referenceConfirmedMmsRequests": 323, - "maximumConfirmedMmsRequests": 417, - "referenceGetNameList": 138, - "referenceGetVariableAccessAttributes": 119, - "maximumGetVariableAccessAttributes": 119, - "referenceGetNamedVariableListAttributes": 2, - "referenceReads": 64, - "maximumReads": 156, - "logicalDevices": 32, - "logicalNodes": 119, - "topLevelDataObjects": 860, - "dataObjectsIncludingSdo": 906, - "scalarLeaves": 4925, - "dataSets": 2, - "fcda": 58, - "logicalReportControls": 32, - "settingControls": 1 - }, - "rule": "Do not optimize or enrich structural discovery further while SCL semantic convergence is still open. Any required instance-value acquisition belongs to explicit Save SCL or trusted-SCL reconnect phases." - }, - "p1ProjectionOrderRepair": { - "contractId": "P1-CF-DO-SCOPED", - "status": "physically-proven-r10", - "preservesP0StructuralDiscovery": true, - "physicalR9Reproduction": { - "projectionErrors": 46, - "affectedFcRoots": 18, - "affectedSclLeaves": 191, - "functionalConstraint": "CF", - "sclArrayCountAttributes": 0 - }, - "rule": "Never use SCL LNodeType cross-DO order as authority for an MMS multi-DO CF structure. Read each CF DataObject by exact MMS object name and batch those references with the existing bounded Read executor.", - "expectedPhysicalOutcome": { - "projectionErrors": 0, - "noSilentCrossDoValueSwap": true, - "sameAssociation": true, - "noAdditionalDiscoveryGva": true - }, - "physicalR10Outcome": { - "edition2": { - "initialTargets": 709, - "doScopedTargets": 184, - "successfulReads": 709, - "failedReads": 0, - "projectionErrors": 0 - }, - "edition1": { - "initialTargets": 708, - "doScopedTargets": 184, - "successfulReads": 708, - "failedReads": 0, - "projectionErrors": 0 - }, - "reportBackedRuntimePoints": 58, - "unresolvedRuntimePoints": 0, - "actualInformationReportObserved": true - } - }, - "p2CaseSensitiveValuePipeline": { - "contractId": "P2-CASE-EXACT-VALUES", - "status": "physically-proven-r10", - "preservesP0StructuralDiscovery": true, - "preservesP1CfScopedHydration": true, - "physicalR9Evidence": { - "edition2ProjectedLeaves": 4250, - "edition2InitialValueCache": 4239, - "observedCacheLoss": 11, - "edition1ProjectedLeaves": 4055, - "edition1InitialValueCache": 4055, - "edition1ObservedCacheLoss": 0 - }, - "sourceInvariants": { - "trustedSclValueCacheComparer": "StringComparer.Ordinal", - "canonicalInstanceValueDedupComparer": "StringComparer.Ordinal", - "typeSpecificationMemberComparison": "StringComparison.Ordinal", - "sclDataObjectInstanceTargetComparison": "StringComparison.Ordinal", - "sclDaBdaSdoComponentComparison": "StringComparison.Ordinal", - "normalizationMayNotFoldCase": true - }, - "diagnosticContract": [ - "projectedUniqueValues", - "initialValueCache", - "cacheLoss" - ], - "expectedPhysicalOutcome": { - "edition2CacheLoss": 0, - "edition1CacheLoss": 0, - "ltrkLowercaseTAndUppercaseTRemainDistinct": true, - "noAdditionalDiscoveryGva": true, - "sameAssociation": true - }, - "physicalR10Outcome": { - "edition2": { - "projectedUniqueValues": 4441, - "initialValueCache": 4441, - "cacheLoss": 0 - }, - "edition1": { - "projectedUniqueValues": 4246, - "initialValueCache": 4246, - "cacheLoss": 0 - }, - "actualInformationReportObserved": true - } - } -} From 8ab5dec9f90a584bb0198f7f44ed3cba94a39a79 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:43:38 +0700 Subject: [PATCH 03/19] docs(interoperability): neutralize active contract and preserve original comparison provenance --- docs/INTEROPERABILITY_REFERENCE_CONTRACT.md | 164 ++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/INTEROPERABILITY_REFERENCE_CONTRACT.md diff --git a/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md b/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md new file mode 100644 index 000000000..9d0b25d8f --- /dev/null +++ b/docs/INTEROPERABILITY_REFERENCE_CONTRACT.md @@ -0,0 +1,164 @@ +# IEC 61850 Interoperability Reference Contract + +## Product target + +ARSAS targets independently verified IEC 61850 IEC 61850 engineering semantics with lower wire cost where possible: + +1. one accepted MMS association and bounded structure-first discovery; +2. a complete canonical model with exact LN/DO/SDO/DA/FC identity; +3. Edition 2 IID / Edition 1 ICD that can be reopened by ARSAS, reconnect to the same relay, hydrate values without full discovery, and run configured static reporting. + +The machine-readable authority is `evidence/interoperability-reference-target.json`. + +## Original reference provenance + +The same-relay comparison was an authorized black-box measurement against a separately operated engineering tool. Its original name, source-specific labels and historical context remain verifiable in the immutable [pre-migration repository commit](https://github.com/masarray/arsas/commit/36a4b87a3c2f6d34f73fa36c8a8a6a59763bd435), original evidence blob `a853fd0ea115b320a648b1b2a52fe9a7f6af94cd`. This active contract uses neutral aliases **without changing which tool generated the historical reference**. The measurements are comparison inputs, not proof of source-code or interface lineage. + +## Merged proven baseline + +The physical R10 baseline was tested with: + +- ARSAS `eb8eb13d491f9aa265205852b8a4bab07af440ff`; +- ARIEC61850 tested head `9935d6902d786cc69b299260fe36b835944d5e81`; +- ARIEC61850 merged-main commit `648124097621046f5f127ceb1cf853fea54db730`. + +The tested engine head and merged-main commit have the identical tree SHA +`1cf7e08f333f24994625e8fe8416dbd0a16195b1`. + +Merged engine provenance: + +- PR #134 → main merge `e6779ff74e5716af4fcfc3dc926dae0567b3cdb0`: Smart Discovery performance authority. +- PR #135 → main merge `648124097621046f5f127ceb1cf853fea54db730`: canonical model, SCL interoperability, P1/P2 value pipeline. +- ARSAS PR #324: consumer integration and physical R10 proof. + +## P0 — structural discovery freeze + +Contract: `P0-R9-STRUCTURAL`. + +AA1E1F06R4 physical reference is locked at one association, 323 confirmed MMS +requests, 138 GetNameList, 119 GetVariableAccessAttributes, 2 +GetNamedVariableListAttributes and 64 Reads, while preserving 32 LD / 119 LN / +860 top-level DO / 906 DO+SDO / 4925 scalar leaves / 2 DataSets / 58 FCDA / 32 +logical ReportControls / 1 SettingControl. + +The same-relay external black-box reference tool comparison remains about 417 confirmed requests, 119 GVA +and 156 Reads. ARSAS must not add traffic merely to imitate external black-box reference tool. + +Forbidden regressions include a second discovery association, legacy supplemental +browse, a second full GetNameList sweep, recursive per-leaf GVA, and eager FC-root +value hydration on the discovery critical path. + +## P1 — CF projection-order repair: physically proven + +Contract: `P1-CF-DO-SCOPED`. + +R9 exposed 46 projection errors because SCL LNodeType DO order was incorrectly used +as MMS CF-root child order. P1 reads multi-DO CF data through exact `LN$CF$DO` +references and batches those structured reads. + +R10 physical reuse closes P1: + +- Ed2: 709 initial targets, 525 FC-root targets, 184 DO-scoped targets, + 709/709 successful, 0 failed, `projectionErrors=0`. +- Ed1: 708 initial targets, 524 FC-root targets, 184 DO-scoped targets, + 708/708 successful, 0 failed, `projectionErrors=0`. + +No extra discovery GVA or second association was introduced. + +## P2 — exact-case value pipeline: physically proven + +Contract: `P2-CASE-EXACT-VALUES`. + +R9 Ed2 lost 11 projected values because a consumer cache used case-insensitive +identity. P2 uses exact-case identity through TypeSpecification mapping, initial +projection, trusted-SCL caching, canonical evidence and SCL instance-value targeting. + +R10 physical reuse closes P2: + +- Ed2: `projectedUniqueValues=4441`, `initialValueCache=4441`, `cacheLoss=0`. +- Ed1: `projectedUniqueValues=4246`, `initialValueCache=4246`, `cacheLoss=0`. + +## R10 round-trip/reporting acceptance + +Both generated editions reopen as trusted SCL and reconnect without full discovery: + +- expected/observed/matched MMS domains: 32/32/32; +- DataSets: 2, members: 58, missing members: 0; +- configured report plans: Digital BRCB 36 members + Analog URCB 22 members; +- report-covered runtime points: 58; +- final unresolved runtime points: 0; +- cyclic MMS process polling: 0; +- actual InformationReport traffic observed on both editions. + +The early UI field `Primary unresolved=2` is not an operational loss: the exact +static DataSet schema resolves all 58 runtime points before reporting starts. + +## Save-time instance values + +R10 physically proves bounded Save SCL enrichment while fast discovery stays +unchanged: + +- canonical instance evidence: 3854 leaves; +- Ed2 exported `Val`: 3202; +- Ed1 exported `Val`: 3106. + +The external reference SCL file has about 1529 `Val` elements. A larger count is not +automatically better; the remaining task is semantic path/value comparison, not +count chasing. + +## RCB lock + +The accepted representation remains: + +- 34 runtime RCB objects → 32 logical SCL ReportControls; +- `Services/ConfReportControl max=34`; +- Buffer/Digital and Unbuffer/Analog remain the two configured report authorities; +- preallocated ADD slots without DataSet never receive an invented `datSet`. + +## Next improvements — do not disturb the proven wire/reuse path + +### 1. Template interning + +R10 Ed2 is semantically correct but verbose: + +- ARSAS: 119 LNodeType / 906 DOType / 752 DAType / about 731 KB; +- external black-box reference tool reference: about 38 / 60 / 17 / about 247 KB. + +Next work may intern only templates with identical ordered semantic fingerprints. +Expanded model counts, FC ownership, values, DataSets/RCBs and round-trip behavior +must remain unchanged. + +### 2. Reuse one save-enrichment snapshot across Ed2 and Ed1 + +When Ed2 and Ed1 are saved in the same unchanged live association/model generation, +both currently derive the same 3854 instance-evidence leaves. A later optimization +may reuse that bounded snapshot across serializers, but never across reconnect, +model-generation change or explicit refresh. + +### 3. Semantic Val diff + +Compare ARSAS vs external black-box reference tool by exact +`LD/LN/DO/SDO/DA/BDA/FC/bType/value` path, not raw XML position and not total +`Val` count. + +## Regression signatures that are forbidden + +A build is rejected if it restores any of these patterns: + +- legacy `DiscoverAsync` as the public discovery route; +- a second supplemental discovery association; +- recursive per-leaf GVA expansion; +- speculative thousands of Reads on the discovery path; +- cross-DO positional CF projection; +- case-insensitive IEC 61850 member/value identity; +- WYE/DEL/SEQ SDO flattening; +- Edition 2 tracking CDCs in Edition 1; +- invented DataSet bindings for unassigned RCB slots; +- silent canonical-save success without reopen/association validation. + +## Promotion state + +The R10 physical retest has passed and merge is allowed. Production promotion is +still a separate release decision. + +The field result, not test count alone, decides interoperability acceptance. From 520e58715197bc3a7d1f22f1f1ac6cfaaa90fe3b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:43:41 +0700 Subject: [PATCH 04/19] docs(interoperability): retire external-product named document --- docs/IEDSCOUT_CONVERGENCE.md | 160 ----------------------------------- 1 file changed, 160 deletions(-) delete mode 100644 docs/IEDSCOUT_CONVERGENCE.md diff --git a/docs/IEDSCOUT_CONVERGENCE.md b/docs/IEDSCOUT_CONVERGENCE.md deleted file mode 100644 index 673294a00..000000000 --- a/docs/IEDSCOUT_CONVERGENCE.md +++ /dev/null @@ -1,160 +0,0 @@ -# IEDScout Convergence Contract - -## Product target - -ARSAS targets IEDScout-equivalent IEC 61850 engineering semantics with lower wire cost where possible: - -1. one accepted MMS association and bounded structure-first discovery; -2. a complete canonical model with exact LN/DO/SDO/DA/FC identity; -3. Edition 2 IID / Edition 1 ICD that can be reopened by ARSAS, reconnect to the same relay, hydrate values without full discovery, and run configured static reporting. - -The machine-readable authority is `evidence/iedscout-convergence-target.json`. - -## Merged proven baseline - -The physical R10 baseline was tested with: - -- ARSAS `eb8eb13d491f9aa265205852b8a4bab07af440ff`; -- ARIEC61850 tested head `9935d6902d786cc69b299260fe36b835944d5e81`; -- ARIEC61850 merged-main commit `648124097621046f5f127ceb1cf853fea54db730`. - -The tested engine head and merged-main commit have the identical tree SHA -`1cf7e08f333f24994625e8fe8416dbd0a16195b1`. - -Merged engine provenance: - -- PR #134 → main merge `e6779ff74e5716af4fcfc3dc926dae0567b3cdb0`: Smart Discovery performance authority. -- PR #135 → main merge `648124097621046f5f127ceb1cf853fea54db730`: canonical model, SCL interoperability, P1/P2 value pipeline. -- ARSAS PR #324: consumer integration and physical R10 proof. - -## P0 — structural discovery freeze - -Contract: `P0-R9-STRUCTURAL`. - -AA1E1F06R4 physical reference is locked at one association, 323 confirmed MMS -requests, 138 GetNameList, 119 GetVariableAccessAttributes, 2 -GetNamedVariableListAttributes and 64 Reads, while preserving 32 LD / 119 LN / -860 top-level DO / 906 DO+SDO / 4925 scalar leaves / 2 DataSets / 58 FCDA / 32 -logical ReportControls / 1 SettingControl. - -The same-relay IEDScout comparison remains about 417 confirmed requests, 119 GVA -and 156 Reads. ARSAS must not add traffic merely to imitate IEDScout. - -Forbidden regressions include a second discovery association, legacy supplemental -browse, a second full GetNameList sweep, recursive per-leaf GVA, and eager FC-root -value hydration on the discovery critical path. - -## P1 — CF projection-order repair: physically proven - -Contract: `P1-CF-DO-SCOPED`. - -R9 exposed 46 projection errors because SCL LNodeType DO order was incorrectly used -as MMS CF-root child order. P1 reads multi-DO CF data through exact `LN$CF$DO` -references and batches those structured reads. - -R10 physical reuse closes P1: - -- Ed2: 709 initial targets, 525 FC-root targets, 184 DO-scoped targets, - 709/709 successful, 0 failed, `projectionErrors=0`. -- Ed1: 708 initial targets, 524 FC-root targets, 184 DO-scoped targets, - 708/708 successful, 0 failed, `projectionErrors=0`. - -No extra discovery GVA or second association was introduced. - -## P2 — exact-case value pipeline: physically proven - -Contract: `P2-CASE-EXACT-VALUES`. - -R9 Ed2 lost 11 projected values because a consumer cache used case-insensitive -identity. P2 uses exact-case identity through TypeSpecification mapping, initial -projection, trusted-SCL caching, canonical evidence and SCL instance-value targeting. - -R10 physical reuse closes P2: - -- Ed2: `projectedUniqueValues=4441`, `initialValueCache=4441`, `cacheLoss=0`. -- Ed1: `projectedUniqueValues=4246`, `initialValueCache=4246`, `cacheLoss=0`. - -## R10 round-trip/reporting acceptance - -Both generated editions reopen as trusted SCL and reconnect without full discovery: - -- expected/observed/matched MMS domains: 32/32/32; -- DataSets: 2, members: 58, missing members: 0; -- configured report plans: Digital BRCB 36 members + Analog URCB 22 members; -- report-covered runtime points: 58; -- final unresolved runtime points: 0; -- cyclic MMS process polling: 0; -- actual InformationReport traffic observed on both editions. - -The early UI field `Primary unresolved=2` is not an operational loss: the exact -static DataSet schema resolves all 58 runtime points before reporting starts. - -## Save-time instance values - -R10 physically proves bounded Save SCL enrichment while fast discovery stays -unchanged: - -- canonical instance evidence: 3854 leaves; -- Ed2 exported `Val`: 3202; -- Ed1 exported `Val`: 3106. - -IEDScout's golden file has about 1529 `Val` elements. A larger count is not -automatically better; the remaining task is semantic path/value comparison, not -count chasing. - -## RCB lock - -The accepted representation remains: - -- 34 runtime RCB objects → 32 logical SCL ReportControls; -- `Services/ConfReportControl max=34`; -- Buffer/Digital and Unbuffer/Analog remain the two configured report authorities; -- preallocated ADD slots without DataSet never receive an invented `datSet`. - -## Next improvements — do not disturb the proven wire/reuse path - -### 1. Template interning - -R10 Ed2 is semantically correct but verbose: - -- ARSAS: 119 LNodeType / 906 DOType / 752 DAType / about 731 KB; -- IEDScout reference: about 38 / 60 / 17 / about 247 KB. - -Next work may intern only templates with identical ordered semantic fingerprints. -Expanded model counts, FC ownership, values, DataSets/RCBs and round-trip behavior -must remain unchanged. - -### 2. Reuse one save-enrichment snapshot across Ed2 and Ed1 - -When Ed2 and Ed1 are saved in the same unchanged live association/model generation, -both currently derive the same 3854 instance-evidence leaves. A later optimization -may reuse that bounded snapshot across serializers, but never across reconnect, -model-generation change or explicit refresh. - -### 3. Semantic Val diff - -Compare ARSAS vs IEDScout by exact -`LD/LN/DO/SDO/DA/BDA/FC/bType/value` path, not raw XML position and not total -`Val` count. - -## Regression signatures that are forbidden - -A build is rejected if it restores any of these patterns: - -- legacy `DiscoverAsync` as the public discovery route; -- a second supplemental discovery association; -- recursive per-leaf GVA expansion; -- speculative thousands of Reads on the discovery path; -- cross-DO positional CF projection; -- case-insensitive IEC 61850 member/value identity; -- WYE/DEL/SEQ SDO flattening; -- Edition 2 tracking CDCs in Edition 1; -- invented DataSet bindings for unassigned RCB slots; -- silent canonical-save success without reopen/association validation. - -## Promotion state - -The R10 physical retest has passed and merge is allowed. Production promotion is -still a separate release decision. - -The field result, not test count alone, decides interoperability acceptance. From b500cbaed9482f651d23a1a9bb4016fbd6304afc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:44:04 +0700 Subject: [PATCH 05/19] refactor(ci): provide neutral IEC 61850 reference acceptance workflow --- .../interoperability-reference-guard.yml | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 .github/workflows/interoperability-reference-guard.yml diff --git a/.github/workflows/interoperability-reference-guard.yml b/.github/workflows/interoperability-reference-guard.yml new file mode 100644 index 000000000..dd5a9d0bc --- /dev/null +++ b/.github/workflows/interoperability-reference-guard.yml @@ -0,0 +1,327 @@ +name: IEC 61850 Interoperability Reference Guard + +on: + pull_request: + paths: + - "Services/NativeIec61850Client*.cs" + - "Services/SclAssistedConnectionPreparation.cs" + - "Services/CanonicalSclReloadValidator.cs" + - "MainWindow.xaml.cs" + - "Directory.Build.targets" + - "scripts/enable-smart-discovery-capture.ps1" + - "engines/ARIEC61850.lock.json" + - "evidence/interoperability-reference-target.json" + - "docs/INTEROPERABILITY_REFERENCE_CONTRACT.md" + - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" + - ".github/workflows/interoperability-reference-guard.yml" + - ".github/workflows/scl-interoperability-r7.yml" + workflow_dispatch: + +concurrency: + group: interoperability-reference-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + interoperability-reference-contract: + name: interoperability-reference-contract + runs-on: windows-latest + + steps: + - name: Checkout exact ARSAS revision + shell: pwsh + env: + ARSAS_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + if ($env:ARSAS_SOURCE_SHA -notmatch '^[0-9a-f]{40}$') { + throw "Invalid ARSAS source SHA: $env:ARSAS_SOURCE_SHA" + } + git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:GITHUB_REPOSITORY.git" ARSAS + git -C .\ARSAS fetch --quiet --depth 1 origin $env:ARSAS_SOURCE_SHA + git -C .\ARSAS checkout --quiet --detach $env:ARSAS_SOURCE_SHA + $actual = (git -C .\ARSAS rev-parse HEAD).Trim() + if ($actual -ne $env:ARSAS_SOURCE_SHA) { + throw "ARSAS SHA mismatch. Expected $env:ARSAS_SOURCE_SHA, got $actual." + } + + - name: Verify single convergence authority + shell: pwsh + run: | + $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json + $target = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json + + if ($target.status -ne 'physical-retest-passed-merge-ready') { + throw "Convergence status changed unexpectedly: $($target.status)" + } + if ([int]$target.activeStack.enginePerformance.pullRequest -ne 134 -or + [int]$target.activeStack.engineModelAndScl.pullRequest -ne 135 -or + [int]$target.activeStack.engineModelAndScl.basePullRequest -ne 134 -or + [int]$target.activeStack.consumerIntegration.pullRequest -ne 324) { + throw 'Merged engine authority must remain PR #134 + PR #135 with ARSAS #324 provenance.' + } + if ($lock.commit -ne $target.activeStack.engineModelAndScl.head -or + [int]$lock.sourcePullRequest -ne 135) { + throw 'Engine lock drifted from the convergence single source of truth.' + } + if ([bool]$target.promotion.productionPromoted -or + [bool]$target.promotion.mergeAllowedBeforePhysicalRetest -or + [bool]$target.promotion.physicalRetestRequired -or + -not [bool]$target.promotion.mergeAllowedAfterPhysicalRetest -or + -not [bool]$target.promotion.physicalRetestPassed) { + throw 'R10 physical-retest/merge gate drifted.' + } + if ([int]$target.physicalReference.externalReferenceCapture.associations -ne 1 -or + [int]$target.physicalReference.externalReferenceCapture.getVariableAccessAttributes -ne 119 -or + [int]$target.physicalReference.rejectedLegacyArsasCapture.associations -ne 2 -or + [int]$target.physicalReference.rejectedLegacyArsasCapture.getVariableAccessAttributes -lt 20000) { + throw 'Physical external IEC 61850 reference/legacy regression reference changed unexpectedly.' + } + + + $p0 = $target.p0StructuralDiscoveryFreeze + if ($p0.contractId -ne 'P0-R9-STRUCTURAL' -or + $p0.status -ne 'implemented-and-r9-physically-proven' -or + -not [bool]$p0.sourcePolicy.smartRouteRequired -or + -not [bool]$p0.sourcePolicy.associationScopedSingleFlightRequired -or + -not [bool]$p0.sourcePolicy.applicationMmsGateExclusive -or + -not [bool]$p0.sourcePolicy.logicalNodeRootTypeStrategyRequired -or + -not [bool]$p0.sourcePolicy.eagerInitialFcReadsForbidden -or + -not [bool]$p0.sourcePolicy.supplementalLegacyBrowseForbidden -or + -not [bool]$p0.sourcePolicy.secondFullGetNameListSweepForbidden -or + -not [bool]$p0.sourcePolicy.recursivePerLeafGvaStormForbidden -or + -not [bool]$p0.sourcePolicy.saveTimeAndTrustedSclValueReadsRemainSeparate) { + throw 'P0 structural-discovery freeze policy was weakened.' + } + + if ([int]$p0.frozenOptions.maxConcurrentChains -ne 8 -or + [int]$p0.frozenOptions.unknownPeerMaxConcurrentChains -ne 4 -or + [int]$p0.frozenOptions.maxDomains -ne 256 -or + [int]$p0.frozenOptions.maxVariableNamesPerDomain -ne 20000 -or + [int]$p0.frozenOptions.maxVariableListNamesPerDomain -ne 4096 -or + [int]$p0.frozenOptions.maxNameListPages -ne 64 -or + -not [bool]$p0.frozenOptions.probeReportAttributes -or + [int]$p0.frozenOptions.maxReportAttributeProbes -ne 64 -or + -not [bool]$p0.frozenOptions.readDataSetDirectories -or + [int]$p0.frozenOptions.maxDataSetDirectoryReads -ne 64) { + throw 'P0 frozen smart-discovery options drifted.' + } + + $p0Relay = $p0.aa1e1f06r4Acceptance + if ([int]$p0Relay.associations -ne 1 -or + [int]$p0Relay.referenceConfirmedMmsRequests -ne 323 -or + [int]$p0Relay.maximumConfirmedMmsRequests -ne 417 -or + [int]$p0Relay.referenceGetNameList -ne 138 -or + [int]$p0Relay.referenceGetVariableAccessAttributes -ne 119 -or + [int]$p0Relay.maximumGetVariableAccessAttributes -ne 119 -or + [int]$p0Relay.referenceGetNamedVariableListAttributes -ne 2 -or + [int]$p0Relay.referenceReads -ne 64 -or + [int]$p0Relay.maximumReads -ne 156 -or + [int]$p0Relay.logicalDevices -ne 32 -or + [int]$p0Relay.logicalNodes -ne 119 -or + [int]$p0Relay.topLevelDataObjects -ne 860 -or + [int]$p0Relay.dataObjectsIncludingSdo -ne 906 -or + [int]$p0Relay.scalarLeaves -ne 4925 -or + [int]$p0Relay.dataSets -ne 2 -or + [int]$p0Relay.fcda -ne 58 -or + [int]$p0Relay.logicalReportControls -ne 32 -or + [int]$p0Relay.settingControls -ne 1) { + throw 'P0 AA1E1F06R4 discovery acceptance contract drifted.' + } + + $p1 = $target.p1ProjectionOrderRepair + if ($p1.contractId -ne 'P1-CF-DO-SCOPED' -or + $p1.status -ne 'physically-proven-r10' -or + [int]$p1.physicalR10Outcome.edition2.projectionErrors -ne 0 -or + [int]$p1.physicalR10Outcome.edition1.projectionErrors -ne 0 -or + [int]$p1.physicalR10Outcome.edition2.failedReads -ne 0 -or + [int]$p1.physicalR10Outcome.edition1.failedReads -ne 0 -or + [int]$p1.physicalR10Outcome.reportBackedRuntimePoints -ne 58 -or + [int]$p1.physicalR10Outcome.unresolvedRuntimePoints -ne 0 -or + -not [bool]$p1.physicalR10Outcome.actualInformationReportObserved) { + throw 'P1 physical projection-order proof regressed.' + } + + $p2 = $target.p2CaseSensitiveValuePipeline + if ($p2.contractId -ne 'P2-CASE-EXACT-VALUES' -or + $p2.status -ne 'physically-proven-r10' -or + -not [bool]$p2.preservesP0StructuralDiscovery -or + -not [bool]$p2.preservesP1CfScopedHydration -or + [int]$p2.physicalR9Evidence.observedCacheLoss -ne 11 -or + [int]$p2.expectedPhysicalOutcome.edition2CacheLoss -ne 0 -or + [int]$p2.expectedPhysicalOutcome.edition1CacheLoss -ne 0 -or + -not [bool]$p2.expectedPhysicalOutcome.ltrkLowercaseTAndUppercaseTRemainDistinct -or + -not [bool]$p2.expectedPhysicalOutcome.noAdditionalDiscoveryGva -or + -not [bool]$p2.expectedPhysicalOutcome.sameAssociation -or + [int]$p2.physicalR10Outcome.edition2.cacheLoss -ne 0 -or + [int]$p2.physicalR10Outcome.edition1.cacheLoss -ne 0 -or + [int]$p2.physicalR10Outcome.edition2.projectedUniqueValues -ne [int]$p2.physicalR10Outcome.edition2.initialValueCache -or + [int]$p2.physicalR10Outcome.edition1.projectedUniqueValues -ne [int]$p2.physicalR10Outcome.edition1.initialValueCache -or + $p2.sourceInvariants.trustedSclValueCacheComparer -ne 'StringComparer.Ordinal' -or + $p2.sourceInvariants.typeSpecificationMemberComparison -ne 'StringComparison.Ordinal' -or + $p2.sourceInvariants.sclDataObjectInstanceTargetComparison -ne 'StringComparison.Ordinal') { + throw 'P2 exact-case value pipeline contract was weakened.' + } + + $r10 = $target.physicalEvidence.arsasR10 + if ($r10.testedArtifact.appHead -ne 'eb8eb13d491f9aa265205852b8a4bab07af440ff' -or + $r10.testedArtifact.engineTestedHead -ne '9935d6902d786cc69b299260fe36b835944d5e81' -or + $r10.testedArtifact.engineMergedMain -ne '648124097621046f5f127ceb1cf853fea54db730' -or + [int]$r10.discovery.associations -ne 1 -or + [int]$r10.discovery.confirmedMmsRequests -ne 323 -or + [int]$r10.discovery.getVariableAccessAttributes -ne 119 -or + [int]$r10.reuseEdition2.matchedDomains -ne 32 -or + [int]$r10.reuseEdition2.initialTargets -ne 709 -or + [int]$r10.reuseEdition2.successfulReads -ne 709 -or + [int]$r10.reuseEdition2.failedReads -ne 0 -or + [int]$r10.reuseEdition2.projectionErrors -ne 0 -or + [int]$r10.reuseEdition2.cacheLoss -ne 0 -or + [int]$r10.reuseEdition2.reportBackedRuntimePoints -ne 58 -or + [int]$r10.reuseEdition2.unresolvedRuntimePoints -ne 0 -or + -not [bool]$r10.reuseEdition2.actualInformationReportObserved -or + [int]$r10.reuseEdition1.matchedDomains -ne 32 -or + [int]$r10.reuseEdition1.initialTargets -ne 708 -or + [int]$r10.reuseEdition1.successfulReads -ne 708 -or + [int]$r10.reuseEdition1.failedReads -ne 0 -or + [int]$r10.reuseEdition1.projectionErrors -ne 0 -or + [int]$r10.reuseEdition1.cacheLoss -ne 0 -or + [int]$r10.reuseEdition1.reportBackedRuntimePoints -ne 58 -or + [int]$r10.reuseEdition1.unresolvedRuntimePoints -ne 0 -or + -not [bool]$r10.reuseEdition1.actualInformationReportObserved) { + throw 'R10 physical SCL reuse acceptance evidence regressed.' + } + + $r9 = $target.physicalEvidence.arsasR9 + if ([int]$r9.discovery.associations -ne 1 -or + [int]$r9.discovery.confirmedMmsRequests -ne 323 -or + [int]$r9.discovery.getVariableAccessAttributes -ne 119 -or + [int]$r9.discovery.topLevelDataObjects -ne 860 -or + [int]$r9.discovery.dataObjectsIncludingSdo -ne 906 -or + [int]$r9.discovery.scalarLeaves -ne 4925) { + throw 'R9 physical discovery/model acceptance evidence drifted.' + } + + if ([int]$r9.reuseEdition2.expectedDomains -ne 32 -or + [int]$r9.reuseEdition2.matchedDomains -ne 32 -or + [int]$r9.reuseEdition2.successfulReads -ne [int]$r9.reuseEdition2.fcRoots -or + [int]$r9.reuseEdition2.failedReads -ne 0 -or + [int]$r9.reuseEdition2.reportBackedRuntimePoints -ne 58 -or + [int]$r9.reuseEdition2.unresolvedRuntimePoints -ne 0 -or + -not [bool]$r9.reuseEdition2.actualInformationReportObserved -or + [int]$r9.reuseEdition1.expectedDomains -ne 32 -or + [int]$r9.reuseEdition1.matchedDomains -ne 32 -or + [int]$r9.reuseEdition1.successfulReads -ne [int]$r9.reuseEdition1.fcRoots -or + [int]$r9.reuseEdition1.failedReads -ne 0 -or + [int]$r9.reuseEdition1.reportBackedRuntimePoints -ne 58 -or + [int]$r9.reuseEdition1.unresolvedRuntimePoints -ne 0 -or + -not [bool]$r9.reuseEdition1.actualInformationReportObserved) { + throw 'R9 trusted-SCL reconnect/reporting acceptance evidence regressed.' + } + + if ([int]$target.openGaps.trustedSclProjectionParity.targetProjectionErrors -ne 0 -or + [int]$target.openGaps.trustedSclProjectionParity.observedEdition2 -ne [int]$r9.reuseEdition2.projectionErrors -or + [int]$target.openGaps.trustedSclProjectionParity.observedEdition1 -ne [int]$r9.reuseEdition1.projectionErrors -or + [int]$target.openGaps.trustedSclProjectionParity.r10ObservedEdition2 -ne 0 -or + [int]$target.openGaps.trustedSclProjectionParity.r10ObservedEdition1 -ne 0 -or + [int]$target.openGaps.edition2CaseDistinctInitialValueCache.observedProjectedMinusCached -ne 11 -or + [int]$target.openGaps.edition2CaseDistinctInitialValueCache.r10ObservedProjectedMinusCached -ne 0 -or + [int]$target.openGaps.preallocatedAddReportControls.observedWithoutDataSet -ne 30 -or + [int]$target.openGaps.preallocatedAddReportControls.r10FatalMissingDatasetErrors -ne 0) { + throw 'R9 history or R10 resolved semantic evidence drifted.' + } + + "ENGINE_REPOSITORY=$($lock.repository)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + "ENGINE_COMMIT=$($lock.commit)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Checkout exact convergence engine + shell: pwsh + run: | + git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ENGINE_REPOSITORY.git" ARIEC61850 + git -C .\ARIEC61850 fetch --quiet --depth 1 origin $env:ENGINE_COMMIT + git -C .\ARIEC61850 checkout --quiet --detach $env:ENGINE_COMMIT + $actual = (git -C .\ARIEC61850 rev-parse HEAD).Trim() + if ($actual -ne $env:ENGINE_COMMIT) { + throw "Engine SHA mismatch. Expected $env:ENGINE_COMMIT, got $actual." + } + + - name: Verify discovery and SCL anti-regression contracts + shell: pwsh + run: | + $capture = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryCapture.cs -Raw + $sclAssisted = Get-Content .\ARSAS\Services\NativeIec61850Client.SclAssisted.cs -Raw + $canonicalModel = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\LiveIedCanonicalModel.cs -Raw + $typeHierarchy = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\LiveIedVariableTypeHierarchy.cs -Raw + $canonicalExporter = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\CanonicalLiveIedSclExporter.cs -Raw + $targets = Get-Content .\ARSAS\Directory.Build.targets -Raw + $smart = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartDiscovery.cs -Raw + $singleFlight = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartDiscoverySingleFlight.cs -Raw + $smartGva = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartVariableAccessAttributes.cs -Raw + $lnParts = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\Iec61850ReferenceParts.cs -Raw + $exporter = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\LiveIedSclExporter.cs -Raw + $registry = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\Iec61850StandardModelRegistry.cs -Raw + $cdc = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\CdcInferenceEngine.cs -Raw + + $valueCacheIndex = $sclAssisted.IndexOf('_trustedSclInitialValues =', [System.StringComparison]::Ordinal) + if ($valueCacheIndex -lt 0) { + throw 'P2 trusted-SCL value cache declaration is missing.' + } + $valueCacheLength = [Math]::Min(260, $sclAssisted.Length - $valueCacheIndex) + $valueCacheSegment = $sclAssisted.Substring($valueCacheIndex, $valueCacheLength) + if ($valueCacheSegment -notmatch 'StringComparer\.Ordinal' -or + $valueCacheSegment -match 'StringComparer\.OrdinalIgnoreCase' -or + $sclAssisted -notmatch 'projectedUniqueValues' -or + $sclAssisted -notmatch 'initialValueCacheLoss' -or + $sclAssisted -notmatch 'cacheLoss=\{initialValueCacheLoss\}' -or + $canonicalModel -notmatch 'new HashSet\(StringComparer\.Ordinal\)' -or + $typeHierarchy -notmatch 'current\.Children\[index\]\.Name, part, StringComparison\.Ordinal\)' -or + $canonicalExporter -notmatch 'dataObjectName\.Trim\(\),\s*[\s\S]*?StringComparison\.Ordinal\)') { + throw 'P2 case-sensitive value path implementation regressed.' + } + + if ($targets -notmatch 'VerifyPhysicalProvenSmartDiscoveryRoute' -or + $targets -notmatch '\-VerifyOnly' -or + $targets -match 'GITHUB_WORKFLOW' -or + $targets -match 'SmartDiscoveryProductionPromoted' -or + $capture -notmatch 'SmartDiscoveryStructuralFreezeContract = "P0-R9-STRUCTURAL"' -or + $capture -notmatch 'CreateP0FrozenSmartDiscoveryOptions' -or + $capture -notmatch 'MaxConcurrentChains = 8' -or + $capture -notmatch 'UnknownPeerMaxConcurrentChains = 4' -or + $capture -notmatch 'MaxNameListPages = 64' -or + $capture -notmatch 'ProbeReportAttributes = true' -or + $capture -notmatch 'MaxReportAttributeProbes = 64' -or + $capture -notmatch 'ReadDataSetDirectories = true' -or + $capture -notmatch 'MaxDataSetDirectoryReads = 64' -or + $capture -notmatch 'GetOrCreateSmartDiscoveryAssociationFlight' -or + $capture -notmatch 'DiscoverSmartSingleFlightAsync' -or + $capture -notmatch 'ProbeSmartAsync' -or + $capture -notmatch 'initialFcRoots=deferred' -or + $capture -notmatch 'InitialFcReadExecutionResult\? initialRead = null' -or + $capture -match '_session\.DiscoverAsync\(' -or + $capture -match 'DiscoverDomainVariableNamesAsync' -or + $capture -match 'TryBuildSupplementalGetNameListSnapshotAsync' -or + $capture -match 'DiscoverDomainVariableTypeTreeNamesAsync' -or + $capture -match 'AddAdaptiveLogicalNodeSiblingProbeSignalsAsync' -or + $capture -match 'EnrichEngineeringUnitsAsync' -or + $capture -match 'InitialFcReadPlanner\.FromSclModel' -or + $capture -match 'ExecuteInitialFcReadPlanSmartAsync' -or + $smart -notmatch 'DiscoverSmartAsync' -or + $singleFlight -notmatch 'DiscoverSmartSingleFlightAsync' -or + $singleFlight -notmatch 'DiscoverSmartAsync\(options, CancellationToken\.None\)' -or + $smartGva -notmatch 'GetVariableAccessAttributesSmartAsync') { + throw 'P0 structural discovery freeze regressed away from the accepted external IEC 61850 reference convergence path.' + } + + if ($lnParts -notmatch 'instanceStart' -or + $lnParts -notmatch 'IsFourLetterLnClass' -or + $exporter -notmatch 'TryResolveStandardSubDataObjectCdc' -or + $exporter -notmatch 'DataObjectReferencePaths' -or + $exporter -notmatch 'IsEdition2ServiceTrackingCdc' -or + $registry -notmatch 'Key\("TCTR", "ARtg"\)' -or + $registry -notmatch 'Key\("TVTR", "VRtg"\)' -or + $registry -notmatch 'Key\("LTIM", "TmChgDT"\)' -or + $registry -notmatch 'Key\("LTRK", "BrcbTrk"\)' -or + $registry -notmatch 'Key\("XCBR", "EEName"\)' -or + $registry -notmatch 'Key\("LLN0", "MltLev"\)' -or + $cdc -notmatch '"CST".*"BTS".*"UTS".*"STS".*"CTS"') { + throw 'Canonical IEC model / SCL semantic authority regressed.' + } + + Write-Host 'external IEC 61850 reference convergence contract PASS.' From 27398d3c1d9c6725f7ee0bd9685dce21e208713a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:44:06 +0700 Subject: [PATCH 06/19] refactor(ci): retire external-product named workflow entrypoint --- .../workflows/iedscout-convergence-guard.yml | 327 ------------------ 1 file changed, 327 deletions(-) delete mode 100644 .github/workflows/iedscout-convergence-guard.yml diff --git a/.github/workflows/iedscout-convergence-guard.yml b/.github/workflows/iedscout-convergence-guard.yml deleted file mode 100644 index e340266ff..000000000 --- a/.github/workflows/iedscout-convergence-guard.yml +++ /dev/null @@ -1,327 +0,0 @@ -name: IEDScout Convergence Guard - -on: - pull_request: - paths: - - "Services/NativeIec61850Client*.cs" - - "Services/SclAssistedConnectionPreparation.cs" - - "Services/CanonicalSclReloadValidator.cs" - - "MainWindow.xaml.cs" - - "Directory.Build.targets" - - "scripts/enable-smart-discovery-capture.ps1" - - "engines/ARIEC61850.lock.json" - - "evidence/iedscout-convergence-target.json" - - "docs/IEDSCOUT_CONVERGENCE.md" - - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" - - ".github/workflows/iedscout-convergence-guard.yml" - - ".github/workflows/scl-interoperability-r7.yml" - workflow_dispatch: - -concurrency: - group: iedscout-convergence-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - iedscout-convergence-contract: - name: iedscout-convergence-contract - runs-on: windows-latest - - steps: - - name: Checkout exact ARSAS revision - shell: pwsh - env: - ARSAS_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - if ($env:ARSAS_SOURCE_SHA -notmatch '^[0-9a-f]{40}$') { - throw "Invalid ARSAS source SHA: $env:ARSAS_SOURCE_SHA" - } - git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:GITHUB_REPOSITORY.git" ARSAS - git -C .\ARSAS fetch --quiet --depth 1 origin $env:ARSAS_SOURCE_SHA - git -C .\ARSAS checkout --quiet --detach $env:ARSAS_SOURCE_SHA - $actual = (git -C .\ARSAS rev-parse HEAD).Trim() - if ($actual -ne $env:ARSAS_SOURCE_SHA) { - throw "ARSAS SHA mismatch. Expected $env:ARSAS_SOURCE_SHA, got $actual." - } - - - name: Verify single convergence authority - shell: pwsh - run: | - $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json - $target = Get-Content .\ARSAS\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json - - if ($target.status -ne 'physical-retest-passed-merge-ready') { - throw "Convergence status changed unexpectedly: $($target.status)" - } - if ([int]$target.activeStack.enginePerformance.pullRequest -ne 134 -or - [int]$target.activeStack.engineModelAndScl.pullRequest -ne 135 -or - [int]$target.activeStack.engineModelAndScl.basePullRequest -ne 134 -or - [int]$target.activeStack.consumerIntegration.pullRequest -ne 324) { - throw 'Merged engine authority must remain PR #134 + PR #135 with ARSAS #324 provenance.' - } - if ($lock.commit -ne $target.activeStack.engineModelAndScl.head -or - [int]$lock.sourcePullRequest -ne 135) { - throw 'Engine lock drifted from the convergence single source of truth.' - } - if ([bool]$target.promotion.productionPromoted -or - [bool]$target.promotion.mergeAllowedBeforePhysicalRetest -or - [bool]$target.promotion.physicalRetestRequired -or - -not [bool]$target.promotion.mergeAllowedAfterPhysicalRetest -or - -not [bool]$target.promotion.physicalRetestPassed) { - throw 'R10 physical-retest/merge gate drifted.' - } - if ([int]$target.physicalReference.iedScoutCapture.associations -ne 1 -or - [int]$target.physicalReference.iedScoutCapture.getVariableAccessAttributes -ne 119 -or - [int]$target.physicalReference.rejectedLegacyArsasCapture.associations -ne 2 -or - [int]$target.physicalReference.rejectedLegacyArsasCapture.getVariableAccessAttributes -lt 20000) { - throw 'Physical IEDScout/legacy regression reference changed unexpectedly.' - } - - - $p0 = $target.p0StructuralDiscoveryFreeze - if ($p0.contractId -ne 'P0-R9-STRUCTURAL' -or - $p0.status -ne 'implemented-and-r9-physically-proven' -or - -not [bool]$p0.sourcePolicy.smartRouteRequired -or - -not [bool]$p0.sourcePolicy.associationScopedSingleFlightRequired -or - -not [bool]$p0.sourcePolicy.applicationMmsGateExclusive -or - -not [bool]$p0.sourcePolicy.logicalNodeRootTypeStrategyRequired -or - -not [bool]$p0.sourcePolicy.eagerInitialFcReadsForbidden -or - -not [bool]$p0.sourcePolicy.supplementalLegacyBrowseForbidden -or - -not [bool]$p0.sourcePolicy.secondFullGetNameListSweepForbidden -or - -not [bool]$p0.sourcePolicy.recursivePerLeafGvaStormForbidden -or - -not [bool]$p0.sourcePolicy.saveTimeAndTrustedSclValueReadsRemainSeparate) { - throw 'P0 structural-discovery freeze policy was weakened.' - } - - if ([int]$p0.frozenOptions.maxConcurrentChains -ne 8 -or - [int]$p0.frozenOptions.unknownPeerMaxConcurrentChains -ne 4 -or - [int]$p0.frozenOptions.maxDomains -ne 256 -or - [int]$p0.frozenOptions.maxVariableNamesPerDomain -ne 20000 -or - [int]$p0.frozenOptions.maxVariableListNamesPerDomain -ne 4096 -or - [int]$p0.frozenOptions.maxNameListPages -ne 64 -or - -not [bool]$p0.frozenOptions.probeReportAttributes -or - [int]$p0.frozenOptions.maxReportAttributeProbes -ne 64 -or - -not [bool]$p0.frozenOptions.readDataSetDirectories -or - [int]$p0.frozenOptions.maxDataSetDirectoryReads -ne 64) { - throw 'P0 frozen smart-discovery options drifted.' - } - - $p0Relay = $p0.aa1e1f06r4Acceptance - if ([int]$p0Relay.associations -ne 1 -or - [int]$p0Relay.referenceConfirmedMmsRequests -ne 323 -or - [int]$p0Relay.maximumConfirmedMmsRequests -ne 417 -or - [int]$p0Relay.referenceGetNameList -ne 138 -or - [int]$p0Relay.referenceGetVariableAccessAttributes -ne 119 -or - [int]$p0Relay.maximumGetVariableAccessAttributes -ne 119 -or - [int]$p0Relay.referenceGetNamedVariableListAttributes -ne 2 -or - [int]$p0Relay.referenceReads -ne 64 -or - [int]$p0Relay.maximumReads -ne 156 -or - [int]$p0Relay.logicalDevices -ne 32 -or - [int]$p0Relay.logicalNodes -ne 119 -or - [int]$p0Relay.topLevelDataObjects -ne 860 -or - [int]$p0Relay.dataObjectsIncludingSdo -ne 906 -or - [int]$p0Relay.scalarLeaves -ne 4925 -or - [int]$p0Relay.dataSets -ne 2 -or - [int]$p0Relay.fcda -ne 58 -or - [int]$p0Relay.logicalReportControls -ne 32 -or - [int]$p0Relay.settingControls -ne 1) { - throw 'P0 AA1E1F06R4 discovery acceptance contract drifted.' - } - - $p1 = $target.p1ProjectionOrderRepair - if ($p1.contractId -ne 'P1-CF-DO-SCOPED' -or - $p1.status -ne 'physically-proven-r10' -or - [int]$p1.physicalR10Outcome.edition2.projectionErrors -ne 0 -or - [int]$p1.physicalR10Outcome.edition1.projectionErrors -ne 0 -or - [int]$p1.physicalR10Outcome.edition2.failedReads -ne 0 -or - [int]$p1.physicalR10Outcome.edition1.failedReads -ne 0 -or - [int]$p1.physicalR10Outcome.reportBackedRuntimePoints -ne 58 -or - [int]$p1.physicalR10Outcome.unresolvedRuntimePoints -ne 0 -or - -not [bool]$p1.physicalR10Outcome.actualInformationReportObserved) { - throw 'P1 physical projection-order proof regressed.' - } - - $p2 = $target.p2CaseSensitiveValuePipeline - if ($p2.contractId -ne 'P2-CASE-EXACT-VALUES' -or - $p2.status -ne 'physically-proven-r10' -or - -not [bool]$p2.preservesP0StructuralDiscovery -or - -not [bool]$p2.preservesP1CfScopedHydration -or - [int]$p2.physicalR9Evidence.observedCacheLoss -ne 11 -or - [int]$p2.expectedPhysicalOutcome.edition2CacheLoss -ne 0 -or - [int]$p2.expectedPhysicalOutcome.edition1CacheLoss -ne 0 -or - -not [bool]$p2.expectedPhysicalOutcome.ltrkLowercaseTAndUppercaseTRemainDistinct -or - -not [bool]$p2.expectedPhysicalOutcome.noAdditionalDiscoveryGva -or - -not [bool]$p2.expectedPhysicalOutcome.sameAssociation -or - [int]$p2.physicalR10Outcome.edition2.cacheLoss -ne 0 -or - [int]$p2.physicalR10Outcome.edition1.cacheLoss -ne 0 -or - [int]$p2.physicalR10Outcome.edition2.projectedUniqueValues -ne [int]$p2.physicalR10Outcome.edition2.initialValueCache -or - [int]$p2.physicalR10Outcome.edition1.projectedUniqueValues -ne [int]$p2.physicalR10Outcome.edition1.initialValueCache -or - $p2.sourceInvariants.trustedSclValueCacheComparer -ne 'StringComparer.Ordinal' -or - $p2.sourceInvariants.typeSpecificationMemberComparison -ne 'StringComparison.Ordinal' -or - $p2.sourceInvariants.sclDataObjectInstanceTargetComparison -ne 'StringComparison.Ordinal') { - throw 'P2 exact-case value pipeline contract was weakened.' - } - - $r10 = $target.physicalEvidence.arsasR10 - if ($r10.testedArtifact.appHead -ne 'eb8eb13d491f9aa265205852b8a4bab07af440ff' -or - $r10.testedArtifact.engineTestedHead -ne '9935d6902d786cc69b299260fe36b835944d5e81' -or - $r10.testedArtifact.engineMergedMain -ne '648124097621046f5f127ceb1cf853fea54db730' -or - [int]$r10.discovery.associations -ne 1 -or - [int]$r10.discovery.confirmedMmsRequests -ne 323 -or - [int]$r10.discovery.getVariableAccessAttributes -ne 119 -or - [int]$r10.reuseEdition2.matchedDomains -ne 32 -or - [int]$r10.reuseEdition2.initialTargets -ne 709 -or - [int]$r10.reuseEdition2.successfulReads -ne 709 -or - [int]$r10.reuseEdition2.failedReads -ne 0 -or - [int]$r10.reuseEdition2.projectionErrors -ne 0 -or - [int]$r10.reuseEdition2.cacheLoss -ne 0 -or - [int]$r10.reuseEdition2.reportBackedRuntimePoints -ne 58 -or - [int]$r10.reuseEdition2.unresolvedRuntimePoints -ne 0 -or - -not [bool]$r10.reuseEdition2.actualInformationReportObserved -or - [int]$r10.reuseEdition1.matchedDomains -ne 32 -or - [int]$r10.reuseEdition1.initialTargets -ne 708 -or - [int]$r10.reuseEdition1.successfulReads -ne 708 -or - [int]$r10.reuseEdition1.failedReads -ne 0 -or - [int]$r10.reuseEdition1.projectionErrors -ne 0 -or - [int]$r10.reuseEdition1.cacheLoss -ne 0 -or - [int]$r10.reuseEdition1.reportBackedRuntimePoints -ne 58 -or - [int]$r10.reuseEdition1.unresolvedRuntimePoints -ne 0 -or - -not [bool]$r10.reuseEdition1.actualInformationReportObserved) { - throw 'R10 physical SCL reuse acceptance evidence regressed.' - } - - $r9 = $target.physicalEvidence.arsasR9 - if ([int]$r9.discovery.associations -ne 1 -or - [int]$r9.discovery.confirmedMmsRequests -ne 323 -or - [int]$r9.discovery.getVariableAccessAttributes -ne 119 -or - [int]$r9.discovery.topLevelDataObjects -ne 860 -or - [int]$r9.discovery.dataObjectsIncludingSdo -ne 906 -or - [int]$r9.discovery.scalarLeaves -ne 4925) { - throw 'R9 physical discovery/model acceptance evidence drifted.' - } - - if ([int]$r9.reuseEdition2.expectedDomains -ne 32 -or - [int]$r9.reuseEdition2.matchedDomains -ne 32 -or - [int]$r9.reuseEdition2.successfulReads -ne [int]$r9.reuseEdition2.fcRoots -or - [int]$r9.reuseEdition2.failedReads -ne 0 -or - [int]$r9.reuseEdition2.reportBackedRuntimePoints -ne 58 -or - [int]$r9.reuseEdition2.unresolvedRuntimePoints -ne 0 -or - -not [bool]$r9.reuseEdition2.actualInformationReportObserved -or - [int]$r9.reuseEdition1.expectedDomains -ne 32 -or - [int]$r9.reuseEdition1.matchedDomains -ne 32 -or - [int]$r9.reuseEdition1.successfulReads -ne [int]$r9.reuseEdition1.fcRoots -or - [int]$r9.reuseEdition1.failedReads -ne 0 -or - [int]$r9.reuseEdition1.reportBackedRuntimePoints -ne 58 -or - [int]$r9.reuseEdition1.unresolvedRuntimePoints -ne 0 -or - -not [bool]$r9.reuseEdition1.actualInformationReportObserved) { - throw 'R9 trusted-SCL reconnect/reporting acceptance evidence regressed.' - } - - if ([int]$target.openGaps.trustedSclProjectionParity.targetProjectionErrors -ne 0 -or - [int]$target.openGaps.trustedSclProjectionParity.observedEdition2 -ne [int]$r9.reuseEdition2.projectionErrors -or - [int]$target.openGaps.trustedSclProjectionParity.observedEdition1 -ne [int]$r9.reuseEdition1.projectionErrors -or - [int]$target.openGaps.trustedSclProjectionParity.r10ObservedEdition2 -ne 0 -or - [int]$target.openGaps.trustedSclProjectionParity.r10ObservedEdition1 -ne 0 -or - [int]$target.openGaps.edition2CaseDistinctInitialValueCache.observedProjectedMinusCached -ne 11 -or - [int]$target.openGaps.edition2CaseDistinctInitialValueCache.r10ObservedProjectedMinusCached -ne 0 -or - [int]$target.openGaps.preallocatedAddReportControls.observedWithoutDataSet -ne 30 -or - [int]$target.openGaps.preallocatedAddReportControls.r10FatalMissingDatasetErrors -ne 0) { - throw 'R9 history or R10 resolved semantic evidence drifted.' - } - - "ENGINE_REPOSITORY=$($lock.repository)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append - "ENGINE_COMMIT=$($lock.commit)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append - - - name: Checkout exact convergence engine - shell: pwsh - run: | - git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ENGINE_REPOSITORY.git" ARIEC61850 - git -C .\ARIEC61850 fetch --quiet --depth 1 origin $env:ENGINE_COMMIT - git -C .\ARIEC61850 checkout --quiet --detach $env:ENGINE_COMMIT - $actual = (git -C .\ARIEC61850 rev-parse HEAD).Trim() - if ($actual -ne $env:ENGINE_COMMIT) { - throw "Engine SHA mismatch. Expected $env:ENGINE_COMMIT, got $actual." - } - - - name: Verify discovery and SCL anti-regression contracts - shell: pwsh - run: | - $capture = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryCapture.cs -Raw - $sclAssisted = Get-Content .\ARSAS\Services\NativeIec61850Client.SclAssisted.cs -Raw - $canonicalModel = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\LiveIedCanonicalModel.cs -Raw - $typeHierarchy = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\LiveIedVariableTypeHierarchy.cs -Raw - $canonicalExporter = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\CanonicalLiveIedSclExporter.cs -Raw - $targets = Get-Content .\ARSAS\Directory.Build.targets -Raw - $smart = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartDiscovery.cs -Raw - $singleFlight = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartDiscoverySingleFlight.cs -Raw - $smartGva = Get-Content .\ARIEC61850\src\AR.Iec61850\Mms\MmsClientSession.SmartVariableAccessAttributes.cs -Raw - $lnParts = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\Iec61850ReferenceParts.cs -Raw - $exporter = Get-Content .\ARIEC61850\src\AR.Iec61850\Scl\Export\LiveIedSclExporter.cs -Raw - $registry = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\Iec61850StandardModelRegistry.cs -Raw - $cdc = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\CdcInferenceEngine.cs -Raw - - $valueCacheIndex = $sclAssisted.IndexOf('_trustedSclInitialValues =', [System.StringComparison]::Ordinal) - if ($valueCacheIndex -lt 0) { - throw 'P2 trusted-SCL value cache declaration is missing.' - } - $valueCacheLength = [Math]::Min(260, $sclAssisted.Length - $valueCacheIndex) - $valueCacheSegment = $sclAssisted.Substring($valueCacheIndex, $valueCacheLength) - if ($valueCacheSegment -notmatch 'StringComparer\.Ordinal' -or - $valueCacheSegment -match 'StringComparer\.OrdinalIgnoreCase' -or - $sclAssisted -notmatch 'projectedUniqueValues' -or - $sclAssisted -notmatch 'initialValueCacheLoss' -or - $sclAssisted -notmatch 'cacheLoss=\{initialValueCacheLoss\}' -or - $canonicalModel -notmatch 'new HashSet\(StringComparer\.Ordinal\)' -or - $typeHierarchy -notmatch 'current\.Children\[index\]\.Name, part, StringComparison\.Ordinal\)' -or - $canonicalExporter -notmatch 'dataObjectName\.Trim\(\),\s*[\s\S]*?StringComparison\.Ordinal\)') { - throw 'P2 case-sensitive value path implementation regressed.' - } - - if ($targets -notmatch 'VerifyPhysicalProvenSmartDiscoveryRoute' -or - $targets -notmatch '\-VerifyOnly' -or - $targets -match 'GITHUB_WORKFLOW' -or - $targets -match 'SmartDiscoveryProductionPromoted' -or - $capture -notmatch 'SmartDiscoveryStructuralFreezeContract = "P0-R9-STRUCTURAL"' -or - $capture -notmatch 'CreateP0FrozenSmartDiscoveryOptions' -or - $capture -notmatch 'MaxConcurrentChains = 8' -or - $capture -notmatch 'UnknownPeerMaxConcurrentChains = 4' -or - $capture -notmatch 'MaxNameListPages = 64' -or - $capture -notmatch 'ProbeReportAttributes = true' -or - $capture -notmatch 'MaxReportAttributeProbes = 64' -or - $capture -notmatch 'ReadDataSetDirectories = true' -or - $capture -notmatch 'MaxDataSetDirectoryReads = 64' -or - $capture -notmatch 'GetOrCreateSmartDiscoveryAssociationFlight' -or - $capture -notmatch 'DiscoverSmartSingleFlightAsync' -or - $capture -notmatch 'ProbeSmartAsync' -or - $capture -notmatch 'initialFcRoots=deferred' -or - $capture -notmatch 'InitialFcReadExecutionResult\? initialRead = null' -or - $capture -match '_session\.DiscoverAsync\(' -or - $capture -match 'DiscoverDomainVariableNamesAsync' -or - $capture -match 'TryBuildSupplementalGetNameListSnapshotAsync' -or - $capture -match 'DiscoverDomainVariableTypeTreeNamesAsync' -or - $capture -match 'AddAdaptiveLogicalNodeSiblingProbeSignalsAsync' -or - $capture -match 'EnrichEngineeringUnitsAsync' -or - $capture -match 'InitialFcReadPlanner\.FromSclModel' -or - $capture -match 'ExecuteInitialFcReadPlanSmartAsync' -or - $smart -notmatch 'DiscoverSmartAsync' -or - $singleFlight -notmatch 'DiscoverSmartSingleFlightAsync' -or - $singleFlight -notmatch 'DiscoverSmartAsync\(options, CancellationToken\.None\)' -or - $smartGva -notmatch 'GetVariableAccessAttributesSmartAsync') { - throw 'P0 structural discovery freeze regressed away from the accepted IEDScout convergence path.' - } - - if ($lnParts -notmatch 'instanceStart' -or - $lnParts -notmatch 'IsFourLetterLnClass' -or - $exporter -notmatch 'TryResolveStandardSubDataObjectCdc' -or - $exporter -notmatch 'DataObjectReferencePaths' -or - $exporter -notmatch 'IsEdition2ServiceTrackingCdc' -or - $registry -notmatch 'Key\("TCTR", "ARtg"\)' -or - $registry -notmatch 'Key\("TVTR", "VRtg"\)' -or - $registry -notmatch 'Key\("LTIM", "TmChgDT"\)' -or - $registry -notmatch 'Key\("LTRK", "BrcbTrk"\)' -or - $registry -notmatch 'Key\("XCBR", "EEName"\)' -or - $registry -notmatch 'Key\("LLN0", "MltLev"\)' -or - $cdc -notmatch '"CST".*"BTS".*"UTS".*"STS".*"CTS"') { - throw 'Canonical IEC model / SCL semantic authority regressed.' - } - - Write-Host 'IEDScout convergence contract PASS.' From b26450243f2b5735b1329007b145d7634ed68190 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:44:58 +0700 Subject: [PATCH 07/19] refactor(ci): consume neutral interoperability reference evidence --- .github/workflows/scl-interoperability-r7.yml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/scl-interoperability-r7.yml b/.github/workflows/scl-interoperability-r7.yml index b7d06d0a6..9a349a2ca 100644 --- a/.github/workflows/scl-interoperability-r7.yml +++ b/.github/workflows/scl-interoperability-r7.yml @@ -14,8 +14,8 @@ on: - "MainWindow.xaml.cs" - "Services/CanonicalSclReloadValidator.cs" - "engines/ARIEC61850.lock.json" - - "evidence/iedscout-convergence-target.json" - - "docs/IEDSCOUT_CONVERGENCE.md" + - "evidence/interoperability-reference-target.json" + - "docs/INTEROPERABILITY_REFERENCE_CONTRACT.md" - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" - "tests/ARSAS.Tests/CanonicalSclReloadValidatorTests.cs" - ".github/workflows/scl-interoperability-r7.yml" @@ -52,7 +52,7 @@ jobs: shell: pwsh run: | $lock = Get-Content .\ARSAS\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json - $convergence = Get-Content .\ARSAS\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $convergence = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json if ($lock.repository -ne 'masarray/ARIEC61850' -or $lock.commit -notmatch '^[0-9a-f]{40}$') { throw 'Invalid ARIEC61850 R7 lock.' @@ -71,7 +71,7 @@ jobs: [bool]$convergence.promotion.physicalRetestRequired -or -not [bool]$convergence.promotion.mergeAllowedAfterPhysicalRetest -or -not [bool]$convergence.promotion.physicalRetestPassed) { - throw 'IEDScout convergence single-source-of-truth does not match the physically proven merged-engine + ARSAS #324 authority.' + throw 'external IEC 61850 reference convergence single-source-of-truth does not match the physically proven merged-engine + ARSAS #324 authority.' } if ([int]$lock.sourcePullRequest -notin @(134, 135)) { throw "Unexpected R7 engine source PR: $($lock.sourcePullRequest)" @@ -111,7 +111,7 @@ jobs: $cdcInference = Get-Content .\ARIEC61850\src\AR.Iec61850\Discovery\CdcInferenceEngine.cs -Raw $consumer = Get-Content .\ARSAS\Services\NativeIec61850Client.CanonicalModel.cs -Raw $capture = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryCapture.cs -Raw - $convergence = Get-Content .\ARSAS\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $convergence = Get-Content .\ARSAS\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json $buildTargets = Get-Content .\ARSAS\Directory.Build.targets -Raw $lifecycle = Get-Content .\ARSAS\Services\NativeIec61850Client.SmartDiscoveryLifecycle.cs -Raw $sclClient = Get-Content .\ARSAS\Services\NativeIec61850Client.SclAssisted.cs -Raw @@ -175,16 +175,16 @@ jobs: $capture -match 'DiscoverDomainVariableTypeTreeNamesAsync' -or $capture -match 'AddAdaptiveLogicalNodeSiblingProbeSignalsAsync' -or $capture -match 'EnrichEngineeringUnitsAsync') { - throw 'IEDScout convergence source contract regressed: smart structure-first discovery or semantic SCL authority is missing, or a forbidden legacy browse path returned.' + throw 'external IEC 61850 reference convergence source contract regressed: smart structure-first discovery or semantic SCL authority is missing, or a forbidden legacy browse path returned.' } - if ([int]$convergence.physicalReference.iedScoutCapture.associations -ne 1 -or + if ([int]$convergence.physicalReference.externalReferenceCapture.associations -ne 1 -or [int]$convergence.physicalReference.rejectedLegacyArsasCapture.associations -ne 2 -or -not [bool]$convergence.discoveryAcceptance.exactlyOneAssociation -or -not [bool]$convergence.discoveryAcceptance.supplementalLegacyAssociationForbidden -or -not [bool]$convergence.sclAcceptance.reopenInArsasRequired -or -not [bool]$convergence.sclAcceptance.physicalReconnectRequired) { - throw 'IEDScout physical acceptance target was weakened.' + throw 'external IEC 61850 reference physical acceptance target was weakened.' } if ($consumer -notmatch 'LiveIedCanonicalModelBuilder\.Build\(model, communication, initialRead\)' -or @@ -278,7 +278,7 @@ jobs: throw "Portable EXE smoke test failed: $($process.ExitCode)" } - $convergencePath = ".\ARSAS\evidence\iedscout-convergence-target.json" + $convergencePath = ".\ARSAS\evidence\interoperability-reference-target.json" $convergenceHash = (Get-FileHash $convergencePath -Algorithm SHA256).Hash.ToLowerInvariant() $convergence = Get-Content $convergencePath -Raw | ConvertFrom-Json @@ -306,8 +306,8 @@ jobs: path: | ARSAS\dist\ARSAS-*-win-x64-portable.exe ARSAS\dist\R7-SCL-INTEROP-BUILD.txt - ARSAS\evidence\iedscout-convergence-target.json - ARSAS\docs\IEDSCOUT_CONVERGENCE.md + ARSAS\evidence\interoperability-reference-target.json + ARSAS\docs\INTEROPERABILITY_REFERENCE_CONTRACT.md ARSAS\TestResults\*.trx if-no-files-found: error retention-days: 14 From 66f27a71fcdef452cd6c2e9645302d2f6cd66c9a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:45:01 +0700 Subject: [PATCH 08/19] refactor(ci): consume neutral interoperability reference evidence --- .github/workflows/smart-discovery-mainline-readiness.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smart-discovery-mainline-readiness.yml b/.github/workflows/smart-discovery-mainline-readiness.yml index f0b4f05a4..db18e8061 100644 --- a/.github/workflows/smart-discovery-mainline-readiness.yml +++ b/.github/workflows/smart-discovery-mainline-readiness.yml @@ -17,7 +17,7 @@ jobs: - name: Verify R10 physical convergence authority shell: powershell run: | - $target = Get-Content .\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $target = Get-Content .\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json $lock = Get-Content .\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json if ($target.status -ne 'physical-retest-passed-merge-ready' -or From 07b42933bb758a7720fb72a2c7cab6089b659433 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:45:04 +0700 Subject: [PATCH 09/19] refactor(ci): consume neutral interoperability reference evidence --- .github/workflows/smart-discovery-post-merge-production.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smart-discovery-post-merge-production.yml b/.github/workflows/smart-discovery-post-merge-production.yml index b74229e10..f2322c8b2 100644 --- a/.github/workflows/smart-discovery-post-merge-production.yml +++ b/.github/workflows/smart-discovery-post-merge-production.yml @@ -19,7 +19,7 @@ jobs: - name: Verify R10 physical convergence authority shell: powershell run: | - $target = Get-Content .\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $target = Get-Content .\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json $lock = Get-Content .\engines\ARIEC61850.lock.json -Raw | ConvertFrom-Json if ($target.status -ne 'physical-retest-passed-merge-ready' -or @@ -86,7 +86,7 @@ jobs: shell: powershell run: | New-Item -ItemType Directory -Force .\TestResults | Out-Null - $target = Get-Content .\evidence\iedscout-convergence-target.json -Raw | ConvertFrom-Json + $target = Get-Content .\evidence\interoperability-reference-target.json -Raw | ConvertFrom-Json [ordered]@{ schemaVersion = 1 sourceCommit = $env:GITHUB_SHA From 4cf0a96bdbbcb5a9c326dcc62ddd83306bd2aea2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:45:28 +0700 Subject: [PATCH 10/19] test(interoperability): assert neutral reference contract and unchanged physical acceptance --- .../CanonicalLiveSclExportRegressionTests.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index 0dc8afeb2..ca637bcff 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -168,7 +168,7 @@ public void SclAssistedReconnect_UsesNativeCallingIdentityAndSafeParallelValueRe [Fact] public void P0StructuralDiscoveryFreeze_LocksR9WireAndModelBudget() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"contractId\": \"P0-R9-STRUCTURAL\"", contract, StringComparison.Ordinal); Assert.Contains("\"status\": \"implemented-and-r9-physically-proven\"", contract, StringComparison.Ordinal); @@ -195,7 +195,7 @@ public void P0StructuralDiscoveryFreeze_LocksR9WireAndModelBudget() [Fact] public void P1ProjectionOrderRepair_LocksPhysicalRootCauseAndExactStrategy() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"contractId\": \"P1-CF-DO-SCOPED\"", contract, StringComparison.Ordinal); Assert.Contains("\"status\": \"physically-proven-r10\"", contract, StringComparison.Ordinal); @@ -213,7 +213,7 @@ public void P1ProjectionOrderRepair_LocksPhysicalRootCauseAndExactStrategy() [Fact] public void P2CaseSensitiveValuePipeline_LocksLosslessExactPathIdentity() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); var client = File.ReadAllText(FindRepoFile("Services/NativeIec61850Client.SclAssisted.cs")); Assert.Contains("\"contractId\": \"P2-CASE-EXACT-VALUES\"", contract, StringComparison.Ordinal); @@ -239,7 +239,7 @@ public void P2CaseSensitiveValuePipeline_LocksLosslessExactPathIdentity() [Fact] public void R9PhysicalReuse_LocksWorkingPathAndKeepsSemanticProjectionGapOpen() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"confirmedMmsRequests\": 323", contract, StringComparison.Ordinal); Assert.Contains("\"getVariableAccessAttributes\": 119", contract, StringComparison.Ordinal); @@ -271,7 +271,7 @@ public void R9PhysicalReuse_LocksWorkingPathAndKeepsSemanticProjectionGapOpen() [Fact] public void R10PhysicalReuse_ClosesP1P2AndLocksReportBackedRoundTrip() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); Assert.Contains("\"arsasR10\"", contract, StringComparison.Ordinal); Assert.Contains("\"initialTargets\": 709", contract, StringComparison.Ordinal); @@ -345,18 +345,18 @@ public void R7Workflow_BindsArtifactToExactSourceHeadAndReloadContracts() Assert.Contains("-VerifyOnly", buildTargets, StringComparison.Ordinal); Assert.DoesNotContain("GITHUB_WORKFLOW", buildTargets, StringComparison.Ordinal); Assert.DoesNotContain("SmartDiscoveryProductionPromoted", buildTargets, StringComparison.Ordinal); - Assert.Contains("evidence/iedscout-convergence-target.json", workflow, StringComparison.Ordinal); - Assert.Contains("IEDScout convergence source contract regressed", workflow, StringComparison.Ordinal); + Assert.Contains("evidence/interoperability-reference-target.json", workflow, StringComparison.Ordinal); + Assert.Contains("external IEC 61850 reference convergence source contract regressed", workflow, StringComparison.Ordinal); Assert.Contains("TryResolveStandardSubDataObjectCdc", workflow, StringComparison.Ordinal); Assert.Contains("IsEdition2ServiceTrackingCdc", workflow, StringComparison.Ordinal); Assert.Contains("TryBuildSupplementalGetNameListSnapshotAsync", workflow, StringComparison.Ordinal); } [Fact] - public void IedScoutConvergenceContract_PhysicalRetestPassedAndMergeReady() + public void InteroperabilityReferenceContract_PhysicalRetestPassedAndMergeReady() { - var contract = File.ReadAllText(FindRepoFile("evidence/iedscout-convergence-target.json")); - var documentation = File.ReadAllText(FindRepoFile("docs/IEDSCOUT_CONVERGENCE.md")); + var contract = File.ReadAllText(FindRepoFile("evidence/interoperability-reference-target.json")); + var documentation = File.ReadAllText(FindRepoFile("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md")); Assert.Contains("\"status\": \"physical-retest-passed-merge-ready\"", contract, StringComparison.Ordinal); Assert.Contains("\"pullRequest\": 134", contract, StringComparison.Ordinal); @@ -376,11 +376,11 @@ public void IedScoutConvergenceContract_PhysicalRetestPassedAndMergeReady() Assert.Contains("Merged proven baseline", documentation, StringComparison.Ordinal); Assert.Contains("The field result, not test count alone", documentation, StringComparison.Ordinal); - var guard = File.ReadAllText(FindRepoFile(".github/workflows/iedscout-convergence-guard.yml")); - Assert.Contains("name: IEDScout Convergence Guard", guard, StringComparison.Ordinal); - Assert.Contains("name: iedscout-convergence-contract", guard, StringComparison.Ordinal); + var guard = File.ReadAllText(FindRepoFile(".github/workflows/interoperability-reference-guard.yml")); + Assert.Contains("name: IEC 61850 Interoperability Reference Guard", guard, StringComparison.Ordinal); + Assert.Contains("name: interoperability-reference-contract", guard, StringComparison.Ordinal); Assert.Contains("Merged engine authority must remain PR #134 + PR #135 with ARSAS #324 provenance", guard, StringComparison.Ordinal); - Assert.Contains("P0 structural discovery freeze regressed away from the accepted IEDScout convergence path", guard, StringComparison.Ordinal); + Assert.Contains("P0 structural discovery freeze regressed away from the accepted external IEC 61850 reference convergence path", guard, StringComparison.Ordinal); Assert.Contains("Canonical IEC model / SCL semantic authority regressed", guard, StringComparison.Ordinal); } @@ -390,8 +390,8 @@ public void SourceClean_GuardsApprovedFirstPartyConvergenceAuthorities() var source = File.ReadAllText(FindRepoFile("scripts/verify-source-clean.ps1")); Assert.Contains("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal); - Assert.Contains("docs/IEDSCOUT_CONVERGENCE.md", source, StringComparison.Ordinal); - Assert.Contains("evidence/iedscout-convergence-target.json", source, StringComparison.Ordinal); + Assert.Contains("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md", source, StringComparison.Ordinal); + Assert.Contains("evidence/interoperability-reference-target.json", source, StringComparison.Ordinal); Assert.Contains("CanonicalLiveSclExportRegressionTests.cs", source, StringComparison.Ordinal); Assert.Contains("identifierScanExempt", source, StringComparison.Ordinal); } From bc9399464c1b33dd6e85a5c983bb85c75c57a487 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:45:46 +0700 Subject: [PATCH 11/19] chore(provenance): remove retired name exceptions and neutralize synthetic fixture copy --- scripts/verify-source-clean.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/verify-source-clean.ps1 b/scripts/verify-source-clean.ps1 index 3f1782a88..3c0fd91e3 100644 --- a/scripts/verify-source-clean.ps1 +++ b/scripts/verify-source-clean.ps1 @@ -60,12 +60,9 @@ $TextExtensions = @( # external interoperability label so the acceptance contract remains discoverable. $ApprovedConvergenceIdentifierPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) @( - ".github/workflows/iedscout-convergence-guard.yml", ".github/workflows/smart-discovery-post-merge-production.yml", ".github/workflows/smart-discovery-mainline-readiness.yml", ".github/workflows/scl-interoperability-r7.yml", - "docs/IEDSCOUT_CONVERGENCE.md", - "evidence/iedscout-convergence-target.json", "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" ) | ForEach-Object { [void]$ApprovedConvergenceIdentifierPaths.Add($_) } From 6b7c608a7f129d0b1456e12f69c91ffda3026705 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:45:49 +0700 Subject: [PATCH 12/19] chore(provenance): remove retired name exceptions and neutralize synthetic fixture copy --- docs/FAT_V2_IMPLEMENTATION_PLAN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAT_V2_IMPLEMENTATION_PLAN.md b/docs/FAT_V2_IMPLEMENTATION_PLAN.md index 2db841587..401128781 100644 --- a/docs/FAT_V2_IMPLEMENTATION_PLAN.md +++ b/docs/FAT_V2_IMPLEMENTATION_PLAN.md @@ -93,7 +93,7 @@ Required regressions: - identical duplicate source collapses safely; - conflicting IED/AP sources block; - fingerprint is order-independent; -- Siemens-like 36 ST + 22 MX fixture remains 58/58. +- Synthetic cross-LD 36 ST + 22 MX fixture remains 58/58. ### P4 — FAT workspace UX From 29963a92a6e74e2a7d40735aff76a90dea195c9d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:46:36 +0700 Subject: [PATCH 13/19] test(interoperability): lock neutral schema to original physical proof and immutable provenance --- ...abilityReferenceEvidenceRegressionTests.cs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs diff --git a/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs b/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs new file mode 100644 index 000000000..fa6946b01 --- /dev/null +++ b/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs @@ -0,0 +1,74 @@ +using System.Text.Json; + +namespace ARSAS.Tests; + +public sealed class InteroperabilityReferenceEvidenceRegressionTests +{ + [Fact] + public void NeutralAuthority_PreservesOriginalPhysicalAcceptanceMetrics() + { + using var document = JsonDocument.Parse( + File.ReadAllText(FindRepositoryFile("evidence/interoperability-reference-target.json"))); + var root = document.RootElement; + var reference = root.GetProperty("physicalReference"); + var capture = reference.GetProperty("externalReferenceCapture"); + var scl = reference.GetProperty("externalSclReference"); + + Assert.Equal(32, reference.GetProperty("logicalDevices").GetInt32()); + Assert.Equal(119, reference.GetProperty("logicalNodes").GetInt32()); + Assert.Equal(2, reference.GetProperty("dataSets").GetInt32()); + Assert.Equal(1, capture.GetProperty("associations").GetInt32()); + Assert.Equal(417, capture.GetProperty("confirmedMmsRequests").GetInt32()); + Assert.Equal(119, capture.GetProperty("getVariableAccessAttributes").GetInt32()); + Assert.Equal(156, capture.GetProperty("reads").GetInt32()); + Assert.Equal(58, scl.GetProperty("fcda").GetInt32()); + + var rejected = reference.GetProperty("rejectedLegacyArsasCapture"); + Assert.Equal(2, rejected.GetProperty("associations").GetInt32()); + Assert.Equal(30552, rejected.GetProperty("confirmedMmsRequests").GetInt32()); + + var r10 = root.GetProperty("physicalEvidence").GetProperty("arsasR10"); + foreach (var edition in new[] { "edition1", "edition2" }) + { + var evidence = r10.GetProperty(edition); + Assert.Equal(58, evidence.GetProperty("staticMembers").GetInt32()); + Assert.Equal(58, evidence.GetProperty("reportBackedRuntimePoints").GetInt32()); + Assert.Equal(0, evidence.GetProperty("unresolvedRuntimePoints").GetInt32()); + Assert.Equal(0, evidence.GetProperty("projectionErrors").GetInt32()); + Assert.Equal(0, evidence.GetProperty("cyclicMmsProcessPolling").GetInt32()); + Assert.True(evidence.GetProperty("actualInformationReportObserved").GetBoolean()); + } + } + + [Fact] + public void ActiveReferenceAndOriginalHistoricalProvenance_AreBothDiscoverable() + { + var documentation = File.ReadAllText( + FindRepositoryFile("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md")); + var workflow = File.ReadAllText( + FindRepositoryFile(".github/workflows/interoperability-reference-guard.yml")); + var sourceClean = File.ReadAllText(FindRepositoryFile("scripts/verify-source-clean.ps1")); + + Assert.Contains("36a4b87a3c2f6d34f73fa36c8a8a6a59763bd435", documentation, StringComparison.Ordinal); + Assert.Contains("a853fd0ea115b320a648b1b2a52fe9a7f6af94cd", documentation, StringComparison.Ordinal); + Assert.Contains("name: interoperability-reference-contract", workflow, StringComparison.Ordinal); + Assert.Contains("externalReferenceCapture", workflow, StringComparison.Ordinal); + Assert.Contains("evidence/interoperability-reference-target.json", workflow, StringComparison.Ordinal); + Assert.Contains("evidence/interoperability-reference-target.json", documentation, StringComparison.Ordinal); + Assert.Contains("ApprovedConvergenceIdentifierPaths", sourceClean, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string path) + { + DirectoryInfo? root = new(AppContext.BaseDirectory); + while (root is not null) + { + var candidate = Path.Combine(root.FullName, path); + if (File.Exists(candidate)) + return candidate; + root = root.Parent; + } + + throw new FileNotFoundException(path); + } +} From 3796d466aa46c12a0b20b9404b1201419863eea9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:50:58 +0700 Subject: [PATCH 14/19] test(provenance): align neutral contract checks with actual R10 schema and narrowed exclusions --- .../InteroperabilityReferenceEvidenceRegressionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs b/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs index fa6946b01..58443173f 100644 --- a/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs +++ b/tests/ARSAS.Tests/InteroperabilityReferenceEvidenceRegressionTests.cs @@ -28,7 +28,7 @@ public void NeutralAuthority_PreservesOriginalPhysicalAcceptanceMetrics() Assert.Equal(30552, rejected.GetProperty("confirmedMmsRequests").GetInt32()); var r10 = root.GetProperty("physicalEvidence").GetProperty("arsasR10"); - foreach (var edition in new[] { "edition1", "edition2" }) + foreach (var edition in new[] { "reuseEdition1", "reuseEdition2" }) { var evidence = r10.GetProperty(edition); Assert.Equal(58, evidence.GetProperty("staticMembers").GetInt32()); From d769fdbcda33bf9d86bc7e1aa7d6a861ddd5fe02 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:51:02 +0700 Subject: [PATCH 15/19] test(provenance): align neutral contract checks with actual R10 schema and narrowed exclusions --- tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index ca637bcff..b5d7c170d 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -385,13 +385,13 @@ public void InteroperabilityReferenceContract_PhysicalRetestPassedAndMergeReady( } [Fact] - public void SourceClean_GuardsApprovedFirstPartyConvergenceAuthorities() + public void SourceClean_DoesNotExemptNeutralReferenceEvidence() { var source = File.ReadAllText(FindRepoFile("scripts/verify-source-clean.ps1")); Assert.Contains("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal); - Assert.Contains("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md", source, StringComparison.Ordinal); - Assert.Contains("evidence/interoperability-reference-target.json", source, StringComparison.Ordinal); + Assert.DoesNotContain("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md", source, StringComparison.Ordinal); + Assert.DoesNotContain("evidence/interoperability-reference-target.json", source, StringComparison.Ordinal); Assert.Contains("CanonicalLiveSclExportRegressionTests.cs", source, StringComparison.Ordinal); Assert.Contains("identifierScanExempt", source, StringComparison.Ordinal); } From d6076687e730dc5a3c0f5c2f56710be6748f4633 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:58:01 +0700 Subject: [PATCH 16/19] fix(source-clean): scan every tracked file with no whole-file identifier exemptions --- scripts/verify-source-clean.ps1 | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/scripts/verify-source-clean.ps1 b/scripts/verify-source-clean.ps1 index 3c0fd91e3..6cb2fee20 100644 --- a/scripts/verify-source-clean.ps1 +++ b/scripts/verify-source-clean.ps1 @@ -12,10 +12,17 @@ publish or repeat unrelated product and company names. #> [CmdletBinding()] -param() +param( + [string]$RepositoryRoot, + [switch]$ScanOnly +) $ErrorActionPreference = "Stop" -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$RepoRoot = if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) { + (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +} else { + (Resolve-Path -LiteralPath $RepositoryRoot).Path +} $ForbiddenFilePatterns = @( "LICENSE-APACHE-2.0", @@ -56,16 +63,8 @@ $TextExtensions = @( ".props", ".targets", ".sln", ".slnx", ".txt" ) -# These are first-party convergence authorities. They intentionally contain the -# external interoperability label so the acceptance contract remains discoverable. -$ApprovedConvergenceIdentifierPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) -@( - ".github/workflows/smart-discovery-post-merge-production.yml", - ".github/workflows/smart-discovery-mainline-readiness.yml", - ".github/workflows/scl-interoperability-r7.yml", - "tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs" -) | ForEach-Object { [void]$ApprovedConvergenceIdentifierPaths.Add($_) } - +# No tracked path receives a whole-file external-identifier exemption. Historical +# comparison evidence is linked by immutable commit rather than copied into active files. $Problems = New-Object System.Collections.Generic.List[string] function Normalize-RelativePath { @@ -135,8 +134,7 @@ foreach ($relative in (Get-TrackedRelativePaths)) { } } - $identifierScanExempt = $ApprovedConvergenceIdentifierPaths.Contains($relative) - if (-not $identifierScanExempt -and (Test-ContainsForbiddenIdentifier $relative)) { + if (Test-ContainsForbiddenIdentifier $relative) { $Problems.Add("Forbidden external identifier in path: $relative") } @@ -144,7 +142,7 @@ foreach ($relative in (Get-TrackedRelativePaths)) { if ($TextExtensions -notcontains [IO.Path]::GetExtension($relative).ToLowerInvariant()) { continue } $content = Get-Content -LiteralPath $fullPath -Raw -ErrorAction SilentlyContinue - if (-not $identifierScanExempt -and (Test-ContainsForbiddenIdentifier $content)) { + if (Test-ContainsForbiddenIdentifier $content) { $Problems.Add("Forbidden external identifier in text: $relative") } @@ -162,7 +160,9 @@ if ($Problems.Count -gt 0) { throw "ARSAS source tree failed clean-room validation with $($Problems.Count) problem(s)." } -& (Join-Path $PSScriptRoot "verify-fault-record-bindings.ps1") -& (Join-Path $PSScriptRoot "verify-auto-update.ps1") +if (-not $ScanOnly) { + & (Join-Path $PSScriptRoot "verify-fault-record-bindings.ps1") + & (Join-Path $PSScriptRoot "verify-auto-update.ps1") +} -Write-Host "All Git-tracked ARSAS content passed source, website, external-IP, current-license, binding, and updater checks." -ForegroundColor Green +Write-Host "All Git-tracked ARSAS content passed source and external-identifier checks." -ForegroundColor Green From 22f2919055d872fde19804b35d82e245504752ec Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:58:34 +0700 Subject: [PATCH 17/19] test(source-clean): exercise forbidden identifier rejection across all tracked surfaces --- scripts/test-source-clean-guard.ps1 | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 scripts/test-source-clean-guard.ps1 diff --git a/scripts/test-source-clean-guard.ps1 b/scripts/test-source-clean-guard.ps1 new file mode 100644 index 000000000..1b93f4a7a --- /dev/null +++ b/scripts/test-source-clean-guard.ps1 @@ -0,0 +1,61 @@ +# Copyright 2026 Ari Sulistiono +# SPDX-License-Identifier: GPL-3.0-or-later +<# +Tests the complete source-clean scanner against temporary Git-tracked fixtures. +Construct the known test identifier from code points so this test file itself +does not need an exemption from the same clean-room gate. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +$scanner = Join-Path $PSScriptRoot "verify-source-clean.ps1" +$identifier = -join (@(73, 69, 68, 83, 99, 111, 117, 116) | ForEach-Object { [char]$_ }) +$cases = @( + @{ Path = "Services/Fixture.cs"; Text = "public sealed class ${identifier}Fixture {}"; Expected = "text" }, + @{ Path = "docs/reference.md"; Text = "# $identifier"; Expected = "text" }, + @{ Path = "evidence/fixture.json"; Text = "{`"reference`": `"$identifier`"}"; Expected = "text" }, + @{ Path = ".github/workflows/smart-discovery-post-merge-production.yml"; Text = "name: $identifier"; Expected = "text" }, + @{ Path = "tests/ARSAS.Tests/SyntheticFixture.cs"; Text = "// $identifier"; Expected = "text" }, + @{ Path = "docs/${identifier}-fixture.md"; Text = "# independently generated fixture"; Expected = "path" } +) + +function Invoke-Case { + param( + [Parameter(Mandatory=$true)][string]$RelativePath, + [Parameter(Mandatory=$true)][string]$Content, + [Parameter(Mandatory=$true)][bool]$MustReject, + [string]$Expected = "text" + ) + $root = Join-Path ([IO.Path]::GetTempPath()) ("arsas-clean-room-" + [guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $root -Force | Out-Null + try { + & git -C $root init --quiet + if ($LASTEXITCODE -ne 0) { throw "Fixture Git initialization failed." } + $file = Join-Path $root ($RelativePath.Replace('/', [IO.Path]::DirectorySeparatorChar)) + New-Item -ItemType Directory -Path (Split-Path $file) -Force | Out-Null + [IO.File]::WriteAllText($file, $Content, [Text.UTF8Encoding]::new($false)) + & git -C $root add --all + if ($LASTEXITCODE -ne 0) { throw "Fixture Git staging failed." } + + $output = (& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $scanner -RepositoryRoot $root -ScanOnly 2>&1 | Out-String) + $exitCode = $LASTEXITCODE + if ($MustReject) { + if ($exitCode -eq 0 -or $output -notmatch ("Forbidden external identifier in " + $Expected)) { + throw "Source-clean unexpectedly accepted a forbidden $Expected fixture: $RelativePath; exit=$exitCode; output=$output" + } + } + elseif ($exitCode -ne 0) { + throw "Source-clean rejected a neutral fixture: $RelativePath; exit=$exitCode; output=$output" + } + } + finally { + Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue + } +} + +foreach ($case in $cases) { + Invoke-Case -RelativePath $case.Path -Content $case.Text -MustReject $true -Expected $case.Expected +} +Invoke-Case -RelativePath "docs/synthetic-reference.md" -Content "# ARSAS independent IEC 61850 synthetic evidence" -MustReject $false +Write-Host "Source-clean negative and positive fixture tests PASS." -ForegroundColor Green From 319e21376556b2b0f27800e076c3f7083a666cd0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:58:55 +0700 Subject: [PATCH 18/19] test(ci): run end-to-end source-clean negative fixtures on every Windows build --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3183adef..e537c2703 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,6 +51,10 @@ jobs: shell: powershell run: .\ArIED61850Tester\scripts\verify-source-clean.ps1 + - name: Verify source-clean rejection fixtures + shell: powershell + run: .\ArIED61850Tester\scripts\test-source-clean-guard.ps1 + - name: Verify premium UX, GOOSE, SMV, SAS and release invariants shell: powershell run: | From df32669679d3e128d0b5decbf06077838432f633 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 24 Sep 2026 12:59:17 +0700 Subject: [PATCH 19/19] test(source-clean): assert whole-file exceptions are removed and negative fixtures run --- .../CanonicalLiveSclExportRegressionTests.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs index b5d7c170d..745cf9c02 100644 --- a/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs +++ b/tests/ARSAS.Tests/CanonicalLiveSclExportRegressionTests.cs @@ -385,15 +385,18 @@ public void InteroperabilityReferenceContract_PhysicalRetestPassedAndMergeReady( } [Fact] - public void SourceClean_DoesNotExemptNeutralReferenceEvidence() + public void SourceClean_GuardsEveryTrackedFileWithoutWholeFileExceptions() { var source = File.ReadAllText(FindRepoFile("scripts/verify-source-clean.ps1")); - - Assert.Contains("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal); - Assert.DoesNotContain("docs/INTEROPERABILITY_REFERENCE_CONTRACT.md", source, StringComparison.Ordinal); - Assert.DoesNotContain("evidence/interoperability-reference-target.json", source, StringComparison.Ordinal); - Assert.Contains("CanonicalLiveSclExportRegressionTests.cs", source, StringComparison.Ordinal); - Assert.Contains("identifierScanExempt", source, StringComparison.Ordinal); + var build = File.ReadAllText(FindRepoFile(".github/workflows/build.yml")); + + Assert.DoesNotContain("$ApprovedConvergenceIdentifierPaths", source, StringComparison.Ordinal); + Assert.DoesNotContain("identifierScanExempt", source, StringComparison.Ordinal); + Assert.Contains("if (Test-ContainsForbiddenIdentifier $relative)", source, StringComparison.Ordinal); + Assert.Contains("if (Test-ContainsForbiddenIdentifier $content)", source, StringComparison.Ordinal); + Assert.Contains("test-source-clean-guard.ps1", build, StringComparison.Ordinal); + Assert.Contains("RepositoryRoot", source, StringComparison.Ordinal); + Assert.Contains("ScanOnly", source, StringComparison.Ordinal); }