Skip to content

Report PA-DIM engineering metadata changes - #5029

Draft
marcschier wants to merge 22 commits into
masterfrom
marcschier-pa-dim-master-gap
Draft

marcschier wants to merge 22 commits into
masterfrom
marcschier-pa-dim-master-gap

Conversation

@marcschier

@marcschier marcschier commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Describe the changes here to communicate to the maintainers why they should accept this pull request. By default - this will become the Commit message after merging and thus define history.

PA-DIM analog bindings copy engineering metadata before replacing either published property. Caller mutations to EUInformation, Range, translation dictionaries, or supported formatting arguments do not alter retained metadata. Unsupported copy inputs are rejected before either property changes. Registered EngineeringUnits and EURange changes use existing semantic-event and monitored-value status reporting; initial configuration remains unchanged.

The PA-DIM-local comparator checks retained translations, selected text, fallback formatting and argument contents without changing global LocalizedText.Equals. Dictionary reorders are equivalent only when they preserve the first available locale, first case-insensitive full-locale match, first locale for each language, and retained fallback information. This includes ordinal dictionaries with distinct en-GB and en-gb entries. Undecodable mul text still uses selected-text equality; encoded-entry reordering can change the first-entry fallback. LocalizedText.Copy() retains enumeration order.

The user approved extending shared localization, multidimensional values, and the shared envelope-clone correction. ResourceManager.TranslateValue selects EUInformation.DisplayName and Description for exact scalars and elements of mixed ExtensionObject arrays/matrices. Translated copies preserve original TypeIds and dimensions without changing stored metadata. Null-ID, unsupported and derived structures remain unchanged. A shared internal routing check preserves legacy LocalizedText routing without newly invoking custom overrides for unrelated structures. This is not a general nested-structure visitor.

Hosted mixed-container regressions exposed CoreUtils.Clone(ExtensionObject) normalizing envelope IDs through its body-only constructor. The successor uses an internal copy constructor that retains the original ID, including null, while preserving the previous decoded-body Clone() ownership behavior and retaining undecoded raw bodies. Existing array/matrix Variant construction reuses this shared clone path; no public API or obsolete production calls were added.

Current published head: 92860d417b53dd3bdfec632950b3de9a09ccb3c5, based on integration master f8aa7a62f42b8664f64d33c015f69c0fc0da6e51. Source publication was explicitly authorized. Earlier successors preserve envelopes (a59150199), add matrices/narrow routing (1c57ac313), compare language/default fallback ordering (02ef75b05) and case-insensitive full-locale ordering (fb3156d3f). docs/PADIM.md documents the contracts; only the PA-DIM section of docs/Profiles.md changes. Earlier compile corrections use explicit TryGetStructure<EUInformation>(out EUInformation? decoded) to avoid CS8631 and scalar/array CS0121 ambiguity. This PR stays draft; current-head hosted evidence includes candidate analyzer failures, and matched coverage acceptance remains pending.

Related Issues

Reference all GitHub issues this PR addresses. If there is no issue yet, open one and link it here.

If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR)).

Checklist

Put an x in the boxes that apply. You can complete these step by step after opening the PR.

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings.
  • If this PR changes C# source files, I ran all tests locally using the UA.slnx solution against at least .NET Framework and .NET 10, and all passed. PRs changing only non-C# files do not require these .NET test runs.
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

Validation

Local validation is suspended. The ISA-95 run retained the shared hold after an identity-mismatch cleanup uncertainty. The coordinator subsequently reported that the hold file was unexpectedly absent; the global safety hold remains in force regardless. Do not recreate, remove or alter any hold file. No local validation, restore/build/test, semantic/metadata or parser check, git apply --check, git diff --check, formatter, analyzer or gate admission is permitted until explicit clearance. opcua-contained-gate-v1.ps1 is suspended pending separate requalification; clearing the hold alone does not authorize reuse. Earlier PA-DIM gate evidence is historical. No hold was changed and no shared process was stopped by this follow-up. Hosted/source-publication permissions do not lift these restrictions.

Current head: partial hosted evidence, analyzer failures

CI 42615773871 targets 92860d417b53dd3bdfec632950b3de9a09ccb3c5. Its initial observed status was pending with null conclusion; later captured evidence contains completed failures and incomplete jobs. It is not a passing full-run verdict. The user separately authorized monitoring this exact successor at five-minute intervals through completion, inspecting completed artifacts, and updating this evidence. No support-rating or coverage-nondecrease conclusion is established.

Canonical current-head reports retained under D:\temp\padim-hosted-92860d417:

Matrix PA-DIM job Artifact Canonical PA-DIM TRX
Windows net48 129161532916 13676756964 394 passed, 0 failed, 0 skipped
Windows net10 129161533447 13675099777 394 passed, 0 failed, 0 skipped
Linux net10 129161533884 13675931913 394 passed, 0 failed, 0 skipped

All ten multilingual cases pass in those reports. All three TranslateValueSelectsEngineeringUnitsInMixedArray(1/2/3) cases also pass in each completed server report: Windows net48 server-other job 129161533114, artifact 13676730644, 1,127 passed/0 failed; Windows net10 artifact 13678480055, 11,018 passed/0 failed, total 11,123; Linux net10 artifact 13676112175, 10,896 passed/0 failed, total 11,005. The three predecessor envelope regressions are no longer failing in those reports; this does not establish a passing full run.

Candidate-caused build failures: CA1861 at CoreUtilsTests.cs:359-360 flags the two constant dimension arrays passed to Is.EqualTo. All seven build-UA jobs fail: Linux net8 Release 129161530597, Linux net9 Release 129161530598, Linux net10 Release 129161530616, Windows net48 Debug 129161530622, Windows net10 Release 129161530623, Windows net48 Release 129161530635, Windows net10 Debug 129161530792. Types batches 15 fail compilation on Windows net48 129161533169, Windows net10 129161533763, and Linux net10 129161540892. The new direct clone regressions therefore have no execution verdict. An uncommitted source-only correction uses one private static readonly expected [1, 2] shape and independently checks the input, cloned and Variant-wrapped dimensions against it. It has not been validated, committed or pushed; another publication/run requires user consent.

Windows net48 Client job 129161532881, artifact 13677750637, reports 3,778 passed/1 failed, total 3,784 and executed 3,779. Exact failed case: TryCompleteTransferAsyncShouldUseKnownServerIdsWhenResponseWrong1Async, duration 00:00:00.0270000, System.AccessViolationException. The retained stack names Subscription.DeleteCoreAsync line 1505, MessageProcessor.ResetMessageGenerationAsync line 1397, Subscription.StateManagerAsync line 1427, DisposeAsync lines 841/852/858, and test line 2541. Source line 1505 is the catch boundary during subscription deletion; the stack does not identify a corrupting operation. The test reaches disposal after the transfer assertions. This differs from the predecessor reverse-connect timeout. Neither a candidate cause nor a baseline reproduction is established; no speculative Client patch is included.

Finished Linux/Windows net10 PA-DIM-project coverage at current head, using exact package/class-line filters: Client 170/202 lines and 95/116 branches; Server 1555/1686 lines and 868/1034 branches. These are single-project reports, not matched aggregate current-head qualification.

Prepared regression evidence:

