3 Commits

Author SHA1 Message Date
Marc Schier e8bec27d5d Complete WoT binding preservation and two-pump aggregation (#4431)
# Description

Complete WoT Binding preservation and the two-pump aggregation workflow
without implicit overlays or multi-source action fan-out.

- Add verified linked-document export and bounded partition merging.
Preserve complete source facts, reject contradictory archival content,
and retain usable readable content when a future native grammar is
unsupported.
- Wire local Methods and Condition events through the existing
injectable binding/runtime infrastructure. Keep local and upstream
identities distinct, translate namespace-bearing values, and bound
occurrence routes by source and generation.
- Wait for asynchronous event-producer readiness before completing
subscription creation, so the first alarm is not lost. Preserve
cancellation, teardown, and startup-failure reporting.
- Import runtime types in inheritance order and attach typed Method
argument Properties. Keep generated Double spelling deterministic across
supported frameworks and remove an AOT-unsafe registry validation
conversion.
- Generate both pumps and their asset/group projections. Expose fifteen
typed readings per pump, separate source-owned controls, and
acknowledge/confirm/reset workflows. Extend the demo script for managed
and NativeAOT execution, bounded failures, and exact-process cleanup.
- Synchronize current-state documentation and specification fixtures.
Compose fixture paths from platform-independent components and emit/pin
LF pump XML so byte-exact regeneration holds on Windows, Linux, and
macOS.
- Keep sample JSON readable with sorted members, two-space indentation,
and LF line endings. Normalize generated filenames to single hyphens
without changing registry resource IDs, document values, logical links,
or dependency ordering; the manifest maps resource IDs to their physical
filenames.

Most changed files are deterministic regenerations of checked-in sample
documents. The demo manifest contains 533 linked resources; the
aggregation server remains generic and has no Pump-specific compiled
model dependency.

No new alarm vocabulary, shelving/suppression/dialog/ConditionRefresh
transport mapping, Azure SDK changes, or complete Pump/OpenUSD parity is
introduced.

## Related Issues

Fixes #4430, which records the approved implementation scope, design
decisions, and acceptance criteria.

Follow-up to #4411. Specification fixtures are pinned to
[OPCF-Members/spec-drafts commit
82620d9](https://github.com/OPCF-Members/spec-drafts/commit/82620d9e313f35386dfc7477f7875cd47d2fd268),
associated with OPCF-Members/spec-drafts#35.

## Validation

- .NET 10 and .NET 8: complete affected converter/schema, WoT
Connectivity, and binding suites passed.
- .NET Framework 4.8: affected libraries, converter/generator, projected
runtime, readiness, and NodeSet-import compatibility runs passed. The
existing filesystem test exceeding the host's legacy path limit passed
through a temporary short-path alias, without changing its assertions.
- All 29 aggregation sample tests passed, including the independent
companion-model oracle.
- Managed/default-port and NativeAOT/alternate-port demo runs completed
with 533 documents, two pumps, thirty Good readings, and all four
pump/source control and alarm workflows.
- Timeout and duplicate-port failure paths were exercised; owned
processes and ports were released.
- Specification artifact/rule checks, byte-identical example mirroring,
statement-digest verification, and changed-file whitespace checks
passed.
- The [Build and Test .NET 10.0 workflow for
278b092](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/34095520510)
passed, including the previously failing Linux/macOS Types and WoT
Connectivity jobs. Portability fixes preserve the existing assertions
and add an explicit LF-output regression.
- After the readability changes in `dc61d032`, all 131 targeted
sample/projected-runtime cases passed on both .NET 10 and .NET Framework
4.8, and all 29 aggregation sample tests passed after rebuilding. All
539 JSON assets meet the formatting and filename rules; the 538 document
JSON values are unchanged, with only physical paths changing in the
manifest. Byte-exact regeneration assertions remain in place.

Local validation was scoped to affected projects rather than the entire
UA.slnx solution. Numeric coverage percentages and remaining CI gates
remain for CI; coverage thresholds were not changed. Existing unrelated
analyzer warnings were left unchanged.

## Checklist

- [ ] 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.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] 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.
- [x] I have addressed **all** PR feedback received.

Regression and integration tests are included and passed as described
above; the combined tests/coverage checkbox remains open because numeric
coverage was not measured locally.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6a9f41b1-72f0-4c74-bd7f-26fa531e5738
2026-09-07 12:24:14 +00:00
Marc Schier 81f200c9d2 Align WoT conversion and runtime with Binding 1.1 (#4375)
## Summary

Align the generic WoT/NodeSet converter, WoT Connectivity registry, and
runtime
binding implementation with the OPC UA WoT Binding 1.1 draft.

### Conversion and preservation

- fix the standard placeholder ModellingRule NodeIds;
- make every produced/restored NodeSet importable by completing required
aliases;
- support `ua:ComponentOf`, arbitrary companion ReferenceTypes,
BrowseNames,
  InverseNames, symmetric references, and exact direction;
- enforce the projection annotation allowlist and deterministic total
order;
- emit and consume Method argument schemas, EventType data, Alarms &
Conditions,
  event severity, engineering units/ranges, localization, ValueRank, and
  ArrayDimensions;
- preserve unsupported content through JSON residue/native projection
rather than
  silently dropping it;
- compare residue conflicts with RFC 8785 canonical equality while
retaining
  byte-exact residue digests;
- stamp generated documents with `uav:bindingVersion: "1.1"`.

### Runtime bindings

- resolve an EventType Thing Model referenced with `tm:ref`, recursively
derive
  its `data` fields as the baseline selection, and overlay explicit
  `uav:eventSelectClauses` that also reference EventType TMs;
- pre-resolve EventType links asynchronously through the bounded
sibling-document
  resolver before synchronous protocol planning;
- retain read compatibility with legacy implementation-specific
`uav:eventFields`;
- expose nested event `data` matching the TD while retaining the flat
transport
  field index for compatibility;
- apply minimum-security constraints through deterministic endpoint
selection and
  fail-closed session verification;
- use one Unicode code-point comparer for projection membership and
endpoint ties.

### Validation and fixtures

- add strict, profile-aware validation while keeping default parsing
permissive and
  residue-preserving;
- pin 27 normative examples byte-for-byte to the specification
repository, with an
  offline SHA-256 manifest and regeneration test;
- regenerate checked-in WoT documents with the final canonical writer
and binding
  version;
- document the generic converter separately from the legacy OPC 10100-1
asset path.

## Notable compatibility changes

- converted NodeSets may gain a completed `<Aliases>` table so standard
  ReferenceType names can be imported;
- previously inverted/dangling placeholder ModellingRules are corrected;
- generated documents gain binding-version, localization,
rank/dimension, and exact
  ReferenceType metadata;
- event data now has the nested shape described by the binding;
- the former NodeId-based `uav:typeDefinitionId` clause is removed;
event
  selections now use EventType-TM `tm:ref` links;
- `uav:eventFields` remains readable but new documents use
  `uav:eventSelectClauses`;
- unknown well-formed future revision/profile claims produce unsupported
warnings
  rather than strict rejection.

## Generated files

Most of the 621-file diff is deterministic regeneration: 517 checked-in
WoT
documents gain the binding version and final canonical spelling.
Regeneration was
run twice and is byte-idempotent.

## Validation

- `dotnet restore UA.slnx`;
- `dotnet build UA.slnx -c Release` across all configured TFMs;
- `Opc.Ua.Types.Tests`: 9,215 net10.0; 9,208 net48; 9,208 net472;
- `Opc.Ua.WotCon.Bindings.Tests`: 611 each on net10.0, net9.0, and
net8.0;
- `Opc.Ua.WotCon.Tests`: 1,136 net10.0;
- `Opc.Ua.WotCon.Samples.Tests`: 14 net10.0;
- NativeAOT test executable: 136/136;
- all 27 strict fixture conversion/import checks;
- formatting, analyzers, `git diff --check`, and regeneration
idempotence.

The full net48 run retains one unrelated existing MAX_PATH failure in

`FileWotRegistryStoreTests.MultiLevelDirectoryCreationSyncsEveryModifiedParentInOrder`.
Solution-wide stress runs also include an intentional 60-minute soak and
unrelated
locale-sensitive/flaky tests; all affected WoT projects and tests pass.

## Specification

Depends on OPCF-Members/spec-drafts#31.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6a9f41b1-72f0-4c74-bd7f-26fa531e5738
2026-09-02 00:09:36 +00:00
Marc Schier a7c776e8d4 Align the WoT implementation with spec: type binding, removed vocabulary, Alarms and Conditions (#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](https://github.com/OPCF-Members/spec-drafts/pull/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](https://github.com/OPCF-Members/spec-drafts/pull/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
2026-08-14 07:55:33 -07:00