Skip to content

Draft: prove registry-first WoT Binding and Connectivity 1.1 stack - #19

Closed
marcschier wants to merge 20 commits into
copilot/3993-live-node-manager-lifecyclefrom
marcschier/wot-binding-xregistry
Closed

Draft: prove registry-first WoT Binding and Connectivity 1.1 stack#19
marcschier wants to merge 20 commits into
copilot/3993-live-node-manager-lifecyclefrom
marcschier/wot-binding-xregistry

Conversation

@marcschier

@marcschier marcschier commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Status and dependencies

Draft proof implementation.

What this proves

Lossless NodeSet2 ↔ WoT conversion

  • Adds a dependency-light, AOT-safe WoT lexical model and converters to Opc.Ua.Types.
  • Preserves unrecognized/unmapped JSON-LD values as digest-protected RFC 6901 residue entries in standard NodeSet Extensions.
  • Emits the deterministic, versioned uav:nodes projection only when readable-only reverse conversion does not reproduce the complete UANodeSet.
  • Supports WhenRequired, Always, and Never preservation modes. uav:nodeSet is emitted only for explicit byte archival or a demonstrated fallback, and completeness tests use Never.
  • Reconstructs from the complete native projection, an explicit envelope, or ordinary binding-authored TD/TM documents.
  • Includes bounded resolver contracts, structured diagnostics and canonical NodeSet comparison.

WoT source generation

  • Accepts .tm.json, .td.json, .tm.jsonld, .td.jsonld and explicitly opted-in .jsonld AdditionalFiles.
  • Converts them to an in-memory UANodeSet and uses the existing direct NodeSet code-generation pipeline.
  • Adds stable MODELGEN030MODELGEN034 diagnostics for parsing, conversion and virtual-path collisions.

Live NodeManager shadow reload

  • Extends [Server] Add live NodeManager lifecycle support OPCFoundation/UA-.NETStandard#4015 with ShadowReloadAsync.
  • Adds ImmediateReloadAsync as the alternative policy: it atomically routes new requests to the replacement, invalidates affected non-durable monitored items with BadNodeIdUnknown, detaches disposed owner graphs, and rejects durable subscriptions before commit.
  • Routes new service requests to the replacement generation while existing monitored items remain on the retired generation.
  • Disposes retired generations after subscriptions drain.
  • Keeps the existing fail-closed ReloadAsync/RemoveAsync behavior unchanged.

Registry-first WoT Connectivity 1.1

  • Compiles the pinned xRegistry model and the single combined WoT-Con 1.1 NodeSet. The combined model contains every published 1.02 node plus the additive registry model in the same namespace, so the old ModelDesign is not generated a second time.
  • Adds a stable, injectable TD/TM registry with in-memory and atomic file-backed stores, versions, epochs, desired/active state, persisted labels and bounds.
  • Exposes browseable xRegistry/WoT-Con 1.1 group/resource/file nodes, FileTransfer, CRUD, label, validation, enable/default-version and detailed Refresh methods.
  • Builds TM/TD dependency closures and materializes them through runtime NodeSet lifecycle Add/ShadowReload/Remove.
  • Resolves document dependencies only from the registry snapshot (or an injected federation layer); arbitrary external document URLs are not fetched.
  • Exposes WotProjectionRetirementPolicy so deployments select graceful or immediate replacement.
  • Retains invalid versions and the previous active projection, emits resource/refresh events, and reuses lifecycle model-change notifications.
  • Adapts the existing v1.02 CreateAsset/WoTFile/CloseAndUpdate workflow to the registry without changing published signatures or clients.

Replaceable protocol binding code-behind

Adds a dependency-light planner/binder project for:

  • HTTP
  • CoAP
  • MQTT
  • Modbus TCP
  • BACnet
  • PROFINET
  • LoRaWAN
  • OPC UA

Concrete optional executor projects cover HTTP, MQTT, Modbus TCP and OPC UA. The OPC UA executor supports Read, Write, native data-change observation, Method invocation and Event subscription, including portable nsu= NodeIds.

BACnet, PROFINET, LoRaWAN and CoAP are planner/validation-only in this proof and report their non-executable capability explicitly.

Review remediation