Behavior Test and evidence
Translation/fallback changes, equal arguments, encoded-mul changes and equivalent reorder controls Six original HostedEngineeringUnitsHonorsPreferredLocalesAndReportsTranslationChanges cases passed on current head in Windows48/Windows10/Linux10 reports.
Changed same-language/unmatched-locale fallback after copying, plus equivalent reorder Same test, en-AU DisplayName/Description cases passed on current head in all three reports.
First case-insensitive full-locale match with distinct en-GB/en-gb, plus equivalent reorder Same test, EN-gb DisplayName/Description cases passed on current head in all three reports.
Nested unit translation, alternate scalar IDs and stored metadata TranslateValueSelectsEngineeringUnitTexts; included in completed passing server reports.
Mixed structures, alternate IDs, rank 1/2/3 shape and source preservation All three TranslateValueSelectsEngineeringUnitsInMixedArray cases pass on current head in Windows48/Windows10/Linux10 reports.
Empty locales/containers, unrelated/derived structures, unsupported/null-ID envelopes TranslateValuePreservesUnrequestedAndUnrelatedStructures; included in completed passing server reports.
Data/event translation without unrelated custom-override calls Three PublishSelectsTranslationOfLocalizedTextValue and two PublishSelectsTranslationOfEngineeringUnitEventField cases; included in completed passing server reports.
Shared clone IDs/ownership and array/matrix Variant shape Six CloneExtensionObjectPreservesEnvelopeAndBodyOwnership cases; blocked by Types compilation failures.
Identifier-only/null/binary/JSON/XML clone representation CloneExtensionObjectPreservesEncodedBodies; blocked by Types compilation failures.

Predecessor fb3156d: PA-DIM passed, shared-server regressions failed

CI 42605987105 targets fb3156d3fc556e8824a44802e0046dd750f2b908, not current head. Initially pending with zero jobs/artifacts; later captured evidence contains completed failures and incomplete jobs. It is not a passing full-run verdict. Its watcher was stopped after successor publication. Hosted artifact parsing did not run local validation.

Matrix PA-DIM job Artifact Canonical PA-DIM TRX
Windows net48 129123124010 13669038705 394 passed, 0 failed, 0 skipped
Windows net10 129123124183 13669423677 394 passed, 0 failed, 0 skipped
Linux net10 129123124412 13669334118 394 passed, 0 failed, 0 skipped

All ten multilingual cases pass in those three reports. These results validate that predecessor's semantic/fallback controls, not the new shared clone implementation.

Windows net48 server-other job 129123124546, artifact 13669017029: 1,124 passed, 3 failed, total 1,127. Failures are TranslateValueSelectsEngineeringUnitsInMixedArray(1), (2), (3): expected alternative IDs nsu=urn:alternative;s=units and other-units, actual i=887; the null-ID unit element was also translated instead of preserved. Windows net10 server job 129123124217 repeats these same three failures (11,015 passed, 6 skipped, total 11,024); Linux net10 server job 129123124264 repeats them (10,893 passed, 10 skipped, total 10,906). Trace: array/matrix Variant constructors call CoreUtils.Clone, whose body-only envelope construction discards original IDs. The new shared correction addresses this mechanism, and these cases pass in the three current-head reports above; direct clone regressions remain blocked by compilation.

Independent Windows net48 Client job 129123123771 fails ReverseConnectFailoverUsesMemberSourceAsync(False,True,False) with TaskCanceledException after 2 minutes: 3,778 passed, 1 failed, 3 skipped. No baseline reproduction or candidate cause is established; no speculative patch is included.

Downloaded reports: D:\temp\padim-hosted-fb3156d3. Finished Linux/Windows net10 PA-DIM-project coverage at fb3156d: Client 170/202 lines and 95/116 branches; Server 1555/1686 lines and 868/1034 branches. These are predecessor single-project reports, not matched aggregate current-head qualification.

Predecessor b416307: six multilingual failures and independent Core failure

CI 42593478435 targets b416307d1ab7a778a533a30817ef6d0be408e774. At its recorded follow-up snapshot, 4 jobs failed and 51 remained incomplete; it was not passing or complete.

Three completed PA-DIM jobs each report 384 passed, 6 failed, 0 skipped, total 390: Windows net48 job 129085002307/artifact 13664720950; Windows net10 job 129085002528/artifact 13664209210; Linux net10 job 129085002750/artifact 13664077745.

All failures are HostedEngineeringUnitsHonorsPreferredLocalesAndReportsTranslationChanges:

Locale Description Observed failure
en-US false German-only update raises no semantic event and no SemanticsChanged.
en-US true German-only update raises no semantic event and no SemanticsChanged.
de-DE false Same event/status failures; initial/updated reads return en-US English/EnglishChanged instead of de-DE Deutsch/DeutschChanged.
de-DE true Same event/status failures; initial/updated reads return en-US English/EnglishChanged instead of de-DE Deutsch/DeutschChanged.
mul false German-only update raises no semantic event and no SemanticsChanged.
mul true German-only update raises no semantic event and no SemanticsChanged.

Linux TRX progress shows German event/status=False/False and default control event/status=True/True in all six. This separates dictionary change notification from nested unit locale selection. EngineeringMetadataDetachesCallerValues(False/True), EngineeringMetadataCopyFailurePreservesProperties and HostedDeviceSupportsTypedDiscoverySimulationAndGeneratedOperations pass in predecessor Linux10/Windows48 reports. Predecessor macOS PA-DIM was queued at the recorded snapshot.

Independent Linux Core job 129085002850 fails ResponseAfterChannelClosedIsDroppedQuietlyAsync: expected TcpTransportListener + 21 dropped-response log absent after 10 seconds; 6,727 passed, 1 failed, 93 skipped. Exact integration-base f8aa CI 42577864883, job 129029722931, artifact 13657092238, records the same case passed in 00:00:00.1056680, with Core 6,728 passed/0 failed. No Core source/test files change here. A shared-baseline reproduction or timing cause is not established. Source suggests a possible distinction between queuing on Faulted and rejecting on Closed channels; evidence does not establish which path occurred. No speculative Core patch is included.

Historical local/compile evidence

git diff --check passed before the earlier follow-up commits and suspension; not rerun. Current local net10/net48 tests/analyzers remain pending. Predecessor b416 multilingual SHA256: 23C47EE9287C45A1800C83001BE1B53591EFC6087B85E7B0CFE0FA43224B4A93.

On 27c6120b9dcfd048fd1bf4d22e7050d614fea22d, this historical command exited 1 in compilation, CS0121 at multilingual test line 176. No tests/TRX; net48 not launched. Do not reuse this suspended wrapper.

powershell.exe -NoProfile -NonInteractive -File D:\temp\opcua-contained-gate-v1.ps1 -WorkingDirectory D:\temp\padim-followup-delivery -Exe dotnet.exe -ExpectedSha256 8D5FC060637A816DE9F66E3F3D1125AA3AE3EB58C1D52854DCFC6C1ABE33F09F -Arguments 'test tests\Opc.Ua.PADIM.Tests\Opc.Ua.PADIM.Tests.csproj --framework net10.0 --configuration Debug --no-restore -p:CustomTestTarget=net10.0 --filter "FullyQualifiedName~EngineeringMetadataDetachesCallerValues|FullyQualifiedName~EngineeringMetadataCopyFailurePreservesProperties|FullyQualifiedName~HostedDeviceSupportsTypedDiscoverySimulationAndGeneratedOperations|FullyQualifiedName~HostedEngineeringUnitsHonorsPreferredLocalesAndReportsTranslationChanges" --logger "trx;LogFileName=focused.trx" --results-directory D:\temp\padim-contained-20261109-1022\net10 -m:1 -nr:false -p:UseSharedCompilation=false' -TimeoutMinutes 60

Result: ROOT_EXIT=1 TOTAL_PROCESSES=41 JOB_EMPTY=true, OWNED_JOB_EMPTY=true. Both hold files were absent immediately afterward; this does not describe the later ISA-95 hold. Logs: D:\temp\opcua-contained-ce6a8faf7e4248af84a0a2fe48399abd\stdout.log and stderr.log. Tested fixture SHA256 0AAAE0DC8BC3F5308ED080F8C89899DCB010E8FAA09C91DDFD8303989B7867A9.

CI 42589029747, head 27c6120, had 8 failed/57 incomplete jobs at November 9, 2026 09:24:27 UTC. All eight inspected failures were candidate CS0121: Windows48 Release 129065972950, Windows48 Debug 129065972953, Linux10 Release 129065973037, Linux8 Release 129065973052, Linux9 Release 129065973212, Windows10 shard8 129065974212, Windows48 shard8 129065974337, Linux10 shard8 129065974620. Shards failed compilation, not test cases. The new fixture is absent from integration base f8aa.