The follow-up review fixes are included:

  • shadow-retired data monitored items remain modifiable, transferable and deletable through their owning NodeManager and trigger prompt retired-generation cleanup;
  • mqtts enables TLS, defaults to 8883, resolves credentials/trust and fails closed;
  • Modbus validates the full 16-bit address range and maps function-only forms exactly;
  • HTTP redirects are explicit, bounded and cannot forward credentials across origins;
  • registry mutations become visible only after an atomic durable store commit;
  • all generated typed WoT event fields are populated;
  • uav:nodeSet.sha256 is mandatory and malformed XML becomes structured diagnostics;
  • one resolver context enforces aggregate limits across the full conversion;
  • event affordances carry uav:eventType, persisted identities use portable nsu= ExpandedNodeIds, and HasComponent subtypes round-trip exactly;
  • generated contexts bind ua and deterministic nsN prefixes; ReferenceType model names are emitted directly in link rel, with uav:refId as definitive identifier, and name/NodeId conflicts are rejected;
  • compact mapToTypeName / congruentTypeName hints remain paired with definitive identifiers, while arbitrary instance targets remain ExpandedNodeIds;
  • the non-published uav:typedReference / refTypeName draft surface is removed;
  • readable BrowseNames/paths prefer @context prefixes (with URI-qualified QualifiedNames also accepted), authored IDs are retained during synthesis, and the proof omits uav:nodes when the semantic mapping alone is equivalent;
  • native conversion covers root/model/alias/node fields, all eight NodeClasses, values, DataType definitions, permissions, translations and XML extensions without an envelope;
  • unknown JSON-LD members survive repeated WoT ↔ NodeSet cycles through bounded residue with stable context/link/affordance selectors;
  • observable / observeproperty is documented as advertised binding support, not a restriction on core UA monitorability.
  • registry FileType write-open/write/write-position/commit paths enforce the management SecureChannel policy while read-only access remains deployment-selectable;
  • committed reload cleanup failures carry the authoritative replacement registration so the coordinator cannot retain a stale handle, and committed model/namespace notifications are still attempted;
  • immediate retirement is serialized with monitored-item and durable-subscription mutations, emits one terminal status for Reporting/Sampling data items, clears event/aggregate queues, suppresses resend, and releases old owner graphs.

Compatibility

  • Existing OPC 10100-1 v1.02 model/NodeIds/method signatures remain unchanged.
  • Existing IWotAssetProviderFactory integrations remain supported.
  • Concrete transport dependencies are isolated from Opc.Ua.WotCon.Server.
  • Planner/core projects retain the repository's legacy target-framework matrix; concrete executors target modern .NET.

Commit structure

  1. Add shadow NodeManager reload support
  2. Add lossless WoT conversion and source generation
  3. Add injectable WoT protocol bindings
  4. Add registry-first WoT Connectivity runtime
  5. Align WoT Connectivity proof to combined 1.1 spec model
  6. Align WoT converter with portable Binding identity
  7. Make WoT NodeSet conversion native-first
  8. Address WoT model concept review feedback
  9. Address WoT mapping review feedback
  10. Prefer context prefixes for WoT QualifiedNames
  11. Address registry resolution and retirement feedback

Validation

  • full Opc.Ua.Types.Tests net10.0 and net48: passed
  • source-generation suite, including all WoT AdditionalFile cases: passed
  • full Opc.Ua.WotCon.Tests and Opc.Ua.WotCon.Binding.Tests net10.0: passed
  • lifecycle-focused net10.0 and net48 suites: passed
  • registry transaction and typed-event integration suites: passed
  • full dotnet build UA.slnx -c Release: passed
  • NativeAOT win-x64 publish/run: 115 passed
  • full converter/source-generator/legacy-TFM validation completed before the final rebase
  • pinned xRegistry/WoT-Con 1.1 models match the specification repository
  • immediate/graceful lifecycle, registry FileType security and committed-reload recovery tests: passed
  • WoT materialization policy tests: 22 passed on net10.0 and net48
  • affected Opc.Ua.Server / Opc.Ua.WotCon.Server builds on net10.0 and net472: passed with 0 warnings
  • git diff --check: clean

marcschier and others added 10 commits July 21, 2026 23:11
…ver limits

- Make uav:nodeSet.sha256 mandatory: a missing, malformed, or
  mismatched digest now produces a structured InvalidDigest/
  DigestMismatch diagnostic and no NodeSet is returned, instead of
  silently skipping integrity verification when the field was absent.
- Catch XML/deserialization failures from UANodeSet.Read and report
  them as a WotDiagnosticCode.MalformedNodeSet diagnostic instead of
  letting the exception escape the converter.
- Create exactly one WotResolutionContext per top-level conversion in
  WotNodeSetConverter.ToNodeSetCore, seeded from the converter options
  (depth, max documents, cumulative bytes), and thread that single
  context through link/thing resolution. Removes the previous
  per-link context ??= new WotResolutionContext() fallback in
  TryResolveTargetNodeId, which reset cycle/limit tracking for every
  link instead of applying bounds across the whole conversion.
- Seed WotNodeSetDocumentConverter's resolution context from its
  configured options instead of resolver defaults.
- Add WotNodeSetConverterOptions.MaxResolverDocuments/
  MaxResolverDocumentBytes/MaxResolverTotalBytes (aligned with
  WotResolverOptions defaults) plus ToResolverOptions() and
  validation, so aggregate resolver limits are configurable through
  the same options object used for the rest of the conversion.
- Add tests: missing/malformed digest, digest mismatch, malformed
  NodeSet XML without throwing, unsupported vs. accepted (base64)
  encoding, multi-link aggregate document/byte limits sharing one
  context, and sibling-link cycle detection that doesn't block
  unrelated links.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After a ShadowReload the retired generation is removed from the routing
table but still owns the monitored items created on it. Modify, Delete,
SetMonitoringMode and Transfer previously iterated only the visible
routing-table managers, so a same-namespace replacement generation would
claim (and fail on) the retired items - returning BadMonitoredItemIdInvalid
and leaving the retired generation's items undrained and its address space
never disposed until another lifecycle operation or shutdown.

Dispatch each data monitored item to its recorded owning NodeManager
(grouped by owner, each owner offered only its own indices) so a retired
generation services its own items and a same-namespace replacement cannot
cross-claim them. Event/all-event handling and result indexing are
unchanged.

Trigger prompt retired-generation cleanup when the last item drains: the
master notifies the lifecycle from the Delete path when an owner is no
longer registered, and the lifecycle runs cleanup on a background task
(request execution context suppressed, coordinated via the lifecycle
semaphore) so nothing is disposed inside the request callback. Existing
fail-closed Reload/Remove behaviour and the deferred-cleanup fallback are
preserved.

Add tests proving that after ShadowReload old data items can be modified,
disabled/re-enabled, transferred and deleted on the retired generation,
new reads use the replacement, old notifications keep flowing, and the
retired generation is disposed after the final drain without another
lifecycle operation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rects)

MQTT: mqtts now enables TLS and defaults to port 8883, resolving username /
password, TLS client certificate and trust anchors through the credential
provider. A declared security scheme fails closed when unresolved, and
username / password never downgrades to plaintext unless explicitly allowed.
plain mqtt stays explicit (port 1883).

Modbus: the planner enforces address 0-65535 and the address+quantity-1 range,
maps function-only forms exactly onto codes 1,2,3,4,5,6,15,16 (mnemonic or
numeric) and rejects op/function and entity/function mismatches. The executor
re-validates the range before the ushort/byte casts.

HTTP: the executor-owned HttpClient disables automatic redirects and applies a
bounded, origin-aware redirect policy that strips custom header/query
credentials across origins, refuses loops, non-http(s) schemes and https->http
downgrades, and caps hops. A caller-supplied client with a credential-bearing
form fails closed unless CallerClientHandlesRedirectSafety is set.

Adds planner, executor and E2E regression tests and updates the binding docs
and options. WotCredential gains runtime-only client certificate / trust
material.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Registry mutations now persist through a transactional store commit
before publishing the snapshot or raising Changed, and the generated
WoT V2 event types are fully populated from the coordinator arguments.

- IWotRegistryStore: replace the per-entity Upsert/Remove methods with a
  single atomic CommitAsync(snapshot). LoadAsync returns only the
  committed generation.
- FileWotRegistryStore: stage content-addressed version blobs durably,
  then switch a single manifest.json pointer with an atomic replace, so a
  crash never exposes a half-written generation; Load reads only the
  committed manifest.
- InMemoryWotRegistryStore: keep the committed snapshot behind an atomic
  reference switch (same failure semantics).