Full local dotnet test UA.slnx -p:CustomTestTarget=net48 -m:1 --no-restore and dotnet test UA.slnx -p:CustomTestTarget=net10.0 -m:1 --no-restore were not run. Approved shared-host plan uses focused local projects and hosted full-matrix validation.

Withdrawn-wrapper run

dotnet test tests\Opc.Ua.PADIM.Tests\Opc.Ua.PADIM.Tests.csproj -f net10.0 -p:CustomTestTarget=net10.0 -m:1 -nr:false --filter "FullyQualifiedName~EngineeringMetadataDetachesCallerValues|FullyQualifiedName~EngineeringMetadataCopyFailurePreservesProperties|FullyQualifiedName~HostedDeviceSupportsTypedDiscoverySimulationAndGeneratedOperations|FullyQualifiedName~HostedEngineeringUnitsHonorsPreferredLocalesAndReportsTranslationChanges" --logger "trx;LogFileName=focused.trx" --results-directory D:\temp\padim-followup-results\focus10 --nologo -v:q was stopped without a verdict. The first report that its waiter stopped before launch was wrong: gate PID28952 survived the parent shell; dotnet PID29028 started at09:58:11 local. The09:01:56Z memory sample was11.094528GiB, below12GiB. Positively identified owned dotnet then gate PIDs were stopped; retained process handle exit -1, no TRX, fresh snapshot both absent. No shared compiler stopped. The withdrawn wrapper lacked job containment, so escaped/unobserved descendants cannot be excluded. Evidence: D:\temp\padim-followup-results\existing-run-monitor.jsonl and existing-run-verdict.json.

Coverage comparison

Requested pre-#4993 baseline bc91e23725f6ba05b6e20639c5b74b960dc85160, CI 42380925376, artifact 13583240260, has no matching PA-DIM components: 0/0 lines/branches, undefined, not 0%.

Approved substitute #4993 merge b9d5a2c3ccea48352ed8147bfa723437c37ccdc7/CI 42384004060/artifact 13582789779; master 9eca587bdec449be97684da5c1c25af50b5e9bba/CI 42562371225/artifact 13654110477; exact integration master f8aa7a62f42b8664f64d33c015f69c0fc0da6e51/CI 42577864883/aggregate artifact 13663800997 all have identical counts using the same exact package/class-line filters:

Package Covered/valid lines Covered/valid branches
Opc.Ua.PADIM.Client 170/202 (84.1584%) 95/116 (81.8966%)
Opc.Ua.PADIM.Server 1471/1608 (91.4801%) 791/956 (82.7406%)

Finished b416 Linux10 artifact 13664077745 and Windows10 artifact 13664209210 each report Client170/202 lines,95/116 branches; Server1498/1626 lines(92.1279%),801/960 branches(83.4375%). These are single-project/single-matrix reports from failed predecessor runs. The newer fb3156d and current-head provisional counts are recorded above. Baselines aggregate hosted matrices and other test projects. A finished matched aggregate current-head report is unavailable; current coverage nondecrease is not established. Higher predecessor rates do not qualify failing shared-server behavior.

Earlier evidence: not validation of current source

Old head 846b16dee9feac7d7302377b749308b47e1f8b82 passed HostedDeviceSupportsTypedDiscoverySimulationAndGeneratedOperations in all four PA-DIM CI reports, each381 passed/0 failed/0 skipped: Windows48 job 128794106951/artifact 13625643996; Windows10 artifact 13625723803; Linux10 artifact 13626137770; macOS job 128794109837/artifact 13630273873.

Historical local commands on that old head:

  • dotnet test tests\Opc.Ua.PADIM.Tests\Opc.Ua.PADIM.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 -m:1 --nologo -v:q: passed381/381.
  • dotnet test tests\Opc.Ua.PADIM.Tests\Opc.Ua.PADIM.Tests.csproj -p:CustomTestTarget=net48 -f net48 -m:1 --nologo -v:q: passed380/381; HostedRuntimeFacetChangesRequireExplicitPublication timed out after30s at PadimClientServerTests.cs:789 through857.
  • dotnet test tests\Opc.Ua.PADIM.Tests\Opc.Ua.PADIM.Tests.csproj -p:CustomTestTarget=net48 -f net48 --no-build --nologo --logger "trx;LogFileName=padim48.trx" --results-directory D:\temp\padim-trx: repeated380/381.
  • dotnet test tests\Opc.Ua.PADIM.Tests\Opc.Ua.PADIM.Tests.csproj -p:CustomTestTarget=net48 -f net48 --no-build --nologo --filter "FullyQualifiedName~HostedRuntimeFacetChangesRequireExplicitPublication": reproduced timeout. Same test passed hosted exact-head; local cause unestablished.

CI 42500925357 completed127 successful/2 failed jobs. macOS 128794110010 failed SharedFileBindingReleasesClosedSessionHandlesAsync(True) with BadNotReadable; aggregate summary failed. Abandoned writer retained provider token during reopen. Merged #4999/9eca587bdec449be97684da5c1c25af50b5e9bba changes binding to CloseSession(session.Id, defer: false) and adds lifetime regressions; already in this branch ancestry, not duplicated. Historical results do not validate new ownership/locale/clone source.

marcschier and others added 5 commits November 8, 2026 16:35
# Description

Implements OPC 10000-21 registrar and device workflows, including ticket
validation, pull and push provisioning, typed client and fluent host
APIs, device lifecycle and reset, connected samples, and tests.

Runtime-backed managed applications can expose independent Part 12
ConfigurationFile providers. The managers recover pending transactions
before publishing applications. File operations and deferred apply or
revert effects use each application's authorization and deployment
lifetime. The typed client retains the namespace-URI application address
across reconnect and confirms only the returned update identifier.

Managed certificate selection uses the per-application type or the
configured registrar default. The selected type must appear in the
advertised group and be supported by the local authority before a CSR is
created. The standard enrollment provider retains it throughout journal
recovery. Legacy custom enrollment providers reject nondefault
selections before registration or CSR creation.

The managed-application verifier supports UA-TCP and HTTPS. HTTPS uses
OS TLS trust and revocation or an exact TLS leaf pin, with hostname and
validity checks on every handshake. It separately verifies the pinned
OPC UA application certificate, expected application URI, and
application proof before sending user credentials. The TLS terminator is
part of the trust boundary; legacy application signatures do not
cryptographically bind it.

The optional external-authentication handoff accepts an externally
installed DCA certificate through an injectable provider. The default
provider validates a dedicated managed CA trust list and exact
ProductInstanceUri and application-URI approval before entering the
shared registrar lifecycle. Typed external pull skips ProvideIdentities.
Registrar push retains external evidence through enrollment recovery
without fabricating a ticket. Ticket checkpoints remain version 1;
external-only checkpoints use version 2.

This PR adds no FDO SDK dependency or FDO protocol implementation.
Device authentication, TO0/TO1/TO2, ownership vouchers,
service-info/fdo.csr exchange, and initial credential installation
remain external. The external service must arrange credential trust with
the deployment's Registrar, SoftwareUpdateManager, and
CertificateManager. No FDO interoperability result is asserted.

**Open integration gap:** Part 22 Resources support from #4990 is
merged, but the connected Part 21 regression finds two
`0:ProvisionableDevice` references: the standard `i=29878` and the
onboarding manager's custom instance. The standard node is owned by the
diagnostics manager. A lifecycle-aware binding must preserve
generation-specific routing and references across publication,
replacement, cancellation, and retirement. Part 21 remains `Partial`
until this integration works. Shared Part 12/20 ConfigurationFile
transfer and open-handle limitations also remain.

### Focused CI repairs and validation

The certificate-manager ownership changes and cancellation handling at
`af0abdd44` have not been locally validated. Shared application
configurations still need a manager-lifetime fix. The trust-list
partial-write fix at `b1aa6c17c` passed 16 focused tests before the
subsequent ownership changes. The macOS WoT integration shard split at
`ae1862881` was checked against recorded selectors, but the new shards'
duration and the C# matrix tests are unverified. The subscription
readiness change at `2f516afe9` is also unverified.

The following earlier run at `c673ee6cf06504a9badc1ebae9efb1296f1943dc`
used `TEMP` and `TMP` set to `D:\temp`, under
`Local\OpcUaSpecSupportBuildGate`:

```powershell
dotnet test tests\Opc.Ua.Gds.Tests\Opc.Ua.Gds.Tests.csproj -f net10.0 -p:CustomTestTarget=net10.0 -m:1 --no-restore --filter 'FullyQualifiedName~Onboarding|FullyQualifiedName~ManagedApplicationPushTests' --logger 'trx;LogFileName=onboarding-final.trx' --results-directory D:\temp\onboarding-4998
```

**Passed:** 1,291 tests, zero failed or skipped, exit code 0. The TRX
reported no nonpassing counters or fixture setup or teardown failures.
This result predates the changes listed above and does not validate the
current head.

Coverage has not been measured. The full local `UA.slnx` net48 and
net10.0 test runs have not been completed. Local builds and tests are
paused because shared-host memory is below the agreed threshold. The SDK
10 VSTest runner also has a known incompatibility with the three TUnit
AOT projects; a different runner would not satisfy the checklist's exact
command requirement. Hosted CI is in progress.

## Related Issues

- Relates to #4973; this reference does not close the tracking issue
while documented gaps remain.
- Part 22 Resources dependency
[#4990](#4990)
merged to master at `bc91e23725f6ba05b6e20639c5b74b960dc85160`.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] If this PR changes C# source files, I ran **all** tests locally
using the **UA.slnx** solution against at least .NET **Framework** and
.NET **10**, and all passed. PRs changing only non-C# files do not
require these .NET test runs.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Code coverage

✅ Coverage gate passed.

Check Result Threshold
✅ Project line rate 86.70% (478959/552410 lines) >= 70.00%
✅ Project branch rate 76.11% >= 60.00%
✅ Patch coverage 97.74% (130/133 changed lines) >= 75.00% (> 100 changed lines)
ℹ️ Baseline delta (advisory) +13.10 pp 73.60% recorded
Uncovered changed lines
  • src/Opc.Ua.PADIM.Server/PadimValueBinding.cs: 420, 444, 460

Coverage is above the recorded baseline - consider ratcheting coverage-thresholds.json.

Thresholds live in coverage-thresholds.json. Whole report before exclusions: line 86.15%, branch 75.59%.

romanett and others added 3 commits November 8, 2026 16:17
# Description