- WotRegistryService: commit durably before publishing m_snapshot or
  raising Changed for every CRUD/default/enabled/labels and projection
  path. On failure Current stays previous, no event is raised, a retry
  re-attempts persistence and a restart never sees partial data.
- WotRegistryNodeManager: populate every generated WoT event field
  (resource identity/kind, generation, phase/outcome, validation outcome,
  load state/failed node/reason, binding uri, refresh summary/request id)
  from WotMaterializationEventArgs via SetChildValue.
- Tests: fault-injection transaction tests (atomicity, no-event, retry,
  restart) and real EventFilter integration tests that verify the typed
  fields are delivered through the running server notifier chain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Regenerate the Opc.Ua.WotCon model from the single revised
Opc.Ua.WoTCon NodeSet2, which incorporates the published OPC 10100-1
v1.02 model (NodeIds 1..172, deprecated) plus the additive registry
nodes (64000+) in one namespace (http://opcfoundation.org/UA/WoT-Con/,
version 1.1.0), replacing the separate 1.02 ModelDesign and the
Opc.Ua.WotCon.V2 companion model.

Model / build:
- Pin Opc.Ua.WoTCon.NodeSet2.xml/.csv (+ xRegistry) and drop the
  V2-named artifacts; update Sync-WotConModels.ps1 mappings/docs.
- Opc.Ua.WotCon.csproj: stop compiling WotConnection.xml/csv; compile
  the combined NodeSet once as Opc.Ua.WotCon (name WotCon, model URI
  WoT-Con/). Legacy 1.02 sources kept under Design/ for reference only.

Server / tests / docs:
- Replace all Opc.Ua.WotCon.V2 namespace/type references with the
  combined Opc.Ua.WotCon generated types (AddOpcUaWotCon, ObjectIds,
  BrowseNames, DataTypeIds, ...); remove user-visible V2 wording and
  rename the registry config section to OpcUa:WotConRegistry:Server.
- Decide model-node ownership cleanly via WotConModelPartition: the
  registry NodeManager owns {WotCon, xRegistry} and loads the registry
  slice (>=64000); the legacy asset NodeManager loads the incorporated
  1.02 slice (<64000). Neither claims the same static node twice.
- Add CombinedModelPreservationTests proving exact 1.02 NodeIds, typed
  method state/results and the generated client API are preserved, and
  that the registry types now coexist in Opc.Ua.WotCon.

Source generator (NodeSet2 -> ModelDesign) fixes exposed by generating
the richer combined model with typed method arguments:
- Map placeholder BrowseNames "<Name>" to "Name_Placeholder".
- Reuse an existing explicit method-type declaration instead of
  synthesizing a colliding duplicate, and carry the concrete method's
  arguments onto it.
- Register the standard encodeable types (Argument) on the value decoder
  so NodeSet2 method argument lists actually decode (was silently empty).
- Disambiguate a by-ref output argument whose name matches an input.
- Skip synthesizing method types for base-namespace (inherited) methods
  and for standalone method-type declarations.

Deprecated 1.02 nodes remain usable and all 1.02 tests pass unchanged
(only mechanical Ua.DataTypeIds qualification where the new combined
DataTypeIds shadows Core).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marcschier marcschier changed the title Draft: prove registry-first WoT Binding and Connectivity stack Draft: prove registry-first WoT Binding and Connectivity 1.1 stack Jul 22, 2026
@marcschier marcschier closed this Jul 25, 2026
@marcschier
marcschier deleted the marcschier/wot-binding-xregistry branch July 25, 2026 06:28
marcschier added a commit that referenced this pull request Aug 26, 2026
…ary, Alarms and Conditions (OPCFoundation#4225)

# Description

Aligns the WoT implementation with the normative changes in the OPC UA
WoT
specification drafts that the stack had not picked up, and fixes the
defects
that alignment surfaced.

The driving artifact is

[OPCF-Members/spec-drafts#19](OPCF-Members/spec-drafts#19),
*"bind a projected node to an already-loaded type, by name or by
NodeId"*, which
landed on the spec `main` after this repository last tracked it.

## 1. Bind a projected node to an existing type (Binding §5.2.1)

§5.2.1 lets a document state that the node it projects is an instance of
a type
that **already exists** — one a companion model defines and the Server
has
loaded, or one a sibling document of the same conversion defines — so a
converter binds to that type instead of defining a second type of the
same
shape.

The converter hardcoded `HasTypeDefinition` to `BaseObjectType`, so a
document
that named its type projected an **untyped** node: a client browsing for
the
companion type would not find it, and nothing said why.

Both forms are now implemented, resolved against the §5.1.5 **local
context** —
the other documents being converted together first, a loaded
AddressSpace as the
fallback. `IWotNodeResolver` supplies that context (is a namespace held;
what
does a qualified BrowseName match; what does an ExpandedNodeId
identify),
`WotCompositeNodeResolver` composes implementations in the specified
order, and
`NullWotNodeResolver` is the default that holds nothing. The converter
needed no
new async surface: the binding is resolved in a pre-resolve pass,
exactly as
Thing references already are, and the synchronous core consumes the
result.

The §5.2.1 table is honoured: a unique name binds; the link settles an
ambiguous
name; a name that resolves to nothing while the link resolves is
**invalid**
(that is a mistake in the name, not a shorthand for the identifier); two
forms
resolving to different Nodes are invalid; a resolved type of the wrong
NodeClass
is invalid. A binding naming a type the local context does not hold
**fails**
rather than falling back to `BaseObjectType` — a silently mistyped node
is worse
than a reported failure.

A compact name is told from ordinary `@type` annotation **by
namespace**, not by
whether the lookup succeeds, so `saref:TemperatureSensor` on a Server
that has
never heard of SAREF stays the annotation it always was.

The definitive form — a `ua:HasTypeDefinition` link whose
`href` is the ExpandedNodeId of the type. An ExpandedNodeId matches
exactly one
Node or none, so it needs no lookup.

A Node has exactly one `HasTypeDefinition`, so:

- two such links → `AmbiguousTypeBinding` (6019), and the root keeps the
default
  rather than being bound to a guess;
- a link with no usable identifier → `InvalidTypeBinding` (6020);
- no binding at all → the `BaseObjectType` default is unchanged.

## 2. Drop the six vocabulary terms `#19` removed

`#19` deleted `uav:capability`, `uav:componentModel`, `uav:reference`,
`uav:congruentType`, `uav:congruentTypeName` and `uav:nameNamespace`
from the
context, the JSON Schema and the ontology. A link now names its
ReferenceType
directly in `rel`, so the three link relations become `ua:HasInterface`,
`ua:HasComponent` and `ua:NonHierarchicalReferences`, and the
congruent-type
pair is superseded by the §5.2.1 type binding.

Their validation and mapping are removed; they are now ordinary residue
like any
other unrecognised term.

### A real gap this uncovered

The converter knew **eight** ReferenceTypes by name and had neither
`NonHierarchicalReferences` (`i=32`) nor `HasInterface` (`i=17603`) — so
the two
relations that *replace* `uav:reference` and `uav:capability` could not
resolve
at all. That includes example 02, which this repository vendors as a
golden
asset and which `#19` rewrote to use them. Both are now in the map.

`uav:congruentType` was also the only term that redirected one reference
to
another while resolving a link target, so `ResolveTargetNodeIdAsync` no
longer
needs a loop. It resolves once and reports an unresolved target; the
resolution
context is still entered and the bytes still counted, so the
per-conversion
document, depth and byte bounds are unchanged.

## 3. Reject an out-of-range event severity instead of clamping it

`uav:severity` had grown in this implementation without being a
specification
term. Rather than drop it, it is proposed upstream in

[OPCF-Members/spec-drafts#21](OPCF-Members/spec-drafts#21),
which defines it with an explicit rule: the value **shall** be in the
OPC 10000-5
range 1..1000, and a consumer **shall not** silently clamp it.

The implementation clamped, so a document asking for severity 5000 was
accepted
and published as 1000 — the mistake was hidden while what the author
asked for
was changed. An affordance carrying an out-of-range severity is now
skipped,
with the reason logged, and its half-built event type and
`GeneratesEvent`
reference are removed.

### Defect found while making that change

The occurrence-time path read `ValidateSeverity(severity) ??
tag.Severity`.
Because `ValidateSeverity(null)` returns the 500 default, it **never**
fell back
to the affordance's authored severity: an occurrence reported without a
severity
published 500 rather than the authored 900. Build-time validation and
the
occurrence-time choice are now separate methods.

## 4. Vendored examples and honest coverage claims

Examples 21 and 22 were missing from the vendored golden assets.
Converting
example 22 is now the end-to-end check for §5.2.1 — the specification's
own
example rather than a fixture written to match the implementation.

`docs/WotBindings.md` claimed ten of the eleven conformance units and
silently
omitted **WoT-ConditionMapping**, and said "twenty worked examples". The
count
is corrected, and WoT-ConditionMapping is now **covered** — see §5
below.

## 5. Alarms and Conditions (Binding §13)

The last unclaimed conformance unit. A projected Condition event now
derives
from the ConditionType it names rather than from `BaseEventType`;
falling back
would lose the Condition state model entirely, leaving a Client unable
to tell
an alarm from an ordinary event.

The two forms follow the hint-plus-pin pattern of §5.3:
`uav:conditionTypeId`
is definitive and wins, `uav:conditionType` is a readable hint resolved
for the
four ConditionTypes §13.1 scopes. An unpinned name outside that set is
reported
rather than guessed.

Four conformance rules are enforced, each because breaking it yields a
document
a consumer can read but cannot act on:

| Rule | Section | Diagnostic |
| --- | --- | --- |
| A Condition event declares `EventId` in its `data` | 13.3 |
`ConditionEventIdMissing` |
| `uav:conditionAction` is inside its closed set | 13.2 |
`InvalidConditionAction` |
| `uav:actsOn` names a Condition event in the same document | 13.4 |
`InvalidConditionTarget` |
| `Acknowledge`/`Confirm`/`AddComment` declare an `EventId` input | 13.4
| `ConditionActionInputMissing` |

`Enable` and `Disable` act on the Condition instance rather than one
occurrence
and are deliberately exempt from the last rule, pinned by a negative
test.
Shelving, suppression, dialog conditions and `ConditionRefresh` stay out
of
scope, as §13.1 scopes the mapping.

## 6. The sibling half of the §5.1.5 local context

`SnapshotWotNodeResolver` indexes the registry snapshot a conversion
runs over
and is wired into `WotNodeSetDocumentConverter`, so a document naming a
type
another document in the same registry projects resolves with no
AddressSpace at
all. Only Thing Models are indexed: a Thing Model projects a
`UAObjectType` and
is a valid target, a Thing Description projects an instance and never
is.

Identity comes from the new public
`WotNodeSetConverter.TryDescribeProjectedType`,
which applies exactly the rules the conversion uses, so an index entry
and the
projected node cannot drift apart. Ambiguity is preserved rather than
resolved:
two siblings sharing a qualified name yield both matches so the caller
can
report it.

## Deliberately not in this PR

- **The AddressSpace-backed `IWotNodeResolver`.** The abstraction, the
composite, the null default and the sibling-document implementation ship
here; the AddressSpace-backed half needs a cached type-hierarchy walk
behind
a server `OperationContext` and follows separately. Until it exists a
host
gets the sibling context plus the null fallback, which reports a binding
as
  unresolved rather than mistyping it.
- **Two §5.2.1 obligations that need the address space**: not
duplicating a
  mandatory instance declaration of the resolved type, and rejecting
  disagreement with an instantiated Thing Model.
- **Connectivity §7.3 parent resolution.**

Each is tracked; none is faked here.

## Related Issues

There is no tracking issue: this came out of a
spec-versus-implementation audit
rather than a reported defect, and the driving artifacts are the two
upstream
spec PRs linked above. Happy to open one if maintainers prefer — the
§5.2.1
follow-up in particular is worth an ADR, since the local-context
resolver is a
new injectable provider spanning two assemblies.

## Checklist

- [x] 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.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

### Verification detail

| | net10.0 | net48 |
|---|---|---|
| `Opc.Ua.Types.Tests` | 8573 / 8573 | 8555 / 8555 |
| `Opc.Ua.WotCon.Tests` | 1075 / 1075 | 1075 / 1075 |

Full `UA.slnx` build: **0 errors, 0 warnings** across all target
frameworks,
re-verified after `master` was merged into the branch.

Every new test was mutation-verified: the type-binding tests against
three
mutations (revert to the hardcoded default, drop the ambiguity check,
accept an
empty `href`), the severity tests against reinstating the clamp, and the
ReferenceType additions against removing them from the map. Each
mutation failed
the suite before the change was kept.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
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.

1 participant