Since #4982, about half of master CI runs fail in `test-macos-net10.0
(17/18) - wot-other-integration`. The tests don't hang. The leg runs
`Opc.Ua.WotCon.Tests` with `TestCategory=Integration`, excluding
`Materialization`, sequentially, and the 45-minute per-project ceiling
(`$PerProjectTimeoutMinutes`) kills it while tests are still passing.
Each live test creates new RSA, P256 and P384 certificates, which is
slow on macOS.

Measured macOS test time per class (TRX of passing run 42394584570 on
a1a3d48, 385 tests, 2486 s test time):

| Class | Tests | Seconds |
|---|---:|---:|
| WotLegacyNativeAdmissionLiveTests | 123 | 615 |
| WotRegistryIdentityLiveTests(False) | 57 | 551 |
| WotRegistryIdentityLiveTests(True) | 57 | 491 |
| WotRegistryProjectionLiveTests | 66 | 403 |
| WotSelectedDependenciesLiveTests | 24 | 131 |
| WotConfiguredOptionsTests | 21 | 118 |
| everything else | 37 | ~140 |

#4991 moved `WotRegistryIdentityLiveTests` into
`wot-other-identity-integration`. On its own master run (42476750357),
the remaining `wot-other-integration` still took **1782 s (29.7 min) of
test time plus 856 s of build**: 271 tests, with
`WotLegacyNativeAdmissionLiveTests` at 816 s. That leaves little margin
on a slow runner.

This PR moves `Opc.Ua.WotCon.Tests.WotLegacy*`
(`WotLegacyNativeAdmissionLiveTests`, `WotLegacyTypeBindingLiveTests`,
`WotLegacyMaterializationLiveTests`) into a new macOS-only entry,
`wot-other-legacy-integration`. Windows, Linux and net48 are unchanged.

Expected macOS test time per entry, from the 42476750357 TRX (the
a1a3d48 numbers are in parentheses):

| Entry | Tests | Expected |
|---|---:|---:|
| wot-other-integration | 144 | ~14 min (~13 min) |
| wot-other-identity-integration | 114 | ~17–20 min (17.4 min) |
| wot-other-legacy-integration | 127 | ~14 min (10.6 min) |

## Verification

- `dotnet test --list-tests` on each filter: original = 385, and other
144 + identity 114 + legacy 127 = 385. The unique-name union equals the
original set, and no name appears in two parts.
- `get-ci-matrix.ps1`: 102 PR entries and 210 full entries, both under
the 256 limit. The macOS WotCon entries are now
`wot-materialization-core`, `-integration`, `-live-events`,
`wot-other-core`, `wot-other-integration`,
`wot-other-identity-integration` and `wot-other-legacy-integration`.
- `CiMatrixScriptTests` (35 tests) pass, extended to require the new
shard and to check that each `WotLegacy*` class is routed to exactly one
shard.

I didn't add per-fixture certificate caching: it would change the setup
of every live test, which goes beyond this CI fix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@marcschier
marcschier marked this pull request as ready for review November 8, 2026 19:39
Copilot AI balanced review requested due to automatic review settings November 8, 2026 19:39
@marcschier
marcschier marked this pull request as draft November 8, 2026 19:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Targeted PA-DIM results cover the regression, but required full-solution validation for net48 and net10.0 remains unrun, leaving cross-component validation outstanding.

No open issues 🎉

What changed in this PR

This PR updates PA-DIM analog bindings to report engineering metadata changes after registration, allowing clients to receive semantic-change notifications while preserving initial materialization behavior.

Changes:

  • Report EngineeringUnits and EURange changes through the node manager’s existing property-change handling.
  • Extend the client/server regression to verify semantic-change events identify the affected measurement for both range and unit updates.
File Description
tests/​Opc.Ua.PADIM.Tests/​PadimClientServerTests.cs Adds event subscriptions and assertions for post-registration range and unit changes.
src/​Opc.Ua.PADIM.Server/​PadimValueBinding.cs Captures previous metadata values and reports property changes for registered analog variables.

Review effort: Balanced

@codecov

codecov Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.85811% with 653 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.83%. Comparing base (570b17e) to head (92860d4).
⚠️ Report is 7 commits behind head on master.

Files with missing lines Patch % Lines
...re/Stack/Configuration/ApplicationConfiguration.cs 49.80% 105 Missing and 25 partials ⚠️
...Opc.Ua.Gds.Client/ServerPushConfigurationClient.cs 71.59% 78 Missing and 22 partials ⚠️
...r/Hosting/OpcUaDeviceRegistrarBuilderExtensions.cs 79.10% 51 Missing and 33 partials ⚠️
....Gds.Server/ManagedApplicationsNodeManager.Push.cs 73.50% 21 Missing and 10 partials ⚠️
...s.Server/ApplicationsNodeManager.PushEnrollment.cs 81.70% 14 Missing and 16 partials ⚠️
src/Opc.Ua.Gds.Server/ApplicationsNodeManager.cs 81.69% 22 Missing and 6 partials ⚠️
...lient/SecretStorePullDcaCertificateReceiptStore.cs 77.88% 11 Missing and 12 partials ⚠️
...r/Onboarding/DeviceRegistrarAuthorityTrustLists.cs 77.88% 11 Missing and 12 partials ⚠️
src/Opc.Ua.Gds.Client/PullDcaWorkflow.cs 93.35% 8 Missing and 12 partials ⚠️
...pc.Ua.Gds.Server/Hosting/GdsServerHostedService.cs 47.05% 15 Missing and 3 partials ⚠️
... and 26 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #5029      +/-   ##
==========================================
- Coverage   83.50%   80.83%   -2.68%     
==========================================
  Files        3714     3776      +62     
  Lines      539134   552410   +13276     
  Branches    91840    94207    +2367     
==========================================
- Hits       450216   446531    -3685     
- Misses      58497    73130   +14633     
- Partials    30421    32749    +2328     
Flag Coverage Δ
actions 80.83% <79.85%> (-2.68%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ient/Fluent/ClientSuppliedConfigurationProvider.cs 74.52% <100.00%> (+5.45%) ⬆️
src/Opc.Ua.Client/Fluent/ManagedSessionBuilder.cs 87.96% <100.00%> (+0.16%) ⬆️
src/Opc.Ua.Client/Session/ManagedSession.cs 80.66% <100.00%> (+0.48%) ⬆️
src/Opc.Ua.Client/Session/ManagedSessionOptions.cs 76.92% <ø> (ø)
...lient/Session/Subscription/SessionEngineContext.cs 91.53% <100.00%> (+0.06%) ⬆️
...Client/Subscription/NotificationMessageDispatch.cs 93.65% <100.00%> (+0.43%) ⬆️
src/Opc.Ua.Client/Subscription/Subscription.cs 88.48% <100.00%> (+0.62%) ⬆️
.../Opc.Ua.Client/Subscription/SubscriptionManager.cs 84.01% <100.00%> (+0.20%) ⬆️
...a.Configuration/ApplicationConfigurationBuilder.cs 95.74% <100.00%> (+0.02%) ⬆️
src/Opc.Ua.Core/Schema/ApplicationConfiguration.cs 77.79% <100.00%> (+3.02%) ⬆️
... and 136 more

... and 230 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

romanett and others added 3 commits November 8, 2026 22:28
…led projections) (#5005)

Part of #5003: fewer client bytes per event notification.

## Changes

**1. Types: reuse repeated short strings in `BinaryDecoder.ReadString`**
(net6+)

Every event of a Publish response carries the same `SourceName`, and
real event sources usually repeat the message text and locale too.
Requests repeat the identifiers of string node ids. The decoder
allocated a new string each time.

`DecodedStringCache` is a per-thread, direct-mapped cache with 128
entries for strings of at most 32 UTF-8 bytes. It returns the instance
decoded last for the same characters, and only after comparing them, so
the decoded value never changes. Invalid UTF-8 gets the same replacement
characters as before. A miss costs one hash of the decoded characters on
top of the allocation. netstandard2.0 and net48 keep the existing path.

**2. Client: rent the notification projection when notifications are
pooled**

With `PoolNotifications` on, the V2 `Subscription` already recycled
`MonitoredItemNotification` and `EventFieldList` after the handler
returns. It still allocated the `DataValueChange[]` /
`EventNotification[]` projection passed to
`ISubscriptionNotificationHandler` for every message. For events, the
recycled `EventFieldList` shell is small, so pooling did not reduce
event allocation at all (table below).

The projection is now rented from `ArrayPool<T>.Shared` and returned,
cleared, after the handler returns, at the same point where the payload
is recycled. Whether to pool is read once per dispatch. The pooled-mode
contract already said handlers must copy the values they retain. The
docs of `PoolNotifications`, `DataValueChange` and `EventNotification`
now say explicitly that this includes the memory itself; copying the
structs out stays safe.

The whole-message path (`ISubscriptionNotificationMessageHandler`) is
unchanged: its `SubscriptionNotificationMessage` may be kept by
handlers.

## Results

Paired harness runs alternating master 621c7f5 and this branch, 3
rounds; runs with competing load on the host were discarded. Scenario:
`events`, 10 sessions × 15 subscriptions, 1000 events/s requested,
encrypted, Server GC on both sides. The server is the same build on both
sides.

| Client bytes per notification | master | this PR |
|---|---:|---:|
| events, managed client | 484–490 | 409–411 (−16 %) |
| events, classic client | 460–463 | 385–391 (−16 %) |
| events, managed client, `--pool-notifications 1` | 468–469 | **350–352
(−25 %)** |
| data change 10 × 15 × 600, `--pool-notifications 1` | 143–199 | 139 |

- Client CPU per notification is unchanged (within noise).
- Read 1 / Read 100 / Write 100 and the unpooled data change line show
no change in client or server bytes, or in CPU beyond run-to-run noise.

The harness server's event `Message` contains a counter (`Load event
N`), so here only `SourceName` repeats. Event sources with templated
messages save more.

Neither change removes the Workstation GC limit described in #5003: the
in-flight notifications are still larger than the default gen0 budget.
They lower the allocation rate that drives it.

## Tests

- New `BinaryDecoderTests`:
- distinct strings (including colliding cache slots), both sides of the
32-byte limit, multi-byte and invalid UTF-8, and zero-terminated input
all decode exactly as `Encoding.UTF8.GetString` does;
  - repeated short strings share one instance; long ones do not.
- New
`SubscriptionTests.PooledNotificationMemoryIsReturnedAfterHandlerAsync`:
with pooling, the memory is cleared after the handler returns while
values the handler copied stay valid. Without pooling, the memory stays
intact.
- net10.0 runs: `Opc.Ua.Types.Tests` 13,971 passed;
`Opc.Ua.Core.Encoders.Tests` 2,947 passed; `Opc.Ua.Client.Tests` 3,635
passed, 2 skipped.
- `Opc.Ua.Types` and `Opc.Ua.Client` build for all target frameworks.
net48 tests were not run locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
…ghtly sample-core debug hang) (#5027)

## Summary

The nightly `sample-core` net10.0-debug legs (Windows and Linux) crash
with a blame hang dump in
`WotSampleEndToEndTests.BadTargetMappingFailsRefreshAsync` (nightly run
[42436716335](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42436716335)).
It is not a deadlock, and it is not Debug-only. Loading the full
535-document WoT sample set makes the xRegistry projection reconcile
quadratically, and Coverlet in the Debug legs pushes the first E2E test
past the 10-minute blame hang timeout.

## Root cause

`XRegistryProjectionEngine` reconciles the entire registry projection on
every registry change. The sample set projects 535 documents (about 70k
nodes), and upload plus refresh cause about 2,700 reconciles. Each
reconcile walked every Resource and re-cleared its change masks, several
times over:

- a recursive `ClearChangeMasks` of every existing group (all of its
Resources),
- for every logical Resource: two recursive clears of the logical
subtree plus one per Version node,
- since #4806 (10-17), a registry-wide
`ClearChangeMasksAsync(includeChildren: true)` to publish
`Registry.Epoch`.

At the end of a run one reconcile took about 130 ms, which adds up to
about 5 of the 7.5 minutes this test takes. Release CI shows the same:
the three full-load E2E tests take 7.5–8.7 min in the Release legs of
the same nightly. Their durations went up at the 10-18 nightly, after
#4806. The 11-01 nightly already has a hang dump in the same test. The
dumps show the reconcile pool thread busy in `ClearChangeMasks` /
`ApplyWotResourceProperties`, not blocked.

The bad-mapping error path itself works: the test passes and the
`ServiceResultException` reaches the client.

## Fix

- **Clear each node once per reconcile.** The group and registry levels
now clear only their own state and non-Resource / non-group children.
Each logical Resource subtree (which contains its Versions) is cleared
once, after its Versions are reconciled.
- **Skip unchanged Resources.** The new optional
`IXRegistryProjectionResourceChangeToken` lets a strategy supply an
immutable token per logical Resource. The engine skips a Resource whose
token is reference-equal to the one it last applied. The WoT adapters
return the immutable `WotResource`, which every registry commit replaces
when anything about that Resource changes. Strategies without the
interface behave as before.
- The three full-sample-load E2E tests now use a 6-minute CTS instead of
30 minutes. A future slowdown then fails as a test timeout, below the CI
blame hang timeout (10 min).

## Measurements (local, Debug, net10.0)

| | before | after |
|---|---|---|
| `BadTargetMappingFailsRefreshAsync` alone | 7 m 22 s | 2 m 11 s |
| time spent in reconcile (same run) | ~5 min | ~3 s |

What remains is per-commit registry persistence (`FileWotRegistryStore`
write-through and manifest replace) and materialization, spread across
many call sites with no single hotspot left.

## Other notes

- The three `SubscriptionManager.ConditionRefreshWorker` threads in the
hang dump belong to the three servers that were still running (SourceA,
SourceB, Aggregation). Locally all three log `Exited Normally` on
teardown, so they are not a leak.

## Tests

- New
`XRegistryProjectionEngineTests.UnchangedChangeTokenSkipsTheVersionedResourceReconcileAsync`.
It fails with the skip disabled.
- All local runs on net10.0, one after another (no parallel test runs):

| Project | Debug | Release |
|---|---|---|
| Opc.Ua.WotCon.Samples.Tests | 75/75 (2 runs) | 75/75 (2 runs, 11m53 /
11m44) |
| Opc.Ua.XRegistry.Tests | 550/550 | 550/550 |
| Opc.Ua.WotCon.Tests | 3321/3321 | 3321/3321 |

- Full-load E2E tests after the fix: BadTargetMapping 1m44 Debug / 1m23
Release; RealSamplesRouteManagement 2m25 / 1m43; UnavailableUpstream
2m09 / 1m35. In CI Release before the fix they took 7m36 / 8m41 / 8m23.
- `Opc.Ua.WotCon.Server` builds for net48.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Description

_Describe the changes here to communicate to the maintainers why they
should accept this pull request. By default - this will become the
Commit message after merging and thus define history._

Extend OPC 40200 Weighing Technology through the existing Scales
runtime, typed client, fluent builders, DI/direct construction, and
samples. Catchweigher and price-labeler processing accepts
equipment-acquired items and equipment-supplied or injectable prices.
Totalizing-hopper completed fills update statistics and TipCounter.
Analog and Boolean-edge recipe waits are opt-in. Ordinary
hopper/checkweigher behavior, manual recipes, PackML, source quality,
and product locks remain supported. Physical sorter, printer and motor
operation, fiscal certification, and optional MCP/UA Lens additions are
excluded.

**Support remains Partial. Do not merge.** Published successor
`ce01effa15cea555c31561d4ab59d39948d6b19b` includes the reviewed
four-file FileSystem lifecycle correction
(`849223eda7ae03d500825dc3cc49183b1285eb46`) and merges current master
`ab5fddad9ab8ba162f10766b0e685c882db31ba9`. [Exact-head CI
42531450874](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42531450874)
completed successfully on this commit, including the code-coverage job.
The matched master-to-ce01 coverage comparison still shows decreases in
the Scales.Server and Scales.Client components; see the coverage results
below. A separate scoped recovery patch remains unrun and unmeasured,
and is not part of this PR head. The existing ready state and merge
settings are unchanged. The retained 299-line notification diagnostic
block is neither committed nor pushed and is not treated as a regression
suite for the selected ordinary-notification policy. The unapproved
FileSystem documentation paragraph is excluded.

**FileSystem lifecycle correction.** Session close previously marked an
idle writer closing while deferred cleanup retained its write
reservation. FileTypeBinder now starts idle memory-stream cleanup
without provider-thread deferral. FileHandle signals completed
asynchronous open only after its linked provider token is detached; it
skips lifetime cancellation only for detached, idle asynchronous opens,
while synchronous compatibility, pending opens, active operations and
disposal retain cancellation. Active work keeps its reservation until it
drains. No synchronous provider callback is introduced into the
asynchronous path.

**Paired Windows candidate evidence.** [Green diagnostic
42529482112](https://github.com/marcschier/UA-.NETStandard/actions/runs/42529482112),
diagnostic head `9aea971fab7d4895306d5bce91060101a8bf8951` with
immutable source parent `4ef034ae635d636a1db3b3c72964bc2ef24ad1a2`,
passed `InlineSessionClosureWaitsForProviderTokenDetachmentAsync` 1/1 on
native Windows net48 and net10.0. Full
FileHandleLifetimeRegressionTests/FileTypeBinderLifetimeTests passed
53/53 net48 and 54/54 net10.0, with no fixture/RunInfo errors. [Red
diagnostic
42529488811](https://github.com/marcschier/UA-.NETStandard/actions/runs/42529488811),
diagnostic head `135523b6964029e75926eaa26206a03dcf074ef1` with source
`0734c09cff268288a94c6dbdd3fc16e68c53ee87`, failed 0/1 on each TFM at
the intended `Assert.That(stream.CanRead, Is.True)` assertion (actual
False), not a timeout or build failure. Both red TRXs and all four green
TRXs were independently inspected. These paired Windows candidate runs
are separate from the completed exact-ce01 CI listed above and do not
establish local full-solution validation.

**User-selected category policy:** preserve existing mixed-category
reporting and measured-only TotalPackagesWeighed totals; retain Partial
until category participation is clarified. OPC 40200
[§7.20.1](https://reference.opcfoundation.org/specs/OPC-40200/7.20.1)
and
[§7.20.3](https://reference.opcfoundation.org/specs/OPC-40200/7.20.3)
define Weighed as category participation, while
[§7.19.3](https://reference.opcfoundation.org/specs/OPC-40200/7.19.3)
permits rejections before or after weighing. The current dynamic Boolean
describes measurement completeness and cannot represent the measured
subset of a mixed category. No new split categories or
incompatible-report rejection is imposed. Ordinary enum-notification
reentry is preserved.

**Validation on immutable fa65 code head.** Opc.Ua.Scales.Tests passed
616/616, zero failed/skipped, on [Windows
net48](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42499377576/job/128792906071),
[Windows
net10.0](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42499377576/job/128792906131),
[Linux
net10.0](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42499377576/job/128792906205),
and [macOS
net10.0](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42499377576/job/128792909555),
excluding LongRunning and Stress. All ten
unit/currency/registered-reading ownership cases and six
unsupported-copy cases passed.

**Historical exact d6 outcome.** [CI
42513278502](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42513278502)
completed with failure on `d6e0a03f3fed85ac84a6e61f4382aaea36cc7024`.
Completed logs verify Scales 616 passed, 0 failed, 0 skipped on [Windows
net48](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42513278502/job/128830867436),
[Windows
net10.0](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42513278502/job/128830867459),
[Linux
net10.0](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42513278502/job/128830867805),
and [macOS
net10.0](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42513278502/job/128830870585).
The sole failed test shard, Linux job 128830870599, failed
`MachineVisionRuntimeTests.SharedFileBindingReleasesClosedSessionHandlesAsync(True)`
with `BadNotReadable`: the file was locked for writing. Vision recorded
712 passed, 1 failed, 1 skipped; aggregate summary 128863789556 also
failed. Both parameter cases passed on preceding fa65 Linux job
128792909544/TRX artifact 13627626176. Those differing outcomes alone do
not establish a cause; the separate lifecycle investigation reproduced
deferred cleanup retaining an idle writer reservation. The correction is
published in ce01; the integrated exact-head CI completed successfully
as linked above.

**Coverage on historical d6.** [Coverage job
128863789514](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/42513278502/job/128863789514)
completed successfully and reported "Coverage is above the recorded
baseline" and "Coverage gate passed." Windows net10.0 Scales coverage
was 793/827 covered lines for Scales.Client (95.89%), 186/186 for Scales
(100%), and 6567/7021 for Scales.Server (93.53%). Line counts and branch
rates matched fa65; this comparison establishes no decrease across the
documentation successor, not against a pre-feature baseline. The passing
coverage gate does not change d6's failed overall result. Exact-ce01 CI
and matched component-coverage results are reported separately above;
local full-solution validation remains unrun.

**Matched Scales component coverage: master ab5 versus exact ce01.**
Master run 42530395411 (artifact 13640808392) compared with exact-ce01
run 42531450874 (artifact 13642293626) shows Scales.Server line coverage
decreasing from 3635/3833 (94.834%) to 6568/7021 (93.548%), while branch
coverage rises from 1474/1842 (80.022%) to 2962/3595 (82.392%).
Scales.Client line coverage decreases from 612/633 (96.682%) to 797/827
(96.372%), and branch coverage decreases from 214/273 (78.388%) to
263/341 (77.126%). Root combined coverage increased from 510227/576601
to 513513/580204 lines and from 219732/278244 to 221408/280233 branches;
that aggregate increase does not erase the component-level decreases.
The separate scoped regression recovery patch has not been run or
measured and does not qualify as restored Scales coverage.

**Overall fa65 CI failed.** macOS job 128792909513 failed
QuietSubscriptionAcknowledgementSurvivesConcurrentPublishesAsync(4):
BadPublishRequestCount was 1 instead of 0. A server BadTimeout was
recorded during that interval, but its relation to the counter increment
is unproven; the acknowledgement assertion passed. Separate
immutable-fa65 hosted controls passed 12/12 in run 42513134265, and an
instrumented full fixture passed 30/30 in run 42514537147. Neither
reproduced the failure. A traced BadNoSubscription/counter increment
occurred after a passing assertion during cleanup and does not explain
the original failure. The zero assertion is retained. This failure is
not attributed to weighing or dismissed as pre-existing.

Checkweigher reports commit selected-product and generic-scale statistic
caches before observers run. Synchronous observers remain inline before
asynchronous observers; exceptions propagate without rolling back
committed counts. Recorder initialization succeeds before caching, and
successful product removal retires captured associations without
independently retaining removed subtrees. Native nominal service writes
recompute GiveAway within writer admission and transfer admission to
derived-value notification delivery. Direct assignments remain
application-serialized and refresh derived values on the next report.

Sample deviation uses n-1; FullCheck uses n. Empty statistics and
one-item samples publish zero placeholders with BadNoData; singleton
FullCheck publishes computed zero with Good quality. GiveAway is
`(acceptedMean - currentNominalWeight) * acceptedCount`, including
negative results. Bad or uncertain nominal quality propagates. Infinite
inputs reject before counters change. Mixed measured/unmeasured
aggregates use the measured subset with UncertainSubNormal; overflow
retains the previous finite value with BadOutOfRange. The equations were
verified against the [VDMA 40200:2025-04
co-publication](https://www.vdma.eu/en/catalog-detail/-/catalog/14137),
identified as identical to OPC 40200 Release 2.00, sections 7.19.3 and
7.20.3. The PDF and excerpts are not redistributed.

Optional notification metadata includes AuxParameters, HelpSource and
VendorNotificationId with absent-versus-empty behavior. Accepted
lower-tolerance percentages use all accepted items, independently of
optional counter nodes; each report refreshes percentages. NaN items do
not increment TotalPackagesWeighed. Mixed-category Weighed remains the
documented conformance limitation under the selected compatibility
policy.

Correlated initialization, reset and acquisition publication rejects
same-flow nested writes with BadInvalidState rather than waiting for the
publisher. External asynchronous writers wait. Initialization failure
remains uncached and retryable before counting. Catchweigher
product-period reset includes zone counters, StartTime/ResetCondition
and percentages without resetting other products, scale totals or
acquisition sequence ownership. No UA Reset method is added.

**User-selected acquisition contract:** publication retains
setter-by-setter ordering. Catchweigher consumes the sequence before the
first output counter changes, or before product counting if no output
statistic exists. Totalizing hopper consumes it before TipCounter
changes. A late observer exception can leave partial published state,
suppresses WeightRegistered, and rejects same-sequence retry with
BadEntryExists without replaying counters. Early catchweigher LastItem
observer failures before counting remain retryable. Applications
reconcile partial state; no rollback or atomic acquisition snapshot is
promised.

**Ordinary enum notification reentry remains separate.** A write
originating in any active ordinary notification flow may run while
another callback is active; external writers wait. Preset notifications
remain inline after the preset lock is released, with CurrentProducts
before product subtrees and CurrentProductsChanged last. Exceptions
propagate and failed change masks are restored without undoing preset
mutation. No deferred peer-drain policy is implemented. The preserved
reentry behavior does not isolate writes from overlapping callbacks;
that limit and mixed-category Weighed retain the Partial rating.

Native async Value writes preserve async-handler precedence and sparse
enum validation. Unsupported custom/synchronous-only paths return
BadNotSupported without calling synchronous handlers or changing the
cache. UInt64 enum values above Int64.MaxValue reject before
callbacks/cache mutation, following the recommendation in [Part 8
section
5.3.3.4](https://reference.opcfoundation.org/specs/OPC-10000-8/v1.05.07/5.3.3.4);
the legacy synchronous fallback remains.

**Breaking change:** public WriteAttributeAsync is non-virtual.
Overrides move to protected WriteAttributeCoreAsync to run inside
admission and call the protected base hook rather than reentering the
public entrypoint. The user accepted this deviation from 1.5.378
compatibility; docs/MigrationGuide.md contains examples. Recipe starts
retain product reservations without controller locks; replacing a
pending release check, including A-to-B-to-A replacement, rejects start.
Without PackML, explicit application release or an allowing check is
required.

**Metadata ownership.** CopyUnit detaches the unit's LocalizedText
display name and description, and CopyCurrency detaches the currency
text, preserving locale and translation fallback. RegisteredReading
receives a separate engineering-unit value from the pending acquisition;
scalar registration is unchanged.
UnsupportedMetadataCopyDoesNotConsumeAcquisitionAsync covers unsupported
formatting arguments and comparers across unit, equipment-currency, and
provider-returned currency sources. All six cases passed:
BadNotSupported propagates before publication, counts remain zero, and
the same sequence with supported metadata counts once. Earlier 1e55
passed 600 compatibility cases and failed ten controls on all four
platforms: six failures demonstrated aliases, while four were
multilingual-fixture defects corrected explicitly with AsMultiLanguage.
The corrected multilingual cases are passing controls, not previously
established alias failures.

See docs/Scales.md and docs/Profiles.md for APIs, ownership, publication
and remaining limits. README/style changes from other sessions are
untouched.

## Related Issues

_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

- Fixes #4969.
- Prerequisite #4960 is merged.
- Shared condition/alarm setters are reviewed in #4992 and the
PackML/FSM seam in #4995. Scales reuses ClearChangeMasksAsync rather
than duplicating those implementations.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] If this PR changes C# source files, I ran **all** tests locally
using the **UA.slnx** solution against at least .NET **Framework** and
.NET **10**, and all passed. PRs changing only non-C# files do not
require these .NET test runs.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

## Validation commands

Candidate diagnostics used Windows Server 2025, Debug. For each
framework, the exact regression and full fixtures were separate
successful invocations in green run 42529482112:

- `& .\.github\scripts\run-dotnet-tests.ps1 -Projects
'tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj' -CustomTestTarget
'net48' -Framework 'net48' -Configuration Debug -Filter
'FullyQualifiedName~Opc.Ua.Server.Tests.FileSystem.FileHandleLifetimeRegressionTests.InlineSessionClosureWaitsForProviderTokenDetachmentAsync'
-HangTimeout '3m' -PerProjectTimeoutMinutes 10 -ResultsDirectory
'artifacts\vision-token\exact' -QuietOutput` — passed 1/1. Same command
on red run 42529488811 failed 0/1 at the intended stream.CanRead
assertion.
- `& .\.github\scripts\run-dotnet-tests.ps1 -Projects
'tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj' -CustomTestTarget
'net10.0' -Framework 'net10.0' -Configuration Debug -Filter
'FullyQualifiedName~Opc.Ua.Server.Tests.FileSystem.FileHandleLifetimeRegressionTests.InlineSessionClosureWaitsForProviderTokenDetachmentAsync'
-HangTimeout '3m' -PerProjectTimeoutMinutes 10 -ResultsDirectory
'artifacts\vision-token\exact' -QuietOutput` — passed 1/1. Same command
on red run 42529488811 failed 0/1 at the intended stream.CanRead
assertion.
- `& .\.github\scripts\run-dotnet-tests.ps1 -Projects
'tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj' -CustomTestTarget
'net48' -Framework 'net48' -Configuration Debug -Filter
'FullyQualifiedName~Opc.Ua.Server.Tests.FileSystem.FileHandleLifetimeRegressionTests|FullyQualifiedName~Opc.Ua.Server.Tests.FileSystem.FileTypeBinderLifetimeTests'
-HangTimeout '3m' -PerProjectTimeoutMinutes 10 -ResultsDirectory
'artifacts\vision-token\full' -QuietOutput` — passed 53/53.
- `& .\.github\scripts\run-dotnet-tests.ps1 -Projects
'tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj' -CustomTestTarget
'net10.0' -Framework 'net10.0' -Configuration Debug -Filter
'FullyQualifiedName~Opc.Ua.Server.Tests.FileSystem.FileHandleLifetimeRegressionTests|FullyQualifiedName~Opc.Ua.Server.Tests.FileSystem.FileTypeBinderLifetimeTests'
-HangTimeout '3m' -PerProjectTimeoutMinutes 10 -ResultsDirectory
'artifacts\vision-token\full' -QuietOutput` — passed 54/54.

The following exact commands appear in completed fa65 and d6 CI logs.
Their historical Scales results are not the exact-ce01 CI result linked
above and do not establish local full-solution validation.

- Windows net48: `dotnet test
tests/Opc.Ua.Scales.Tests/Opc.Ua.Scales.Tests.csproj --no-build
--configuration Release --framework net48 /p:CustomTestTarget=net48
--logger trx --results-directory
D:\a\UA-.NETStandard\UA-.NETStandard\TestResults\Opc.Ua.Scales.Tests
--blame-hang-timeout 10m --blame-hang-dump-type mini --blame-crash
--blame-crash-dump-type mini --filter
TestCategory!=LongRunning&TestCategory!=Stress` — fa65 passed 616/616,
4m40s; d6 passed 616/616, 4m40s.
- Windows net10.0: `dotnet test
tests/Opc.Ua.Scales.Tests/Opc.Ua.Scales.Tests.csproj --no-build
--configuration Release --framework net10.0 /p:CustomTestTarget=net10.0
--logger trx --results-directory
D:\a\UA-.NETStandard\UA-.NETStandard\TestResults\Opc.Ua.Scales.Tests
--blame-hang-timeout 10m --blame-hang-dump-type mini --blame-crash
--blame-crash-dump-type mini /p:CollectCoverage=true --collect:XPlat
Code Coverage --settings ./tests/coverlet.runsettings.xml --filter
TestCategory!=LongRunning&TestCategory!=Stress` — fa65 passed 616/616,
3m44s; d6 passed 616/616, 3m26s.
- Linux net10.0: `dotnet test
tests/Opc.Ua.Scales.Tests/Opc.Ua.Scales.Tests.csproj --no-build
--configuration Release --framework net10.0 /p:CustomTestTarget=net10.0
--logger trx --results-directory
/home/runner/work/UA-.NETStandard/UA-.NETStandard/TestResults/Opc.Ua.Scales.Tests
--blame-hang-timeout 10m --blame-hang-dump-type mini --blame-crash
--blame-crash-dump-type mini /p:CollectCoverage=true --collect:XPlat
Code Coverage --settings ./tests/coverlet.runsettings.xml --filter
TestCategory!=LongRunning&TestCategory!=Stress` — fa65 passed 616/616,
3m11s; d6 passed 616/616, 3m22s.
- macOS net10.0: `dotnet test
tests/Opc.Ua.Scales.Tests/Opc.Ua.Scales.Tests.csproj --no-build
--configuration Release --framework net10.0 /p:CustomTestTarget=net10.0
--logger trx --results-directory
/Users/runner/work/UA-.NETStandard/UA-.NETStandard/TestResults/Opc.Ua.Scales.Tests
--blame-hang-timeout 10m --blame-hang-dump-type mini --blame-crash
--blame-crash-dump-type mini /p:CollectCoverage=true --collect:XPlat
Code Coverage --settings ./tests/coverlet.runsettings.xml --filter
TestCategory!=LongRunning&TestCategory!=Stress` — fa65 passed 616/616,
14m36s; d6 passed 616/616, 17m11s.

Local full-solution commands `dotnet test UA.slnx
-p:CustomTestTarget=net48 -m:1` and `dotnet test UA.slnx
-p:CustomTestTarget=net10.0 -m:1` remain unrun. Admission attempts
either stopped before mutex acquisition because RAM was below 12 GiB, or
passed sustained memory checks but found the shared gate occupied on
nonblocking acquisition. No local .NET command or mutex waiter started.
Latest ce01 admission probe found 11.03 GiB available, below the
required floor. The local-solution checklist remains unchecked; passing
hosted candidate tests and historical coverage do not substitute for it.
No local full-suite, all-warning clearance, or passing overall ce01 CI
result is asserted.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues are recorded for the engineering metadata change-notification update.

No open issues 🎉

Review effort: Balanced

romanett and others added 9 commits November 9, 2026 07:12
…ite-side socket close (#5031)

## Problem
`ClientTest.GetEndpointsOnDiscoveryChannelAsync(False)` sends an
oversized GetEndpoints request (10000 profile URIs) on a discovery
channel. The server rejects it with `BadSecurityPolicyRejected` and
closes the socket. If the close happens while the client is still
writing the multi-chunk request, the client sees a transport error
instead of the rejection.

The test already treated `BadSecureChannelClosed` as inconclusive. On
macOS CI (run 42531694552, job 128887208396) the same race showed up as
`BadTcpInternalError` ("Unexpected error during write operation."), and
the test failed. That status comes from
`UaSCBinaryChannel.WriteSingleChunkAsync`/the gathered and
buffer-collection write paths. They wrap any
non-`ServiceResultException` thrown by
`IUaSCByteTransport.SendChunkAsync` (the raw `SocketException` for
EPIPE/ECONNRESET) as `BadTcpInternalError`.

## Change
Test-only. The test now also treats these transport-close codes as
`Assert.Inconclusive`:
- `BadTcpInternalError`: the failed socket send.
- `BadConnectionClosed`: `TcpByteTransport.RequireConnectedSocket` after
close.
- `BadNotConnected`: the client channel is already shut down.

Every other status must still be `BadSecurityPolicyRejected`.

## Verification
Ran `--filter
"FullyQualifiedName~ClientTest.GetEndpointsOnDiscoveryChannelAsync|FullyQualifiedName~ClientTest.ReadOnDiscoveryChannelAsync"`
three times on net10.0 (Windows). There was no failure; locally the race
takes the `BadSecureChannelClosed` path, so the test is inconclusive.

## Possible follow-up (not in this PR)
The write path could map a `SocketException`/`IOException` from a peer
reset (EPIPE, ECONNRESET, ConnectionAborted) to `BadConnectionClosed`
instead of `BadTcpInternalError`. That would be more accurate for
callers, but it changes product behaviour, so it is left out here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants